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

return.value and return = #364

Merged
merged 3 commits into from
Feb 12, 2023
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
7 changes: 6 additions & 1 deletion source/cli.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 3 additions & 0 deletions source/generate.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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) ->
Expand Down
147 changes: 117 additions & 30 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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]
}
Expand All @@ -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 <Foo> before arrow function,
Expand Down Expand Up @@ -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) {
Expand All @@ -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"
Expand Down Expand Up @@ -7586,6 +7687,7 @@ Init
}

module.processProgram = function(statements) {
addParentPointers(statements)
processPipelineExpressions(statements)
processAssignments(statements)
processFunctions(statements)
Expand Down Expand Up @@ -7626,22 +7728,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
45 changes: 39 additions & 6 deletions test/compat/coffee-for-loops.civet
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ describe "coffeeForLoops", ->
}
"""

// TODO: use i/len instead of i1/len1 in second for loop
testCase """
multiple for in
---
Expand All @@ -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
---
Expand All @@ -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
}
}
Expand Down
Loading