Skip to content

Commit

Permalink
Merge pull request #1396 from DanielXMoore/break-continue-with
Browse files Browse the repository at this point in the history
`break with` and `continue with` for modifying results array in a loop
  • Loading branch information
edemaine committed Sep 11, 2024
2 parents cfa6fc9 + 077e9e3 commit 3ea9e9f
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 22 deletions.
18 changes: 18 additions & 0 deletions civet.dev/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,24 @@ Labels have the colon on the left to avoid conflict with implicit object
literals. The colons are optional in `break` and `continue`.
:::
### Controlling Loop Value
<Playground>
function varVector(items, mean)
for item of items
continue with 0 unless item?
item -= mean
item * item
</Playground>
<Playground>
found :=
loop
item := nextItem()
break with item if item.found
process item
</Playground>
## Other Blocks
### Try Blocks
Expand Down
17 changes: 11 additions & 6 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -4226,6 +4226,11 @@ Label
Colon:colon Identifier:id Whitespace:w ->
return [ id, colon, w ]

# Argument to break/continue, which can include colon or not in input,
# but should not have colon in output
LabelIdentifier
Colon? Identifier:id -> id

LabelledItem
Statement
FunctionDeclaration
Expand Down Expand Up @@ -5043,11 +5048,11 @@ ExpressionStatement
KeywordStatement
# https://262.ecma-international.org/#prod-BreakStatement
# NOTE: Also allow `break :label` for symmetry
Break ( _ Colon? Identifier:id )? ->
Break ( _ LabelIdentifier )? ( _ With MaybeNestedExtendedExpression )? ->
return {
type: "BreakStatement",
children: $2 ? [ $1, $2[0], $2[2] ] : [ $1 ],
// omit colon
with: $3?.[2],
children: [ $1, $2 ],
}

# NOTE: `continue switch` for fallthrough
Expand All @@ -5060,11 +5065,11 @@ KeywordStatement

# https://262.ecma-international.org/#prod-ContinueStatement
# NOTE: Also allow `continue :label` for symmetry
Continue ( _ Colon? Identifier:id )? ->
Continue ( _ LabelIdentifier )? ( _ With MaybeNestedExtendedExpression )? ->
return {
type: "ContinueStatement",
children: $2 ? [ $1, $2[0], $2[2] ] : [ $1 ],
// omit colon
with: $3?.[2],
children: [ $1, $2 ],
}

