diff --git a/civet.dev/cheatsheet.md b/civet.dev/cheatsheet.md
index 483e4394c..bc2e7d4c9 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 7df0c8e82..ba94977d6 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 69f596001..0cfc276b2 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 e2c419ab2..c1a33887e 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 df81a99af..81437775c 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",