diff --git a/civet.dev/cheatsheet.md b/civet.dev/cheatsheet.md index f205abca..39511541 100644 --- a/civet.dev/cheatsheet.md +++ b/civet.dev/cheatsheet.md @@ -194,6 +194,14 @@ result! as string | number a + b = c +### Multi Assignment + + +(count[key] ?= 0)++ +(count[key] ?= 0) += 1 +++count *= 2 + + ### Humanized Operators diff --git a/source/parser.hera b/source/parser.hera index 86f084f3..fea36756 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -213,10 +213,19 @@ UnaryPostfix # https://262.ecma-international.org/#prod-UpdateExpression UpdateExpression # NOTE: Not allowing whitespace betwen prefix and postfix increment operators and operand - UpdateExpressionSymbol UnaryExpression + UpdateExpressionSymbol UnaryExpression -> + return { + type: "UpdateExpression", + assigned: $2, + children: $0, + } LeftHandSideExpression UpdateExpressionSymbol? -> - if ($2) return $0 - return $1 + if (!$2) return $1 + return { + type: "UpdateExpression", + assigned: $1, + children: $0, + } UpdateExpressionSymbol ("++" / "--") -> @@ -266,7 +275,9 @@ AssignmentExpressionTail ActualAssignment # NOTE: Eliminated left recursion # NOTE: Consolidated assignment ops - ( __ LeftHandSideExpression WAssignmentOp )+ ExtendedExpression -> + # NOTE: UpdateExpression instead of LeftHandSideExpression to allow + # e.g. ++x *= 2 which we later convert to ++x, x *= 2 + ( __ UpdateExpression WAssignmentOp )+ ExtendedExpression -> $1 = $1.map((x) => [x[0], x[1], ...x[2]]) $0 = [$1, $2] return { @@ -276,6 +287,7 @@ ActualAssignment // from fake assignments that only add a name to a scope names: null, lhs: $1, + assigned: $1[0][1], exp: $2, } @@ -2508,8 +2520,9 @@ Xnor /!\^\^?/ / "xnor" UnaryOp - # Lookahead to prevent unary operators from overriding block unary operator shorthand - /[!~+-](?!\s|[!~+-]*&)/ -> + # Lookahead to prevent unary operators from overriding update operators + # ++/-- or block unary operator shorthand + /(?!\+\+|--)[!~+-](?!\s|[!~+-]*&)/ -> return { $loc, token: $0 } ( Await / Delete / Void / Typeof ) __ Not # only when CoffeeNotEnabled (see definition of `Not`) @@ -6880,7 +6893,7 @@ Init function processAssignments(statements) { gatherRecursiveAll(statements, n => n.type === "AssignmentExpression" && n.names === null) .forEach(exp => { - let {lhs: $1, exp: $2} = exp, pre = [], tail = [], i = 0, len = $1.length + let {lhs: $1, exp: $2} = exp, tail = [], i = 0, len = $1.length // identifier= if ($1.some((left) => left[left.length-1].special)) { @@ -6895,20 +6908,6 @@ Init $2 = [ call, "(", lhs, ", ", $2, ")" ] } - // Move assignments within LHS to run earlier via comma operator - $1.forEach((lhsPart, i) => { - let expr = lhsPart[1] - while (expr.type === "ParenthesizedExpression") { - expr = expr.expression - } - if (expr.type === "AssignmentExpression") { - pre.push([lhsPart[1], ", "]) - const newLhs = expr.lhs[0][1] - // TODO: use ref to avoid duplicating function calls - lhsPart[1] = newLhs - } - }) - // Force parens around destructuring object assignments // Walk from left to right the minimal number of parens are added and enclose all destructured assignments // TODO: Could validate some lhs ecmascript rules here if we wanted to @@ -6980,9 +6979,53 @@ Init // Gather all identifier names from the lhs array const names = $1.flatMap(([,l]) => l.names || []) - exp.children = [...pre, $1, $2, ...tail] + exp.children = [$1, $2, ...tail] exp.names = names }) + + // Move assignments/updates within LHS of assignments/updates + // to run earlier via comma operator + gatherRecursiveAll(statements, n => n.type === "AssignmentExpression" || n.type === "UpdateExpression") + .forEach(exp => { + function extractAssignment(lhs) { + let expr = lhs + while (expr.type === "ParenthesizedExpression") { + expr = expr.expression + } + if (expr.type === "AssignmentExpression" || + expr.type === "UpdateExpression") { + if (expr.type === "UpdateExpression" && + expr.children[0] === expr.assigned) { // postfix update + post.push([", ", lhs]) + } else { + pre.push([lhs, ", "]) + } + // TODO: use ref to avoid duplicating function calls + return expr.assigned + } + } + const pre = [], post = [] + switch (exp.type) { + case "AssignmentExpression": + if (!exp.lhs) return + exp.lhs.forEach((lhsPart, i) => { + let newLhs = extractAssignment(lhsPart[1]) + if (newLhs) { + lhsPart[1] = newLhs + } + }) + break + case "UpdateExpression": + let newLhs = extractAssignment(exp.assigned) + if (newLhs) { + const i = exp.children.indexOf(exp.assigned) + exp.assigned = exp.children[i] = newLhs + } + break + } + if (pre.length) exp.children.unshift(...pre) + if (post.length) exp.children.push(...post) + }) } // Don't push to prelude unless ref actually ends up in final parse tree, diff --git a/test/assignment.civet b/test/assignment.civet index 602c67e9..cb8ed308 100644 --- a/test/assignment.civet +++ b/test/assignment.civet @@ -415,14 +415,62 @@ describe "assignment", -> (x += 1), x *= 2 """ - testCase.skip """ - initialize and ++ + testCase """ + parenthesized prefix ++ and multiply + --- + (++x) *= 2 + --- + (++x), x *= 2 + """ + + testCase """ + parenthesized suffix ++ and multiply + --- + (x++) *= 2 + --- + x *= 2, (x++) + """ + + testCase """ + prefix ++ and multiply + --- + ++x *= 2 + --- + ++x, x *= 2 + """ + + testCase """ + suffix ++ and multiply + --- + x++ *= 2 + --- + x *= 2, x++ + """ + + testCase """ + initialize and prefix ++ + --- + ++(x ?= 0) + --- + (x ??= 0), ++x + """ + + testCase """ + initialize and suffix ++ --- (x ?= 0)++ --- (x ??= 0), x++ """ + testCase """ + double ++ + --- + ++x++ + --- + ++x, x++ + """ + describe "function assignment operator", -> testCase """ space on both sides