Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

break/continue loop/while/until/for/do refer to anonymous containing iteration #1642

Merged
merged 1 commit into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'
"""
Loading