DebuggerStatement
Expand Down
87 changes: 77 additions & 10 deletions source/parser/function.civet
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import type {
ASTNode
ASTNodeBase
ASTNodeObject
BlockStatement
BreakStatement
CallExpression
ContinueStatement
Declaration
ForStatement
FunctionNode
IterationExpression
IterationFamily
IterationStatement
LabeledStatement
Parameter
ParametersNode
Expand All @@ -29,6 +35,7 @@ import {
findAncestor
findChildIndex
gatherNodes
gatherRecursive
gatherRecursiveAll
gatherRecursiveWithinFunction
type Predicate
Expand All @@ -40,15 +47,16 @@ import {
hasAwait
hasYield
inplacePrepend
insertTrimmingSpace
isEmptyBareBlock
isExit
isFunction
isLoopStatement
isStatement
isWhitespaceOrEmpty
makeLeftHandSideExpression
makeNode
startsWithPredicate
trimFirstSpace
updateParentPointers
wrapIIFE
wrapWithReturn
Expand Down Expand Up @@ -264,7 +272,7 @@ function assignResults(node: StatementTuple[] | ASTNode, collect: (node: ASTNode
exp = (exp as LabeledStatement).statement
{type} = exp

switch exp.type
switch type
when "BreakStatement", "ContinueStatement", "DebuggerStatement", "EmptyStatement", "ReturnStatement", "ThrowStatement"
return
when "Declaration"
Expand Down Expand Up @@ -439,6 +447,42 @@ function insertSwitchReturns(exp): void
exp.caseBlock.clauses.forEach (clause) =>
insertReturn clause

// Process `break with` and `continue with` within a loop
// that already has a resultsRef attribute.
// Returns whether the resultsRef might be modified, so should use let.
function processBreakContinueWith(statement: IterationStatement | ForStatement): boolean
changed .= false
for control of gatherRecursive(statement.block,
(s: ASTNodeObject): s is BreakStatement | ContinueStatement =>
s.type is like "BreakStatement", "ContinueStatement"
(s: ASTNodeObject) =>
isFunction(s) or s.type is "IterationStatement"
)
function controlName: string
switch control.type
when "BreakStatement"
"break"
when "ContinueStatement"
"continue"

// break with <expr> overwrites the results of the loop
// continue with <expr> appends to the results of the loop
if control.with
control.children.unshift
if control.type is "BreakStatement"
changed = true
[statement.resultsRef, ' =', control.with, ';']
else // control.type is "ContinueStatement"
[statement.resultsRef, '.push(', trimFirstSpace(control.with), ');']
updateParentPointers control.with, control

// Brace containing block now that it has multiple statements
block := control.parent
unless block?.type is "BlockStatement"
throw new Error `Expected parent of ${controlName()} to be BlockStatement`
braceBlock block
changed

function wrapIterationReturningResults(
statement: IterationFamily,
outer: { children: StatementTuple[] },
Expand All @@ -456,15 +500,38 @@ function wrapIterationReturningResults(

resultsRef := statement.resultsRef = makeRef "results"

declaration :=
decl .= "const"
if statement.type is "IterationStatement" or statement.type is "ForStatement"
if processBreakContinueWith statement
decl = "let"

// Check for infinite loops with only `break with`, no plain `break`
breakWithOnly := (and)
decl is "let"
isLoopStatement statement
gatherRecursive(statement.block,
(s): s is BreakStatement => s.type is "BreakStatement" and not s.with,
(s) => isFunction(s) or s.type is "IterationStatement")# is 0

declaration: Declaration := {
type: "Declaration"
children: ["const ", resultsRef, "=[]"]
children: [decl, " ", resultsRef]
decl
names: []
bindings: []
}
// Assign [] directly only in const case, so TypeScript can better infer
if decl is "const"
declaration.children.push "=[]"
else // decl is "let"
declaration.children.push ";", resultsRef, "=[]" unless breakWithOnly

outer.children.unshift(["", declaration, ";"])

assignResults statement.block, (node) =>
// TODO: real ast node
[ resultsRef, ".push(", node, ")" ]
unless breakWithOnly
assignResults statement.block, (node) =>
// TODO: real ast node
[ resultsRef, ".push(", node, ")" ]

if collect
statement.children.push collect(resultsRef)
Expand Down Expand Up @@ -567,9 +634,9 @@ function expressionizeIteration(exp: IterationExpression): void
{ async, subtype, block, children, statement } := exp
i := children.indexOf statement
if i < 0
throw new Error("Could not find iteration statement in iteration expression")
throw new Error "Could not find iteration statement in iteration expression"

if subtype is "DoStatement" or subtype is "ComptimeStatement"
if subtype is like "DoStatement", "ComptimeStatement"
// Just wrap with IIFE; insertReturn will apply to the resulting function
children.splice(i, 1, wrapIIFE([["", statement, undefined]], async))
updateParentPointers exp
Expand Down Expand Up @@ -618,7 +685,7 @@ function skipImplicitArguments(args: unknown[]): boolean

/** Transform */
function processCoffeeDo(ws: Whitespace, expression: ASTNode): ASTNode
ws = insertTrimmingSpace(ws, "") as Whitespace
ws = trimFirstSpace(ws) as Whitespace
args: ASTNode[] := []
if expression is like {type: "ArrowFunction"}, {type: "FunctionExpression"}
{ parameters } := expression
Expand Down
8 changes: 6 additions & 2 deletions source/parser/types.civet
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,13 @@ export type IterationStatement
condition: Condition
block: BlockStatement
negated: boolean?
resultsRef: ASTRef?

export type BreakStatement
type: "BreakStatement"
children: Children
parent?: Parent
with: ASTNode?

export type ComptimeStatement
type: "ComptimeStatement"
Expand All @@ -304,6 +306,7 @@ export type ContinueStatement
children: Children
parent?: Parent
special?: "switch"
with: ASTNode?

export type DoStatement
type: "DoStatement"
Expand All @@ -318,6 +321,7 @@ export type ForStatement
declaration: DeclarationStatement?
block: BlockStatement
hoistDec: unknown
resultsRef: ASTRef?

export type SwitchStatement
type: "SwitchStatement"
Expand Down Expand Up @@ -424,8 +428,8 @@ export type DeclarationStatement =
bindings: Binding[]
parent?: Parent
decl: "let" | "const" | "var"
splices: unknown
thisAssignments: ThisAssignments
splices?: unknown
thisAssignments?: ThisAssignments

export type Binding =
type: "Binding"
Expand Down
15 changes: 11 additions & 4 deletions source/parser/util.civet
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import type {
ASTNodeBase
ASTNodeObject
ASTNodeParent
Children
FunctionNode
IsParent
IsToken
IterationStatement
Literal
StatementNode
TypeSuffix
Expand Down Expand Up @@ -171,15 +171,21 @@ function isExit(node: ASTNode): boolean
// Infinite loops
when "IterationStatement"
(and)
node.condition?.type is "ParenthesizedExpression"
node.condition.expression?.type is "Literal"
node.condition.expression?.raw is "true"
isLoopStatement node
gatherRecursiveWithinFunction(node.block,
.type is "BreakStatement").length is 0
// TODO: Distinguish between break of this loop vs. break of inner loops
else
false

// Is this a `loop` statement, or equivalent `while(true)`?
function isLoopStatement(node: ASTNodeObject): node is IterationStatement
(and)
node.type is "IterationStatement"
node.condition?.type is "ParenthesizedExpression"
node.condition.expression?.type is "Literal"
node.condition.expression?.raw is "true"

/**
* Detects Comma, CommaDelimiter, and ParameterElementDelimiter
* with an explicit comma, as should be at the top level of
Expand Down Expand Up @@ -622,6 +628,7 @@ export {
isEmptyBareBlock
isExit
isFunction
isLoopStatement
isStatement
isToken
isWhitespaceOrEmpty
Expand Down
30 changes: 30 additions & 0 deletions test/for.civet
Original file line number Diff line number Diff line change
Expand Up @@ -764,3 +764,33 @@ describe "for", ->
results.push(x ** 2)
}return results})())
"""

testCase """
break/continue with
---
values := for x of y
continue with null unless x?
if(isNaN x) break with
. "NaN"
x+1
---
let results;results=[];for (const x of y) {
if (!(x != null)) { results.push(null);continue }
if(isNaN(x)) { results = [
"NaN"];break}
results.push(x+1)
};const values =results
"""

testCase """
break/continue with outside expression context
---
for x of y
continue with null unless x?
break with x
---
for (const x of y) {
if (!(x != null)) { continue }
break
}
"""
36 changes: 36 additions & 0 deletions test/loop.civet
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,39 @@ describe "loop", ->
---
while(true);
"""

testCase """
break with
---
=>
loop
if special()
break with 'done'
break if finished()
---
() => {
let results;results=[];while(true) {
if (special()) {
results = 'done';break
}
if (finished()) { break } else {results.push(void 0)}
};return results;
}
"""

testCase """
break with only
---
=>
loop
if finished()
break with 'done'
---
() => {
let results;while(true) {
if (finished()) {
results = 'done';break
}
};return results;
}
"""

0 comments on commit 3ea9e9f

Please sign in to comment.