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 used as value for detecting broken loops, simpler global ref resolution #359

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
93 changes: 68 additions & 25 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ PrimaryExpression
Literal
ArrayLiteral
IdentifierReference # NOTE: Must be below ObjectLiteral for inline objects `a: 1, b: 2` to not be shadowed by matching the first identifier
Break -> { type: "BreakValue", children: $0 }
FunctionExpression
ClassExpression
RegularExpressionLiteral
Expand Down Expand Up @@ -3413,23 +3414,25 @@ ExpressionStatement
KeywordStatement
# https://262.ecma-international.org/#prod-BreakStatement
# NOTE: no label
"break" NonIdContinue -> {
Break -> {
type: "BreakStatement",
children: $0,
}

# https://262.ecma-international.org/#prod-ContinueStatement
# NOTE: no label
"continue" NonIdContinue -> {
type: "ContinueStatement",
children: $0,
}
"continue" NonIdContinue ->
return {
type: "ContinueStatement",
children: [{ $loc, token: $1 }],
}

# https://262.ecma-international.org/#sec-debugger-statement
"debugger" NonIdContinue -> {
type: "DebuggerStatement",
children: $0,
}
"debugger" NonIdContinue ->
return {
type: "DebuggerStatement",
children: [{ $loc, token: $1 }],
}

# https://262.ecma-international.org/#prod-ReturnStatement
Return MaybeNestedExpression? -> {
Expand All @@ -3443,6 +3446,10 @@ KeywordStatement
children: $0,
}

Break
"break" NonIdContinue ->
return { $loc, token: $1 }

DebuggerExpression
"debugger" NonIdContinue ->
return {
Expand Down Expand Up @@ -6930,6 +6937,54 @@ Init
})
}

function processBreak(statements) {
let breakRef
gatherRecursive(statements, n => n.type === "BreakValue" && !n.ref)
.forEach((b) => {
// Set b.ref to prevent this BreakValue from being processed again
breakRef = b.ref = {
type: "Ref",
base: "broke",
}
// Replace break token, allowing for whitespace to have been pushed
// into b.children[0]
b.children[b.children.findIndex(c => c.token === "break")] = b.ref
})
if (!breakRef) return
let loops = 0
function breaksInLoops(statements) {
gatherRecursive(statements, n => n.type === "ForStatement" || n.type === "IterationStatement" || n.type === "FunctionExpression" || n.type === "ArrowFunction")
.forEach((loop) => {
// Avoid recursing into other functions
if (loop.type === "FunctionExpression" || loop.type === "ArrowFunction") return
loops++
loop.children = [ "{ ", breakRef, " = false; ", ...loop.children, " }" ]
gatherRecursive(loop, n => n.type === "BreakStatement" || n.type === "SwitchStatement" || n.type === "FunctionExpression" || n.type === "ArrowFunction")
.forEach((b) => {
if (b.type === "BreakStatement") {
b.children = [ "{ ", breakRef, " = true; ", ...b.children, " }" ]
} else if (b.type === "SwitchStatement") {
// Ignore breaks within switch, but look for inner loops
breaksInLoops(b)
}
})
})
}
breaksInLoops(statements)
if (!loops) {
throw new Error("Use of break as value in function with no loops")
}
const declaration = [ "var ", breakRef, {
ts: true,
children: [": boolean"],
}, "; " ]
if (statements.type === "BlockStatement") {
statements.children.splice(1, 0, declaration) // add after {
} else {
statements.unshift(declaration)
}
}

