Skip to content

Commit

Permalink
Merge pull request #1642 from DanielXMoore/break-loop
Browse files Browse the repository at this point in the history
`break/continue loop/while/until/for/do` refer to anonymous containing iteration
  • Loading branch information
edemaine authored Dec 12, 2024
2 parents 39911d7 + d79a7aa commit 1b1d825
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 21 deletions.
15 changes: 15 additions & 0 deletions civet.dev/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2107,6 +2107,21 @@ except for Civet reserved words (e.g. `and`) where a colon is required.
JavaScript reserved words are invalid as labels.
:::
Iterations also get implicit labels if you refer to them by type,
via `break/continue for/while/until/loop/do`:
<Playground>
loop
while list = next()
for item of list
if item is 'skip'
continue for
else if item is 'next'
continue while
else if item is 'done'
break loop
</Playground>
### Controlling Loop Value
<Playground>
Expand Down
36 changes: 26 additions & 10 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -4473,12 +4473,27 @@ Label
# Argument to break/continue, which can include colon or not in input,
# but should not have colon in output
# Colon is required if the identifier is a Civet keyword,
# except for 'loop' which we special-case
# except for iteration keywords which refer to containing anonymous loop
LabelIdentifier
Colon IdentifierName:id -> id
# Infinite loop of break/continue isn't helpful, so treat as label
&Loop IdentifierName:id -> id
Identifier
Colon IdentifierName:id ->
return {
type: "Label",
name: id.name,
children: [id],
}
/(?:loop|while|until|for|do)(?!\p{ID_Continue})/ ->
return {
type: "Label",
special: $0,
name: "", // to be filled in
children: []
}
Identifier:id ->
return {
type: "Label",
name: id.name,
children: [id],
}

