diff --git a/civet.dev/cheatsheet.md b/civet.dev/cheatsheet.md index 483e4394..bc2e7d4c 100644 --- a/civet.dev/cheatsheet.md +++ b/civet.dev/cheatsheet.md @@ -319,6 +319,7 @@ a + b = c a is b a is not b +not a a and b a or b a not in b @@ -326,6 +327,17 @@ a not instanceof b a? +### Two Levels of Precedence + +Like Perl, Ruby, and LiveScript, `and`/`or`/`not` have lower precedence +than `&&`/`||`/`!`/comparisons (unless you [explicitly disable via +`"civet coffeeAndOrNot"`](#coffeescript-operators)). + + +not a == b +a || b and c || d + + ### Includes Operator @@ -1440,6 +1452,11 @@ x == y != z x isnt y + +"civet coffeeAndOr" +a || b and c || d + + "civet coffeeNot" not (x == y) diff --git a/notes/Comparison-to-CoffeeScript.md b/notes/Comparison-to-CoffeeScript.md index 7df0c8e8..ba94977d 100644 --- a/notes/Comparison-to-CoffeeScript.md +++ b/notes/Comparison-to-CoffeeScript.md @@ -154,6 +154,7 @@ Civet provides a compatibility prologue directive that aims to be 97+% compatibl | Configuration | What it enables | |---------------------|---------------------------------------------------------------------| | autoVar | declare implicit vars based on assignment to undeclared identifiers | +| coffeeAndOr | `and` → `&&`, `or` → `||` with same precedence | | coffeeBooleans | `yes`, `no`, `on`, `off` | | coffeeComment | `# single line comments` | | coffeeDo | `do ->`, disables ES6 do/while | @@ -161,7 +162,7 @@ Civet provides a compatibility prologue directive that aims to be 97+% compatibl | coffeeForLoops | for in, of, from loops behave like they do in CoffeeScript | | coffeeInterpolation | `"a string with #{myVar}"` | | coffeeIsnt | `isnt` → `!==` | -| coffeeNot | `not` → `!` | +| coffeeNot | `not` → `!` with same precedence | | coffeeOf | `a of b` → `a in b`, `a not of b` → `!(a in b)`, `a in b` → `b.indexOf(a) >= 0`, `a not in b` → `b.indexOf(a) < 0` | | coffeePrototype | enables `x::` -> `x.prototype` and `x::y` -> `x.prototype.y` shorthand. diff --git a/source/lib.js b/source/lib.js index 69f59600..0cfc276b 100644 --- a/source/lib.js +++ b/source/lib.js @@ -469,6 +469,18 @@ function expressionizeIteration(exp) { ) } +/** +* binops is an array of [__, op, __, exp] tuples +* first is an expression +*/ +function processLowBinaryOpExpression([first, binops]) { + const out = [makeLeftHandSideExpression(first)] + binops.forEach(([pre, op, post, exp]) => { + out.push(pre, op, post, makeLeftHandSideExpression(exp)) + }) + return out +} + function processBinaryOpExpression($0) { const expandedOps = expandChainedComparisons($0) @@ -1410,6 +1422,10 @@ function makeLeftHandSideExpression(expression) { case "CallExpression": case "MemberExpression": case "ParenthesizedExpression": + case "DebuggerExpression": // wrapIIFE + case "SwitchExpression": // wrapIIFE + case "ThrowExpression": // wrapIIFE + case "TryExpression": // wrapIIFE return expression default: return { @@ -2820,6 +2836,14 @@ function processReturnValue(func) { return true } +function processLowUnaryExpression(pre, exp) { + if (!pre.length) return exp + return { + type: "UnaryExpression", + children: [...pre, makeLeftHandSideExpression(exp)], + } +} + function processUnaryExpression(pre, exp, post) { if (!(pre.length || post)) return exp // Handle "?" postfix @@ -3154,6 +3178,8 @@ module.exports = { processCoffeeInterpolation, processConstAssignmentDeclaration, processLetAssignmentDeclaration, + processLowBinaryOpExpression, + processLowUnaryExpression, processParams, processProgram, processReturnValue, diff --git a/source/main.coffee b/source/main.coffee index e2c419ab..c1a33887 100644 --- a/source/main.coffee +++ b/source/main.coffee @@ -76,6 +76,10 @@ uncacheable = new Set [ "JSXOptionalClosingFragment" "JSXTag" "LeftHandSideExpression" + "LowBinaryOpExpression" + "LowBinaryOpRHS" + "LowRHS" + "LowUnaryExpression" "MemberExpression" "MemberExpressionRest" "Nested" @@ -126,6 +130,7 @@ uncacheable = new Set [ "SingleLineAssignmentExpression" "SingleLineBinaryOpRHS" "SingleLineComment" + "SingleLineLowBinaryOpRHS" "SingleLineStatements" "SnugNamedProperty" "Statement" diff --git a/source/parser.hera b/source/parser.hera index df81a99a..81437775 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -34,6 +34,8 @@ const { processCoffeeInterpolation, processConstAssignmentDeclaration, processLetAssignmentDeclaration, + processLowBinaryOpExpression, + processLowUnaryExpression, processProgram, processUnaryExpression, quoteString, @@ -259,10 +261,31 @@ NonPipelineArgumentPart } return $1 +# NOTE: Match low-precedence binary ops first +LowBinaryOpExpression + LowUnaryExpression LowBinaryOpRHS* -> + if (!$2.length) return $1 + return processLowBinaryOpExpression($0) + +# NOTE: Excluding low-precedence binary ops BinaryOpExpression UnaryExpression BinaryOpRHS* -> - if ($2.length) return processBinaryOpExpression($0) - return $1 + if (!$2.length) return $1 + return processBinaryOpExpression($0) + +LowBinaryOpRHS + # Snug binary ops a+b + LowBinaryOp:op LowRHS:rhs -> + // Insert empty whitespace placeholder to maintan structure + return [[], op, [], rhs] + # Spaced binary ops a + b + # a + # + b + # Does not match + # a + # +b + NewlineBinaryOpAllowed ( NotDedented LowBinaryOp ( _ / ( EOS __ ) ) LowRHS ):rhs -> rhs + !NewlineBinaryOpAllowed SingleLineLowBinaryOpRHS -> $2 BinaryOpRHS # Snug binary ops a+b @@ -278,12 +301,23 @@ BinaryOpRHS NewlineBinaryOpAllowed ( NotDedented BinaryOp ( _ / ( EOS __ ) ) RHS ):rhs -> rhs !NewlineBinaryOpAllowed SingleLineBinaryOpRHS -> $2 +SingleLineLowBinaryOpRHS + # NOTE: It's named single line but that's only for the operator, the RHS can be after a newline + # This is to maintain compatibility with CoffeeScript conditions + _?:ws1 LowBinaryOp:op ( _ / ( EOS __ ) ):ws2 RHS:rhs -> + return [ws1 || [], op, ws2, rhs] + SingleLineBinaryOpRHS # NOTE: It's named single line but that's only for the operator, the RHS can be after a newline # This is to maintain compatibility with CoffeeScript conditions _?:ws1 BinaryOp:op ( _ / ( EOS __ ) ):ws2 RHS:rhs -> return [ws1 || [], op, ws2, rhs] +LowRHS + ParenthesizedAssignment + LowUnaryExpression + ExpressionizedStatement + RHS ParenthesizedAssignment UnaryExpression @@ -292,7 +326,13 @@ RHS ParenthesizedAssignment InsertOpenParen ActualAssignment InsertCloseParen +LowUnaryExpression + # NOTE: Here is the transition from low precedence to high precedence + LowUnaryOp*:pre BinaryOpExpression:exp -> + return processLowUnaryExpression(pre, exp) + # https://262.ecma-international.org/#prod-UnaryExpression +# but excluding low-precedence unary ops UnaryExpression # NOTE: Merged AwaitExpression with UnaryOp # https://262.ecma-international.org/#prod-AwaitExpression @@ -465,7 +505,7 @@ NestedTernaryRest # https://262.ecma-international.org/#prod-ShortCircuitExpression ShortCircuitExpression # NOTE: We don't need to track the precedence of all the binary operators so they all collapse into this - BinaryOpExpression + LowBinaryOpExpression PipelineExpression _?:ws PipelineHeadItem:head ( NotDedented Pipe __ PipelineTailItem )+:body -> @@ -2597,6 +2637,10 @@ CoffeeWordAssignmentOp "and=" -> "&&=" "or=" -> "||=" +LowBinaryOp + !CoffeeAndOrEnabled "and" NonIdContinue -> "&&" + !CoffeeAndOrEnabled "or" NonIdContinue -> "||" + BinaryOp BinaryOpSymbol -> if (typeof $1 === "string") return { $loc, token: $1 } @@ -2670,10 +2714,10 @@ BinaryOpSymbol "==" -> if(module.config.coffeeEq) return "===" return $1 - "and" NonIdContinue -> "&&" + CoffeeAndOrEnabled "and" NonIdContinue -> "&&" "&&" CoffeeOfEnabled "of" NonIdContinue -> "in" - "or" NonIdContinue -> "||" + CoffeeAndOrEnabled "or" NonIdContinue -> "||" "||" # NOTE: ^^ must be above ^ "^^" / ( "xor" NonIdContinue ) -> @@ -2775,6 +2819,9 @@ Xor Xnor /!\^\^?/ / "xnor" +LowUnaryOp + !CoffeeNotEnabled Not + UnaryOp # Lookahead to prevent unary operators from overriding update operators # ++/-- or block unary operator shorthand @@ -2782,7 +2829,7 @@ UnaryOp return { $loc, token: $0 } AwaitOp ( Delete / Void / Typeof ) !":" _? - Not # only when CoffeeNotEnabled (see definition of `Not`) + CoffeeNotEnabled Not # https://github.com/tc39/proposal-await.ops AwaitOp @@ -4756,8 +4803,7 @@ New return { $loc, token: $1 } Not - # Not keyword only active in compat mode - CoffeeNotEnabled "not" NonIdContinue " "? -> + "not" NonIdContinue " "? -> return { $loc, token: "!" } Of @@ -6135,6 +6181,11 @@ InsertVar "" -> return { $loc, token: "var " } +CoffeeAndOrEnabled + "" -> + if(module.config.coffeeAndOr) return + return $skip + CoffeeBinaryExistentialEnabled "" -> if(module.config.coffeeBinaryExistential) return @@ -6274,6 +6325,7 @@ Reset module.config = parse.config = { autoVar: false, autoLet: false, + coffeeAndOr: false, coffeeBinaryExistential: false, coffeeBooleans: false, coffeeClasses: false, @@ -6417,11 +6469,20 @@ Reset // default to deno compatibility if running in deno module.config.deno = typeof Deno !== "undefined" + // coffeeAndOrNot shorthand for coffeeAndOr and coffeeNot + Object.defineProperty(module.config, "coffeeAndOrNot", { + set(b) { + module.config.coffeeAndOr = b + module.config.coffeeNot = b + } + }) + // Expand setting coffeeCompat to the individual options Object.defineProperty(module.config, "coffeeCompat", { set(b) { for (const option of [ "autoVar", + "coffeeAndOr", "coffeeBinaryExistential", "coffeeBooleans", "coffeeClasses",