function processFunctions(statements) {
gatherRecursiveAll(statements, n => {
return (
Expand All @@ -6940,6 +6995,7 @@ Init
.forEach((f) => {
processParams(f)
const { block, returnType } = f
processBreak(block)
if (module.config.implicitReturns) {
const isVoid = returnType === "void"
const isBlock = block?.type === "BlockStatement"
Expand All @@ -6953,6 +7009,7 @@ Init
.forEach((f) => {
processParams(f)
const {signature, block} = f
processBreak(block)
if (module.config.implicitReturns) {
const isConstructor = signature.name === "constructor"
const isVoid = signature.returnType === "void"
Expand Down Expand Up @@ -7568,6 +7625,7 @@ Init
processPipelineExpressions(statements)
processAssignments(statements)
processFunctions(statements)
processBreak(statements) // after all functions already processed
processSwitchExpressions(statements)
processTryExpressions(statements)

Expand Down Expand Up @@ -7605,22 +7663,7 @@ Init
}

function populateRefs(statements) {
const refNodes = gatherNodes(statements, ({type}) => type === "Ref")
const blockNodes = new Set(gatherNodes(statements, ({type}) => type === "BlockStatement"))
const forNodes = gatherNodes(statements, ({type}) => type === "ForStatement")

// Populate refs from inside out
forNodes.forEach(({declaration, block}) => {
// Need to include declarations with block because they share scope
if (block.type === "BlockStatement") {
populateRefs([declaration, ...block.children])
} else { // single non-block statement
populateRefs([declaration, ...block])
}
blockNodes.delete(block)
})

blockNodes.forEach(({expressions}) => populateRefs(expressions))
const refNodes = gatherRecursive(statements, ({type}) => type === "Ref")

if (refNodes.length) {
// Find all ids within nested scopes
Expand Down
200 changes: 200 additions & 0 deletions test/break.civet
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
{testCase, throws} from ./helper.civet

describe "break as value", ->
testCase """
for loop
---
for item of list
break if match item
process item
found := break
---
var broke: boolean; { broke = false; for (const item of list) {
if (match(item)) { { broke = true; break } }
process(item)
} }
const found = broke
"""

testCase """
while loop
---
while item = next()
break if match item
found := break
---
var broke: boolean; { broke = false; while (item = next()) {
if (match(item)) { { broke = true; break } }
} }
const found = broke
"""

testCase """
loop
---
loop
break if match()
found := break
---
var broke: boolean; { broke = false; while(true) {
if (match()) { { broke = true; break } }
} }
const found = broke
"""

testCase """
two loops
---
loop break
loop break
found := break
---
var broke: boolean; { broke = false; while(true) { broke = true; break } }
{ broke = false; while(true) { broke = true; break } }
const found = broke
"""

testCase """
broke in scope
---
loop break
broke := break
---
var broke1: boolean; { broke1 = false; while(true) { broke1 = true; break } }
const broke = broke1
"""

testCase """
if break
---
for item of list
break if match item
process item
if break
console.log 'found'
---
var broke: boolean; { broke = false; for (const item of list) {
if (match(item)) { { broke = true; break } }
process(item)
} }
if (broke) {
console.log('found')
}
"""

testCase """
for loop, if break in function
---
function f
for item of list
break if match item
process item
if break
console.log 'found'
---
function f() {var broke: boolean;
{ broke = false; for (const item of list) {
if (match(item)) { { broke = true; break } }
process(item)
} }
if (broke) {
return console.log('found')
}
return
}
"""

testCase """
return break
---
function f
for item of list
break if match item
process item
return break
---
function f() {var broke: boolean;
{ broke = false; for (const item of list) {
if (match(item)) { { broke = true; break } }
process(item)
} }
return broke
}
"""

testCase """
ignore switch
---
loop break
switch foo
case 1
break
when 2
break
found := break
---
var broke: boolean; { broke = false; while(true) { broke = true; break } }
switch(foo) {
case 1:
break
case 2: {
break;break;
}
}
const found = broke
"""

testCase """
expressionized loop
---
items :=
for item of list
break if match item
item
found := break
---
var broke: boolean; const items =
(()=>{const results=[];{ broke = false; for (const item of list) {
if (match(item)) { { broke = true; break } }
results.push(item)
} }; return results})()
const found = broke
"""

testCase """
loops in switch
---
switch foo
case 1
loop break
found := break
---
var broke: boolean; switch(foo) {
case 1:
{ broke = false; while(true) { broke = true; break } }
}
const found = broke
"""

throws """
must be a loop
---
unrelated := 'red herring'
found := break
"""

throws """
must be a loop in scope, outer
---
function foo
loop forever
found := break
"""

throws """
must be a loop in scope, inner
---
loop forever
function foo
found := break
"""
Loading