Skip to content

Commit

Permalink
Low-precedence and/or/not
Browse files Browse the repository at this point in the history
* New "low-predecence" operators `and`/`or`/`not` via parse tree and
  parenthesization, effectively just below `&&`/`||`/`!`
* Implement `not` in all cases (fixes #287)
* New `coffeeAndOr` restores CoffeeScript's precedence for `and`/or`
  (same as `&&`/`||`)
* Old `coffeeNot` restores CoffeeScript's precedence for `not`
  (same as `!`)
  • Loading branch information
edemaine committed Jun 4, 2023
1 parent 2cc4a0b commit b42abb6
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 9 deletions.
17 changes: 17 additions & 0 deletions civet.dev/cheatsheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,13 +319,25 @@ a + b = c
<Playground>
a is b
a is not b
not a
a and b
a or b
a not in b
a not instanceof b
a?
</Playground>
### 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)).
<Playground>
not a == b
a || b and c || d
</Playground>
### Includes Operator
<Playground>
Expand Down Expand Up @@ -1440,6 +1452,11 @@ x == y != z
x isnt y
</Playground>
<Playground>
"civet coffeeAndOr"
a || b and c || d
</Playground>
<Playground>
"civet coffeeNot"
not (x == y)
Expand Down
3 changes: 2 additions & 1 deletion notes/Comparison-to-CoffeeScript.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,15 @@ 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 |
| coffeeEq | `==` → `===`, `!=` → `!==` |
| 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.
Expand Down
26 changes: 26 additions & 0 deletions source/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -3154,6 +3178,8 @@ module.exports = {
processCoffeeInterpolation,
processConstAssignmentDeclaration,
processLetAssignmentDeclaration,
processLowBinaryOpExpression,
processLowUnaryExpression,
processParams,
processProgram,
processReturnValue,
Expand Down
5 changes: 5 additions & 0 deletions source/main.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ uncacheable = new Set [
"JSXOptionalClosingFragment"
"JSXTag"
"LeftHandSideExpression"
"LowBinaryOpExpression"
"LowBinaryOpRHS"
"LowRHS"
"LowUnaryExpression"
"MemberExpression"
"MemberExpressionRest"
"Nested"
Expand Down Expand Up @@ -126,6 +130,7 @@ uncacheable = new Set [
"SingleLineAssignmentExpression"
"SingleLineBinaryOpRHS"
"SingleLineComment"
"SingleLineLowBinaryOpRHS"
"SingleLineStatements"
"SnugNamedProperty"
"Statement"
Expand Down
77 changes: 69 additions & 8 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const {
processCoffeeInterpolation,
processConstAssignmentDeclaration,
processLetAssignmentDeclaration,
processLowBinaryOpExpression,
processLowUnaryExpression,
processProgram,
processUnaryExpression,
quoteString,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 ->
Expand Down Expand Up @@ -2597,6 +2637,10 @@ CoffeeWordAssignmentOp
"and=" -> "&&="
"or=" -> "||="

LowBinaryOp
!CoffeeAndOrEnabled "and" NonIdContinue -> "&&"
!CoffeeAndOrEnabled "or" NonIdContinue -> "||"

BinaryOp
BinaryOpSymbol ->
if (typeof $1 === "string") return { $loc, token: $1 }
Expand Down Expand Up @@ -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 ) ->
Expand Down Expand Up @@ -2775,14 +2819,17 @@ Xor
Xnor
/!\^\^?/ / "xnor"

LowUnaryOp
!CoffeeNotEnabled Not

UnaryOp
# Lookahead to prevent unary operators from overriding update operators
# ++/-- or block unary operator shorthand
/(?!\+\+|--)[!~+-](?!\s|[!~+-]*&)/ ->
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -6135,6 +6181,11 @@ InsertVar
"" ->
return { $loc, token: "var " }

CoffeeAndOrEnabled
"" ->
if(module.config.coffeeAndOr) return
return $skip

CoffeeBinaryExistentialEnabled
"" ->
if(module.config.coffeeBinaryExistential) return
Expand Down Expand Up @@ -6274,6 +6325,7 @@ Reset
module.config = parse.config = {
autoVar: false,
autoLet: false,
coffeeAndOr: false,
coffeeBinaryExistential: false,
coffeeBooleans: false,
coffeeClasses: false,
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit b42abb6

Please # to comment.