From ec292bda73ba34665c89ab9992213a95b03c6083 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Sat, 11 Feb 2023 17:45:15 -0500 Subject: [PATCH 1/3] Simpler global ref resolution Fixes `var broke` colliding with otherwise declared `broke` variable --- source/parser.hera | 17 +---------- test/compat/coffee-for-loops.civet | 45 ++++++++++++++++++++++++++---- test/function.civet | 16 +++++------ 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index 16c3fd98..a8098e48 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -7626,22 +7626,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/compat/coffee-for-loops.civet b/test/compat/coffee-for-loops.civet index ee692d45..16857195 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 98b294fe..38741bc6 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; }) """ From 6222e693c338a131ab693d0ba9fccbda30dd50a1 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Sun, 12 Feb 2023 14:53:20 -0500 Subject: [PATCH 2/3] return.value and return = Fixes #361 --- source/cli.coffee | 7 ++- source/generate.coffee | 3 + source/parser.hera | 130 ++++++++++++++++++++++++++++++++++++----- test/function.civet | 102 ++++++++++++++++++++++++++++++++ 4 files changed, 227 insertions(+), 15 deletions(-) diff --git a/source/cli.coffee b/source/cli.coffee index 413377ca..4f17d895 100644 --- a/source/cli.coffee +++ b/source/cli.coffee @@ -147,7 +147,12 @@ repl = (options) -> else '🐱> ' writer: if options.ast - (obj) -> JSON.stringify obj, null, 2 + (obj) -> + try + JSON.stringify obj, null, 2 + catch e + console.log "Failed to stringify: #{e}" + obj else if options.compile (obj) -> if typeof obj == 'string' diff --git a/source/generate.coffee b/source/generate.coffee index b2d3ad96..82046adf 100644 --- a/source/generate.coffee +++ b/source/generate.coffee @@ -51,12 +51,15 @@ export default gen = (node, options) -> # Remove empty arrays, empty string, null, undefined from node tree # Useful for debugging so I don't need to step though tons of empty nodes +# Also remove parent pointers so we can JSON.stringify the tree export prune = (node) -> if node is null or node is undefined return if node.length is 0 return + if node.parent? + delete node.parent if Array.isArray(node) a = node.map (n) -> diff --git a/source/parser.hera b/source/parser.hera index a8098e48..e90ed08b 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -592,6 +592,10 @@ CallExpression type: "CallExpression", children: [$1, ...$2, ...rest.flat()], }) + # NOTE: Special `return.value` (and `return =` shorthand) + # for changing the automatic return value of function + ( "return.value" NonIdContinue ) / ( "return" &WAssignmentOp ) -> + return { type: "ReturnValue", children: $0 } MemberExpression:member AllowedTrailingMemberExpressions:trailing CallExpressionRest*:rest -> if (rest.length || trailing.length) { @@ -3436,8 +3440,10 @@ KeywordStatement } # https://262.ecma-international.org/#prod-ReturnStatement - Return MaybeNestedExpression? -> { + # NOTE: Modified to leave room for `return.value` and `return =` + Return !("." / WAssignmentOp) MaybeNestedExpression?:expression -> { type: "ReturnStatement", + expression, children: $0, } @@ -6298,6 +6304,13 @@ Init } } + function getIndent(statement) { + let indent = statement?.[0] + // Hacky way to get the indent of the last expression + if (Array.isArray(indent)) indent = indent[indent.length - 1] + return indent + } + // [indent, statement, semicolon] function insertReturn(node) { if (!node) return @@ -6324,9 +6337,7 @@ Init const [, exp, semi] = node if (semi?.type === "SemicolonDelimiter") return - let indent = node[0] - // Hacky way to get the indent of the last expression - if (Array.isArray(indent)) indent = indent[indent.length - 1] + let indent = getIndent(node) if (!exp) return switch (exp.type) { @@ -6859,13 +6870,15 @@ Init // Gather nodes that match a predicate recursing into all unmatched children // i.e. if the predicate matches a node it is not recursed into further - function gatherRecursive(node, predicate) { + function gatherRecursive(node, predicate, skipPredicate) { if (node == null) return [] if (Array.isArray(node)) { return node.flatMap((n) => gatherRecursive(n, predicate)) } + if (skipPredicate?.(node)) return + if (predicate(node)) { return [node] } @@ -6888,6 +6901,39 @@ Init return nodes } + function isFunction({type}) { + return type === "FunctionExpression" || type === "ArrowFunction" || + type === "MethodDefinition" + } + function gatherRecursiveWithinFunction(node, predicate) { + return gatherRecursive(node, predicate, isFunction) + } + + function addParentPointers(node, parent) { + if (node == null) return + if (typeof node !== "object") return + node.parent = parent + if (Array.isArray(node)) { + for (const child of node) { + addParentPointers(child, node) + } + } else if (node.children) { + for (const child of node.children) { + addParentPointers(child, node) + } + } + } + + // Find nearest strict ancestor that satisfies predicate, + // aborting (and returning undefined) if stopPredicate returns true + function findAncestor(node, predicate, stopPredicate) { + node = node.parent + while (node && !stopPredicate?.(node)) { + if (predicate(node)) return node + node = node.parent + } + } + function processParams(f) { const { type, parameters, block } = f // Check for singleton TypeParameters before arrow function, @@ -6951,17 +6997,72 @@ Init }) } - function processFunctions(statements) { - gatherRecursiveAll(statements, n => { - return ( - n.type === "FunctionExpression" || - n.type === "ArrowFunction" - ) + // Support for `return.value` and `return =` + // for changing automatic return value of function. + // Returns whether any present (so shouldn't do implicit return). + function processReturnValue(block) { + const values = gatherRecursiveWithinFunction(block, + ({type}) => type === "ReturnValue") + if (!values.length) return false + + const ref = { + type: "Ref", + base: "ret", + id: "ret", + } + + let declared + values.forEach(value => { + value.children = [ref] + + // Check whether return.value already declared within this function + const ancestor = findAncestor(value, + ({type}) => type === "Declaration", + isFunction) + if (ancestor) declared = true }) + + // Add declaration of return.value after { + if (!declared) { + block.expressions.unshift([ + getIndent(block.expressions[0]), + { + type: "Declaration", + children: ["let ", ref, ";\n"], + names: [], + } + ]) + } + + // Transform existing `return` -> `return ret` + gatherRecursiveWithinFunction(block, + r => r.type === "ReturnStatement" && !r.expression) + .forEach((r) => { + r.expression = ref + r.children.splice(-1, 1, " ", ref) + }) + + // Implicit return before } + if (block.children.at(-2)?.type !== "ReturnStatement") { + block.expressions.push([ + [ "\n", getIndent(block.expressions.at(-1)) ], + { + type: "ReturnStatement", + expression: ref, + children: ["return ", ref] + } + ]) + } + + return true + } + + function processFunctions(statements) { + gatherRecursiveAll(statements, ({type}) => type === "FunctionExpression" || type === "ArrowFunction") .forEach((f) => { processParams(f) const { block, returnType } = f - if (module.config.implicitReturns) { + if (!processReturnValue(block) && module.config.implicitReturns) { const isVoid = returnType === "void" const isBlock = block?.type === "BlockStatement" if (!isVoid && isBlock) { @@ -6970,11 +7071,11 @@ Init } }) - gatherRecursiveAll(statements, n => n.type === "MethodDefinition") + gatherRecursiveAll(statements, ({type}) => type === "MethodDefinition") .forEach((f) => { processParams(f) const {signature, block} = f - if (module.config.implicitReturns) { + if (!processReturnValue(block) && module.config.implicitReturns) { const isConstructor = signature.name === "constructor" const isVoid = signature.returnType === "void" const isSet = signature.modifier === "set" @@ -7586,6 +7687,7 @@ Init } module.processProgram = function(statements) { + addParentPointers(statements) processPipelineExpressions(statements) processAssignments(statements) processFunctions(statements) diff --git a/test/function.civet b/test/function.civet index 38741bc6..6ac6d147 100644 --- a/test/function.civet +++ b/test/function.civet @@ -1313,3 +1313,105 @@ describe "function", -> } }) """ + + describe "return.value", -> + testCase """ + return = + --- + function f + return = 5 + --- + function f() { + let ret; + ret = 5 + return ret + } + """ + + testCase """ + return += + --- + function f + return = 5 + return += 5 + --- + function f() { + let ret; + ret = 5 + ret += 5 + return ret + } + """ + + testCase """ + return.value = + --- + function f + return.value = 5 + --- + function f() { + let ret; + ret = 5 + return ret + } + """ + + testCase """ + bare return + --- + function f(arg) + return = 'default' + return unless arg + return = arg + --- + function f(arg) { + let ret; + ret = 'default' + if(!(arg)) { return ret } + ret = arg + return ret + } + """ + + testCase """ + existing ret + --- + function f(arg) + ret := arg + return = ret + --- + function f(arg) { + let ret1; + const ret = arg + ret1 = ret + return ret1 + } + """ + + testCase """ + big example + --- + function f(list) + for item of list + if match item + return = item + break + if return.value + return += 1 + list.destroy() + --- + function f(list) { + let ret; + for (const item of list) { + if (match(item)) { + ret = item + break + } + } + if (ret) { + ret += 1 + } + list.destroy() + return ret + } + """ From c9d92d5e4bdb10357fe6e571d303363749705d51 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Sun, 12 Feb 2023 15:28:04 -0500 Subject: [PATCH 3/3] Bug fix from review --- source/parser.hera | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/parser.hera b/source/parser.hera index e90ed08b..562e59c3 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -6877,7 +6877,7 @@ Init return node.flatMap((n) => gatherRecursive(n, predicate)) } - if (skipPredicate?.(node)) return + if (skipPredicate?.(node)) return [] if (predicate(node)) { return [node]