LabelledItem
Statement
Expand Down Expand Up @@ -4580,7 +4595,7 @@ LoopStatement
LoopClause:clause BlockOrEmptyStatement:block ->
return {
...clause,
type: "IterationStatement",
//type: "IterationStatement", [from LoopClause]
children: [...clause.children, block],
block,
}
Expand All @@ -4599,7 +4614,7 @@ LoopClause
}
return {
type: "IterationStatement",
subtype: kind.token,
subtype: "loop",
children: [kind, condition],
condition,
generator,
Expand All @@ -4611,8 +4626,8 @@ DoWhileStatement
Do:d ( _? Star )?:generator NoPostfixBracedOrEmptyBlock:block __:ws WhileClause:clause ->
return {
...clause,
type: "IterationStatement",
subtype: "do-while",
//type: "IterationStatement", [from WhileClause]
subtype: `do-${clause.subtype}`,
children: [ d, block, ws, clause ],
block,
generator,
Expand Down Expand Up @@ -4654,14 +4669,15 @@ WhileStatement

WhileClause
( While / Until ):kind ( _? Star )?:generator _?:ws Condition:condition ->
const subtype = kind.token
if (kind.negated) {
kind = { ...kind, token: "while" }
condition = negateCondition(condition)
}

return {
type: "IterationStatement",
subtype: kind.token,
subtype,
children: [ kind, ws, condition ],
condition,
generator,
Expand Down
9 changes: 1 addition & 8 deletions source/parser/function.civet
Original file line number Diff line number Diff line change
Expand Up @@ -602,13 +602,6 @@ function processBreakContinueWith(statement: IterationStatement | ForStatement):
for control of gatherRecursiveWithinFunction(statement.block,
.type is "BreakStatement" or .type is "ContinueStatement"
)
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
Expand Down Expand Up @@ -642,7 +635,7 @@ function processBreakContinueWith(statement: IterationStatement | ForStatement):
// 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`
throw new Error `Expected parent of ${control.type.toLowerCase().replace "statement", ""} to be BlockStatement`
braceBlock block
changed

Expand Down
51 changes: 50 additions & 1 deletion source/parser/lib.civet
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import type {
ASTNodeObject
ASTRef
BlockStatement
BreakStatement
Call
CallExpression
CaseBlock
CatchBinding
CatchClause
ComptimeStatement
Condition
ContinueStatement
Declaration
DoStatement
ElseClause
Expand All @@ -28,6 +30,8 @@ import type {
IfStatement
Initializer
IterationStatement
Label
LabelledStatement
MemberExpression
MethodDefinition
NormalCatchParameter
Expand All @@ -41,7 +45,6 @@ import type {
TypeArguments
TypeNode
TypeSuffix
TypeUnary
WSNode
} from ./types.civet

Expand Down Expand Up @@ -1413,6 +1416,51 @@ function processFinallyClauses(statements: StatementTuple[]): void
]
block.expressions[>=index] = [tuple]

// Handle `break/continue loop/while/for/do` to add necessary labels
// See also `processBreakContinueWith` which handles `break/continue with`
function processBreaksContinues(statements: StatementTuple[]): void
for control of gatherRecursive(statements,
($: ASTNodeObject): $ is BreakStatement | ContinueStatement => Boolean
($.type is "BreakStatement" or $.type is "ContinueStatement") and
$.label?.special
)
label := control.label!
special := label.special!

// Find nearest containing loop/while/for/do iteration statement
{ ancestor } := findAncestor control,
($: ASTNodeObject): $ is IterationStatement | ForStatement =>
special is "for" ? $.type is "ForStatement" : (and)
$.type is "IterationStatement"
$.subtype.startsWith special
// in particular, special = "do" matches "do-while" and "do-until"
isFunction // don't go outside the current function
unless ancestor?
control.children.push
type: "Error"
message: `No matching '${special}' iteration found above '${control.type.toLowerCase().replace "statement", ""} ${special}'`
continue
{ parent } .= ancestor

// Wrap loop with label if there isn't already one
unless parent?.type is "LabelledStatement"
ref := makeRef `_${special.replace "-", "_"}`
label: Label := makeNode
type: "Label"
name: ref
children: [ ref, ":" ]
replaceNode ancestor,
makeNode {}
type: "LabelledStatement"
label
statement: ancestor
children: [ label, " ", ancestor ]
parent
parent = ancestor.parent as LabelledStatement

label.children.push label.name = parent.label.name
delete label.special

function processProgram(root: BlockStatement): void
state := getState()
config := getConfig()
Expand Down Expand Up @@ -1450,6 +1498,7 @@ function processProgram(root: BlockStatement): void
processPatternMatching(statements)
processIterationExpressions(statements)
processFinallyClauses(statements)
processBreaksContinues(statements)

// Hoist hoistDec attributes to actual declarations.
// NOTE: This should come after iteration expressions get processed
Expand Down
5 changes: 4 additions & 1 deletion source/parser/types.civet
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ export type IterationFamily = ForStatement | IterationStatement | DoStatement |

export type IterationStatement
type: "IterationStatement"
subtype: "while" | "until" | "do-while" | "do-until" | "loop"
children: Children
parent?: Parent
condition: Condition
Expand Down Expand Up @@ -491,7 +492,9 @@ export type Label
type: "Label"
children: Children
parent?: Parent
name: string
name: string | ASTRef
// implicit reference to containing anonymous loop
special?: "loop" | "while" | "until" | "for" | "do"

export type AccessStart
type: "AccessStart"
Expand Down
138 changes: 137 additions & 1 deletion test/label.civet
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,145 @@ describe "labels", ->
loop label
---
:loop while x
break loop
break :loop
---
loop: while (x) {
break loop
}
"""

describe "implicit labels via loop/for/while/until/do", ->
testCase """
break/continue loop
---
loop
for item of list
if finished()
break loop
else
continue loop
---
_loop: while(true) {
for (const item of list) {
if (finished()) {
break _loop
}
else {
continue _loop
}
}
}
"""

testCase """
nested break/continue loop
---
loop
continue loop if false
loop
loop
break loop
break loop
break loop
---
_loop: while(true) {
if (false) { continue _loop }
_loop1: while(true) {
_loop2: while(true) {
break _loop2
}
break _loop1
}
break _loop
}
"""

testCase """
break for
---
for x of y
loop
break for
---
_for: for (const x of y) {
while(true) {
break _for
}
}
"""

testCase """
break while/until
---
while cond1
until cond2
if cond3
break while
else
break until
---
_while: while (cond1) {
_until: while (!cond2) {
if (cond3) {
break _while
}
else {
break _until
}
}
}
"""

testCase """
break do..while/until
---
do
do
break do
while cond1
break do
while cond2
---
_do: do {
_do1: do {
break _do1
}
while (cond1)
break _do
}
while (cond2)
"""

throws """
don't exit functions
---
loop
function f()
break loop
---
ParseErrors: unknown:3:11 No matching 'loop' iteration found above 'break loop'
"""

throws """
don't exit arrow functions
---
loop
=>
continue loop
---
ParseErrors: unknown:3:14 No matching 'loop' iteration found above 'continue loop'
"""

throws """
match
---
loop
while true
break for
continue until
break do
---
ParseErrors: unknown:3:11 No matching 'for' iteration found above 'break for'
unknown:4:14 No matching 'until' iteration found above 'continue until'
unknown:5:11 No matching 'do' iteration found above 'break do'
"""

0 comments on commit 1b1d825

Please sign in to comment.