diff --git a/source/parser.hera b/source/parser.hera index 5a38c9916..1530470c4 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -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 @@ -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? -> { @@ -3443,6 +3446,10 @@ KeywordStatement children: $0, } +Break + "break" NonIdContinue -> + return { $loc, token: $1 } + DebuggerExpression "debugger" NonIdContinue -> return { @@ -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 ( @@ -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" @@ -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" @@ -7568,6 +7625,7 @@ Init processPipelineExpressions(statements) processAssignments(statements) processFunctions(statements) + processBreak(statements) // after all functions already processed processSwitchExpressions(statements) processTryExpressions(statements) @@ -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 diff --git a/test/break.civet b/test/break.civet new file mode 100644 index 000000000..199f94305 --- /dev/null +++ b/test/break.civet @@ -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 + """ diff --git a/test/compat/coffee-for-loops.civet b/test/compat/coffee-for-loops.civet index ee692d45a..16857195a 100644 --- a/test/compat/coffee-for-loops.civet +++ b/test/compat/coffee-for-loops.civet @@ -179,6 +179,7 @@ describe "coffeeForLoops", -> } """ + // TODO: use i/len instead of i1/len1 in second for loop testCase """ multiple for in --- @@ -195,12 +196,44 @@ describe "coffeeForLoops", -> a.x } - for (let i = 0, len = d.length; i < len; i++) { - c = d[i] + for (let i1 = 0, len1 = d.length; i1 < len1; i1++) { + c = d[i1] c.x } """ + // TODO: use i1/len instead of i2/len1 in second for loop + testCase """ + multiple for in, separate functions + --- + "civet coffee-compat" + function f + for i in x + i + return + function g + for i in x + i + return + --- + function f() { + var i + for (let i1 = 0, len = x.length; i1 < len; i1++) { + i = x[i1] + i + } + return + } + function g() { + var i + for (let i2 = 0, len1 = x.length; i2 < len1; i2++) { + i = x[i2] + i + } + return + } + """ + testCase """ nested for in loop --- @@ -210,10 +243,10 @@ describe "coffeeForLoops", -> a.x --- var a, c - for (let i1 = 0, len1 = b.length; i1 < len1; i1++) { - a = b[i1] - for (let i = 0, len = d.length; i < len; i++) { - c = d[i] + for (let i = 0, len = b.length; i < len; i++) { + a = b[i] + for (let i1 = 0, len1 = d.length; i1 < len1; i1++) { + c = d[i1] a.x } } diff --git a/test/function.civet b/test/function.civet index 98b294fe5..38741bc66 100644 --- a/test/function.civet +++ b/test/function.civet @@ -854,11 +854,11 @@ describe "function", -> v + 1 --- (function(x) { - const results1=[];for (i = 0; i < x.length; i++) { - const results=[];for (const v of x[i]) { - results.push(v + 1) - };results1.push(results); - };return results1; + const results=[];for (i = 0; i < x.length; i++) { + const results1=[];for (const v of x[i]) { + results1.push(v + 1) + };results.push(results1); + };return results; }) """ @@ -870,9 +870,9 @@ describe "function", -> v + 1 for v of x[i] --- (function(x) { - const results1=[];for (i = 0; i < x.length; i++) { - const results=[];for (const v of x[i]) { results.push(v + 1) };results1.push(results); - };return results1; + const results=[];for (i = 0; i < x.length; i++) { + const results1=[];for (const v of x[i]) { results1.push(v + 1) };results.push(results1); + };return results; }) """