From bfd6a7f2e30e03aac4751e000a1112cccd348222 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Thu, 31 Oct 2024 15:29:22 -0400 Subject: [PATCH 1/4] Use refs in `@` bind shorthand Part of #50 --- source/parser.hera | 25 ++++++++++++++++++------- source/parser/lib.civet | 13 +++++++------ source/parser/types.civet | 10 +++++++++- test/property-access.civet | 5 ++--- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index db531823..25932f79 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -1272,7 +1272,11 @@ FieldDefinition } ThisLiteral - This + This -> { + type: "Identifier", + name: "this", + children: [ $1 ], + } HashThis # NOTE: Added @identifier shorthand, also works for private identifiers # Converts 'IdentifierName' node to string so this won't interfere with refs @@ -1344,7 +1348,11 @@ LengthShorthand # NOTE: Added '@' as a 'this' shorthand from CoffeeScript AtThis At:at -> - return { ...at, token: "this" } + return { + type: "Identifier", + name: "this", + children: [{ ...at, token: "this" }] + } # https://262.ecma-international.org/#prod-LeftHandSideExpression LeftHandSideExpression @@ -7033,14 +7041,17 @@ JSXAttribute # NOTE: @foo and @@foo shorthands # for foo={this.foo} and foo={this.foo.bind(this)} AtThis:at Identifier?:id InlineJSXCallExpressionRest*:rest &JSXAttributeSpace -> - const access = id && { - type: "PropertyAccess", - children: [".", id], - name: id, + const children = [ at, ...rest.flat() ] + if (id) { + children.splice(1, 0, { + type: "PropertyAccess", + children: [".", id], + name: id, + }) } const expr = processCallMemberExpression({ type: "CallExpression", - children: [ at, access, ...rest.flat() ], + children, }) const last = lastAccessInCallExpression(expr) if (!last) return $skip diff --git a/source/parser/lib.civet b/source/parser/lib.civet index 0ccf01ba..065698ec 100644 --- a/source/parser/lib.civet +++ b/source/parser/lib.civet @@ -633,19 +633,20 @@ function processCallMemberExpression(node: CallExpression | MemberExpression): A children: [object, ...children[i+1..]] }) else if glob?.type is "PropertyBind" - // TODO: add ref to ensure object base evaluated only once - prefix := children[0...i] + assert.notEqual i, 0, "@ bind must be preceded by an expression" + prefix := i is 1 ? children[0] : children[ 0 ? [", "] : [] ...glob.args ")" @@ -655,9 +656,9 @@ function processCallMemberExpression(node: CallExpression | MemberExpression): A ] }) else if glob is like {type: "Index", mod: true} - assert.notEqual i, 0, "Index modifier must be preceded by an expression" + assert.notEqual i, 0, "Index access must be preceded by an expression" prefix := i is 1 ? children[0] : children[ --- x.y@z --- - x.y.z.bind(x.y) + let ref;(ref = x.y).z.bind(ref) """ testCase """ @@ -391,13 +391,12 @@ describe "property access", -> x.y.bind(x).z """ - // TODO: needs ref testCase """ multiple bind --- x@y@z --- - x.y.bind(x).z.bind(x.y.bind(x)) + let ref;(ref = x.y.bind(x)).z.bind(ref) """ testCase """ From d325fb785cfe8248a315dbba7be9d121a1924dbb Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Thu, 31 Oct 2024 15:42:14 -0400 Subject: [PATCH 2/4] Move `replaceNode`(s) to `source/parser/util.civet` --- source/parser/function.civet | 5 +-- source/parser/lib.civet | 58 ++------------------------- source/parser/op.civet | 1 + source/parser/pattern-matching.civet | 5 +-- source/parser/util.civet | 59 ++++++++++++++++++++++++++-- 5 files changed, 61 insertions(+), 67 deletions(-) diff --git a/source/parser/function.civet b/source/parser/function.civet index 92eeff9e..541f7571 100644 --- a/source/parser/function.civet +++ b/source/parser/function.civet @@ -43,10 +43,6 @@ import { getHelperRef } from ./helper.civet -import { - replaceNode -} from ./lib.civet - import { findAncestor findChildIndex @@ -74,6 +70,7 @@ import { isWhitespaceOrEmpty makeLeftHandSideExpression makeNode + replaceNode startsWithPredicate trimFirstSpace updateParentPointers diff --git a/source/parser/lib.civet b/source/parser/lib.civet index 065698ec..e355d164 100644 --- a/source/parser/lib.civet +++ b/source/parser/lib.civet @@ -65,7 +65,6 @@ import { inplaceInsertTrimmingSpace inplacePrepend insertTrimmingSpace - isASTNodeObject isComma isEmptyBareBlock isFunction @@ -77,6 +76,8 @@ import { maybeUnwrap parenthesizeType prepend + replaceNode + replaceNodes stripTrailingImplicitComma trimFirstSpace wrapIIFE @@ -722,36 +723,6 @@ function processCallMemberExpression(node: CallExpression | MemberExpression): A } return node -/** - * Replace this node with another, by modifying its parent's children. - */ -function replaceNode(node: ASTNodeObject, newNode: ASTNode, parent?: ASTNodeParent): void - parent ??= node.parent - unless parent? - throw new Error "replaceNode failed: node has no parent" - - function recurse(children: ASTNode[]): boolean - for each child, i of children - if child is node - children[i] = newNode - return true - else if Array.isArray child - return true if recurse child - return false - - unless recurse parent.children - throw new Error "replaceNode failed: didn't find child node in parent" - - // Adjust 'expression' etc. alias pointers - for key, value in parent - if value is node - parent[key] = newNode - - if isASTNodeObject newNode - newNode.parent = parent - // Don't destroy node's parent, as we often include it within newNode - //node.parent = undefined - // Wrap expression in parentheses to make into a statement when: // * object literal expression // * anonymous function expression @@ -1689,29 +1660,6 @@ function reorderBindingRestProperty(props) return { children, names } -/** - * Replace all nodes that match predicate with replacer(node) - */ -function replaceNodes(root, predicate, replacer) - return root unless root? - - array := Array.isArray(root) ? root : root.children - - unless array - if predicate root - return replacer root, root - else - return root - - for each node, i of array - return unless node? - if predicate node - array[i] = replacer node, root - else - replaceNodes node, predicate, replacer - - return root - function typeOfJSX(node, config) { switch (node.type) { case "JSXElement": @@ -1845,8 +1793,8 @@ export { processUnaryExpression processUnaryNestedExpression quoteString - replaceNode reorderBindingRestProperty + replaceNode replaceNodes skipImplicitArguments stripTrailingImplicitComma diff --git a/source/parser/op.civet b/source/parser/op.civet index a8e4b92b..3529e8e5 100644 --- a/source/parser/op.civet +++ b/source/parser/op.civet @@ -7,6 +7,7 @@ import type { import { makeLeftHandSideExpression + replaceNode trimFirstSpace } from ./util.civet diff --git a/source/parser/pattern-matching.civet b/source/parser/pattern-matching.civet index 79664f2e..1ae26b1a 100644 --- a/source/parser/pattern-matching.civet +++ b/source/parser/pattern-matching.civet @@ -14,10 +14,6 @@ import type { SwitchStatement } from ./types.civet -import { - replaceNode -} from ./lib.civet - import { gatherRecursiveAll } from ./traversal.civet @@ -27,6 +23,7 @@ import { isExit makeLeftHandSideExpression makeNode + replaceNode updateParentPointers } from ./util.civet diff --git a/source/parser/util.civet b/source/parser/util.civet index fa4ebe8b..43a6acba 100644 --- a/source/parser/util.civet +++ b/source/parser/util.civet @@ -14,10 +14,6 @@ import type { StatementTuple } from ./types.civet -import { - replaceNode -} from ./lib.civet - import { gatherRecursiveWithinFunction type Predicate @@ -457,6 +453,59 @@ function deepCopy(root: T): T copied.get node +/** + * Replace this node with another, by modifying its parent's children. + */ +function replaceNode(node: ASTNodeObject, newNode: ASTNode, parent?: ASTNodeParent): void + parent ??= node.parent + unless parent? + throw new Error "replaceNode failed: node has no parent" + + function recurse(children: ASTNode[]): boolean + for each child, i of children + if child is node + children[i] = newNode + return true + else if Array.isArray child + return true if recurse child + return false + + unless recurse parent.children + throw new Error "replaceNode failed: didn't find child node in parent" + + // Adjust 'expression' etc. alias pointers + for key, value in parent + if value is node + parent[key] = newNode + + if isASTNodeObject newNode + newNode.parent = parent + // Don't destroy node's parent, as we often include it within newNode + //node.parent = undefined + +/** + * Replace all nodes that match predicate with replacer(node) + */ +function replaceNodes(root, predicate, replacer) + return root unless root? + + array := Array.isArray(root) ? root : root.children + + unless array + if predicate root + return replacer root, root + else + return root + + for each node, i of array + return unless node? + if predicate node + array[i] = replacer node, root + else + replaceNodes node, predicate, replacer + + return root + /** * When cloning subtrees sometimes we need to remove hoistDecs */ @@ -786,6 +835,8 @@ export { prepend removeHoistDecs removeParentPointers + replaceNode + replaceNodes skipIfOnlyWS startsWith startsWithPredicate From e968e7038d5ca122617afe1321468aea89594708 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Thu, 31 Oct 2024 17:17:52 -0400 Subject: [PATCH 3/4] Avoid duplicate calls in relation chains via refs Fixes #50 --- source/parser/op.civet | 38 +++++++++++++++++++++++++--------- source/parser/ref.civet | 1 + source/parser/types.civet | 2 +- source/parser/util.civet | 4 ++-- test/binary-op.civet | 2 +- test/chained-comparisons.civet | 12 ++++++----- 6 files changed, 40 insertions(+), 19 deletions(-) diff --git a/source/parser/op.civet b/source/parser/op.civet index 3529e8e5..44fa27ce 100644 --- a/source/parser/op.civet +++ b/source/parser/op.civet @@ -6,6 +6,7 @@ import type { } from ./types.civet import { + assert makeLeftHandSideExpression replaceNode trimFirstSpace @@ -15,6 +16,10 @@ import { processPatternTest } from ./pattern-matching.civet +import { + maybeRefAssignment +} from ./ref.civet + // Binary operator precedence, from low to high // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table // NOTE: Added ^^ between || and &&, just like ^ is between | and & @@ -285,7 +290,7 @@ function expandChainedComparisons([first, binops]: [ASTNode, [ASTNode, BinaryOp, return results function processChains: void - if chains.length > 0 + if chains# > 0 // At least one relational op, so expand any existence operators `x?` first = expandExistence first for each index, k of chains @@ -297,28 +302,41 @@ function expandChainedComparisons([first, binops]: [ASTNode, [ASTNode, BinaryOp, exp := binop[3] = expandExistence binop[3] results.push first - endIndex := chains[k + 1] ?? i + 1 - results.push ...binops[start...endIndex].flat() - // TODO: add refs to ensure middle expressions are evaluated only once - // NOTE: This first gets discarded if we're in the last iteration of the chain - first = [exp] ++ binops[index + 1...endIndex] - start = endIndex + // If there's more to the chain, ref the right-hand side of this + // relation (which is the left-hand side of the next op) + if k+1 < chains# + endIndex := chains[k + 1] + rhs := + index + 1 < endIndex ? [exp] ++ binops[index + 1...endIndex] : exp + { ref, refAssignment } := maybeRefAssignment rhs + // TODO: do we need to recurse on the binary ops here? + binops[index][3] = makeLeftHandSideExpression refAssignment ?? rhs + results.push ...binops[start...index + 1].flat() + first = ref + start = endIndex + else + results.push ...binops[start...i + 1].flat() else // Advance start if there was no chain results.push first results.push ...binops[start...i + 1].flat() - start = i + 1 + start = i + 1 chains.length = 0 function expandExistence(exp: ASTNode): ASTNode // Expand existence operator like x? if existence := isExistence(exp) + { ref, refAssignment } := maybeRefAssignment existence.expression + if refAssignment? + replaceNode + existence.expression + makeLeftHandSideExpression refAssignment + existence results.push existence, " ", chainOp, " " - existence.expression + ref else exp - ; // avoid implicit return of hoisted function export { getPrecedence diff --git a/source/parser/ref.civet b/source/parser/ref.civet index 44ca1432..a06389c6 100644 --- a/source/parser/ref.civet +++ b/source/parser/ref.civet @@ -35,6 +35,7 @@ function needsRef(expression: ASTNode, base = "ref"): ASTRef | undefined case "Ref": case "Identifier": case "Literal": + case "Placeholder": return } return makeRef(base) diff --git a/source/parser/types.civet b/source/parser/types.civet index 827639f3..312aaba9 100644 --- a/source/parser/types.civet +++ b/source/parser/types.civet @@ -194,7 +194,6 @@ export type BinaryOp = (string & ) | (ChainOp & token?: never relational?: never - assoc?: never ) export type NonNullAssertion @@ -214,6 +213,7 @@ export type ChainOp type: "ChainOp" special: true prec: number + assoc: string children?: never parent?: never diff --git a/source/parser/util.civet b/source/parser/util.civet index 43a6acba..5dfcb518 100644 --- a/source/parser/util.civet +++ b/source/parser/util.civet @@ -456,8 +456,8 @@ function deepCopy(root: T): T /** * Replace this node with another, by modifying its parent's children. */ -function replaceNode(node: ASTNodeObject, newNode: ASTNode, parent?: ASTNodeParent): void - parent ??= node.parent +function replaceNode(node: ASTNode, newNode: ASTNode, parent?: ASTNodeParent): void + parent ??= (node as ASTNodeObject?)?.parent unless parent? throw new Error "replaceNode failed: node has no parent" diff --git a/test/binary-op.civet b/test/binary-op.civet index 8c5b56bc..d7952baf 100644 --- a/test/binary-op.civet +++ b/test/binary-op.civet @@ -366,7 +366,7 @@ describe "binary operations", -> --- a+b is in c*d is in e%f+g --- - (c*d).includes(a+b) && (e%f+g).includes(c*d) + let ref;(ref = c*d).includes(a+b) && (e%f+g).includes(ref) """ testCase """ diff --git a/test/chained-comparisons.civet b/test/chained-comparisons.civet index 8e6ebe53..b3bc5710 100644 --- a/test/chained-comparisons.civet +++ b/test/chained-comparisons.civet @@ -24,15 +24,15 @@ describe "chained comparisons", -> --- a + b < c + d < e + f --- - a + b < c + d && c + d < e + f + let ref;a + b < (ref = c + d) && ref < e + f """ testCase """ - higher precedence + more higher precedence --- a + b + x + y < c + d < e + f --- - a + b + x + y < c + d && c + d < e + f + let ref;a + b + x + y < (ref = c + d) && ref < e + f """ testCase """ @@ -68,9 +68,11 @@ describe "chained comparisons", -> --- (a < b) < c (a + b) < (c + d) < (e + f) + (a < b) < c --- - (a < b) < c; - (a + b) < (c + d) && (c + d) < (e + f) + (a < b) < c + let ref;(a + b) < (ref = (c + d)) && ref < (e + f); + (a < b) < c """ testCase """ From 01b39ae9a7aa5ab4e12ef6f17831a48342aba0c5 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Fri, 1 Nov 2024 14:04:26 -0400 Subject: [PATCH 4/4] Add recursion to handle custom binary ops in refs --- source/parser/op.civet | 302 +++++++++++++++++---------------- test/chained-comparisons.civet | 24 +++ 2 files changed, 176 insertions(+), 150 deletions(-) diff --git a/source/parser/op.civet b/source/parser/op.civet index 44fa27ce..1725da1b 100644 --- a/source/parser/op.civet +++ b/source/parser/op.civet @@ -66,159 +66,159 @@ function getPrecedence(op: BinaryOp): number precedenceMap.get(op.prec ?? op.token) ?? if op.relational then precedenceRelational else precedenceCustomDefault -function processBinaryOpExpression($0) - return recurse expandChainedComparisons($0) - - // Expanded ops is [a, __, op1, __, b, __, op2, __, c, __, op3, __, d], etc. - // NOTE: all operators of higher precedence than relational have been merged into the operand expressions - function recurse(expandedOps) - let i = 2 - while i < expandedOps.length - let op: BinaryOp = expandedOps[i] - // a not instanceof b -> !(a instanceof b) - // Above is high precedence; below are relational so low precedence. - // a in b -> indexOf.call(b, a) >= 0 - // a is in b -> b.includes(a) - // a not in b -> indexOf.call(b, a) < 0 - // a is not in b -> !b.includes(a) - if op.special - let start = i - 2, end = i + 2 - prec := getPrecedence op - - // Grow argument range left or right, and return whether next - // operator is at the same precedence level as op. - function advanceLeft(allowEqual: boolean): boolean - while start >= 4 - prevPrec := getPrecedence expandedOps[start - 2] - unless prevPrec > prec or (allowEqual and prevPrec is prec) - return prevPrec is prec - start -= 4 - false - function advanceRight(allowEqual: boolean): boolean - while end + 4 < expandedOps.length - nextPrec := getPrecedence expandedOps[end + 2] - unless nextPrec > prec or (allowEqual and nextPrec is prec) - return nextPrec is prec - end += 4 - false - - let error - switch op.assoc - when "left", undefined - advanceLeft true - advanceRight false - when "right" - advanceLeft false - advanceRight true - when "non" - if advanceLeft(false) or advanceRight(false) - error = - type: "Error" - message: `non-associative operator ${op.token} used at same precedence level without parenthesization` - when "arguments" - if advanceLeft false - error = - type: "Error" - message: `arguments operator ${op.token} used at same precedence level as ${expandedOps[start - 2].token} to the left` - advanceRight true - else - throw new Error `Unsupported associativity: ${op.assoc}` - - let a = start === i - 2 - ? expandedOps[start] - : expandedOps.slice(start, i - 1) - let wsOp = expandedOps[i - 1] - //let op = expandedOps[i] - let wsB = expandedOps[i + 1] - let b = end === i + 2 - ? expandedOps[i + 2] - : expandedOps.slice(i + 2, end + 1) - if op.assoc is "arguments" - i .= 2 - while i < b.length - if prec is getPrecedence b[i] - unless b[i].token is op.token - error ?= - type: "Error" - message: `arguments operator ${op.token} used at same precedence level as ${b[i].token} to the right` - b[i] = "," - i += 4 +function processBinaryOpExpression($0: [ASTNode, [ASTNode, BinaryOp, ASTNode, ASTNode][]]) + return processExpandedBinaryOpExpression expandChainedComparisons($0) + +// Expanded ops is [a, __, op1, __, b, __, op2, __, c, __, op3, __, d], etc. +// NOTE: all operators of higher precedence than relational have been merged into the operand expressions +function processExpandedBinaryOpExpression(expandedOps: ASTNode[]) + let i = 2 + while i < expandedOps.length + op .= expandedOps[i] as BinaryOp + // a not instanceof b -> !(a instanceof b) + // Above is high precedence; below are relational so low precedence. + // a in b -> indexOf.call(b, a) >= 0 + // a is in b -> b.includes(a) + // a not in b -> indexOf.call(b, a) < 0 + // a is not in b -> !b.includes(a) + if op.special + let start = i - 2, end = i + 2 + prec := getPrecedence op + + // Grow argument range left or right, and return whether next + // operator is at the same precedence level as op. + function advanceLeft(allowEqual: boolean): boolean + while start >= 4 + prevPrec := getPrecedence expandedOps[start - 2] + unless prevPrec > prec or (allowEqual and prevPrec is prec) + return prevPrec is prec + start -= 4 + false + function advanceRight(allowEqual: boolean): boolean + while end + 4 < expandedOps.length + nextPrec := getPrecedence expandedOps[end + 2] + unless nextPrec > prec or (allowEqual and nextPrec is prec) + return nextPrec is prec + end += 4 + false + + let error + switch op.assoc + when "left", undefined + advanceLeft true + advanceRight false + when "right" + advanceLeft false + advanceRight true + when "non" + if advanceLeft(false) or advanceRight(false) + error = + type: "Error" + message: `non-associative operator ${op.token} used at same precedence level without parenthesization` + when "arguments" + if advanceLeft false + error = + type: "Error" + message: `arguments operator ${op.token} used at same precedence level as ${expandedOps[start - 2].token} to the left` + advanceRight true else - b = recurse b - - if op.token is "instanceof" - // Ensure space around `instanceof` - if wsOp.length is 0 - wsOp = " " - if wsB.length is 0 - wsB = " " - - // typeof shorthand: x instanceof "string" -> typeof x === "string" - if b is like { - type: "Literal" - children: [ {type: "StringLiteral"}, ... ] - } - a = ["typeof ", makeLeftHandSideExpression(a)] - if op.negated - op = { ...op, token: "!==", negated: false } - else - op = { ...op, token: "===" } - - if op.asConst - a = makeAsConst a - b = makeAsConst b - - let children, type: string? - if op.type is "PatternTest" - children = [processPatternTest a, b] - else if op.type is "ChainOp" - children = [a, wsOp, "&&", wsB, b] - // Parenthesize chain if it is surrounded by operator with precedence - // in between && (where this will end up being) and relational - // (which the chain should simulate). - if (start-2 >= 0 and - getPrecedence(expandedOps[start-2]) >= precedenceAnd and - expandedOps[start-2].token is not '&&') or - (end+2 < expandedOps.length and - getPrecedence(expandedOps[end+2]) >= precedenceAnd and - expandedOps[end+2].token is not '&&') - children = ["(", ...children, ")"] - else if op.call - wsOp = trimFirstSpace wsOp - if op.reversed - wsB = trimFirstSpace wsB - children = [wsOp, op.call, "(", wsB, b, ", ", a, ")", op.suffix] + throw new Error `Unsupported associativity: ${op.assoc}` + + let a = start === i - 2 + ? expandedOps[start] + : expandedOps.slice(start, i - 1) + let wsOp = expandedOps[i - 1] + //let op = expandedOps[i] + let wsB = expandedOps[i + 1] + let b = end === i + 2 + ? expandedOps[i + 2] + : expandedOps.slice(i + 2, end + 1) + if op.assoc is "arguments" + i .= 2 + while i < b.length + if prec is getPrecedence b[i] + unless b[i].token is op.token + error ?= + type: "Error" + message: `arguments operator ${op.token} used at same precedence level as ${b[i].token} to the right` + b[i] = "," + i += 4 + else + b = processExpandedBinaryOpExpression b + + if op.token is "instanceof" + // Ensure space around `instanceof` + if wsOp.length is 0 + wsOp = " " + if wsB.length is 0 + wsB = " " + + // typeof shorthand: x instanceof "string" -> typeof x === "string" + if b is like { + type: "Literal" + children: [ {type: "StringLiteral"}, ... ] + } + a = ["typeof ", makeLeftHandSideExpression(a)] + if op.negated + op = { ...op, token: "!==", negated: false } else - children = [wsOp, op.call, "(", a, ",", wsB, b, ")", op.suffix] - type = "CallExpression" - else if op.method - wsOp = trimFirstSpace wsOp + op = { ...op, token: "===" } + + if op.asConst + a = makeAsConst a + b = makeAsConst b + + let children, type: string? + if op.type is "PatternTest" + children = [processPatternTest a, b] + else if op.type is "ChainOp" + children = [a, wsOp, "&&", wsB, b] + // Parenthesize chain if it is surrounded by operator with precedence + // in between && (where this will end up being) and relational + // (which the chain should simulate). + if (start-2 >= 0 and + getPrecedence(expandedOps[start-2]) >= precedenceAnd and + expandedOps[start-2].token is not '&&') or + (end+2 < expandedOps.length and + getPrecedence(expandedOps[end+2]) >= precedenceAnd and + expandedOps[end+2].token is not '&&') + children = ["(", ...children, ")"] + else if op.call + wsOp = trimFirstSpace wsOp + if op.reversed wsB = trimFirstSpace wsB - if op.reversed - b = makeLeftHandSideExpression b unless b.type is "CallExpression" - b = dotNumericLiteral b - children = [wsB, b, wsOp, ".", op.method, "(", a, ")"] - else - a = makeLeftHandSideExpression a unless a.type is "CallExpression" - a = dotNumericLiteral a - children = [a, wsOp, ".", op.method, "(", wsB, b, ")"] - type = "CallExpression" - else if op.token - children = [a, wsOp, op, wsB, b] - if (op.negated) children = ["(", ...children, ")"] + children = [wsOp, op.call, "(", wsB, b, ", ", a, ")", op.suffix] + else + children = [wsOp, op.call, "(", a, ",", wsB, b, ")", op.suffix] + type = "CallExpression" + else if op.method + wsOp = trimFirstSpace wsOp + wsB = trimFirstSpace wsB + if op.reversed + b = makeLeftHandSideExpression b unless b.type is "CallExpression" + b = dotNumericLiteral b + children = [wsB, b, wsOp, ".", op.method, "(", a, ")"] else - throw new Error("Unknown operator: " + JSON.stringify(op)) - if (op.negated) children.unshift("!") - children.push error if error? - - expandedOps.splice(start, end - start + 1, { - type - children - }) - i = start + 2 + a = makeLeftHandSideExpression a unless a.type is "CallExpression" + a = dotNumericLiteral a + children = [a, wsOp, ".", op.method, "(", wsB, b, ")"] + type = "CallExpression" + else if op.token + children = [a, wsOp, op, wsB, b] + if (op.negated) children = ["(", ...children, ")"] else - i += 4 - expandedOps + throw new Error("Unknown operator: " + JSON.stringify(op)) + if (op.negated) children.unshift("!") + children.push error if error? + + expandedOps.splice(start, end - start + 1, { + type + children + }) + i = start + 2 + else + i += 4 + expandedOps /** Add dot after numeric literal if needed for method calls. */ function dotNumericLiteral(literal: ASTNode) @@ -307,9 +307,11 @@ function expandChainedComparisons([first, binops]: [ASTNode, [ASTNode, BinaryOp, if k+1 < chains# endIndex := chains[k + 1] rhs := - index + 1 < endIndex ? [exp] ++ binops[index + 1...endIndex] : exp + if index + 1 < endIndex + processExpandedBinaryOpExpression [exp] ++ binops[index + 1...endIndex].flat() + else + exp { ref, refAssignment } := maybeRefAssignment rhs - // TODO: do we need to recurse on the binary ops here? binops[index][3] = makeLeftHandSideExpression refAssignment ?? rhs results.push ...binops[start...index + 1].flat() first = ref diff --git a/test/chained-comparisons.civet b/test/chained-comparisons.civet index b3bc5710..d666548a 100644 --- a/test/chained-comparisons.civet +++ b/test/chained-comparisons.civet @@ -122,6 +122,18 @@ describe "chained comparisons", -> i != null && 0 <= i && i < n && x != null && typeof x === 'object' || a != null && b != null && a < b && c != null && b < c """ + testCase """ + complex expression inside ? + --- + f()? < n + 0 <= f()? + 0 <= f()? < n + --- + let ref;(ref = f()) != null && ref < n + let ref1;(ref1 = f()) != null && 0 <= ref1 + let ref2;(ref2 = f()) != null && 0 <= ref2 && ref2 < n + """ + testCase """ snug chained comparisons --- @@ -145,3 +157,15 @@ describe "chained comparisons", -> --- a!=b && b!=c """ + + testCase """ + with custom operator processing + --- + operator mul same (*) (x,y) x*y + operator add same (+) (x,y) x+y + a < b mul c add d mul e < f + --- + function mul (x,y) { return x*y } + function add (x,y) { return x+y } + let ref;a < (ref = add(mul(b, c), mul(d, e))) && ref < f + """