mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 13:04:30 -08:00
function declarations
This commit is contained in:
@@ -71,7 +71,7 @@ Any kind of loops are not supported. Use recursion or array functions instead.
|
||||
|
||||
`if` / `else` can only contain blocks, not single statements (TODO).
|
||||
|
||||
Functions can be declared only by arrow function syntax. `function` keyword and method definitions (like `const foo = { bar() { /* body */ } }` may be supported in future. Both concise and block body are supported.
|
||||
Both traditional functions and arrow functions with block bodies and concise bodies are supported. Method definitions are not supported.
|
||||
|
||||
Classes are not supported. Some sort of immutable classes may be supported in future. `this` keyword is not currently supported. `new` operator is supported for instantiating builtin classes.
|
||||
|
||||
|
||||
77
src/eval.js
77
src/eval.js
@@ -70,7 +70,11 @@ const codegen_function_expr = (node, cxt) => {
|
||||
|
||||
const args = node.function_args.children.map(do_codegen).join(',')
|
||||
|
||||
const call = (node.is_async ? 'async ' : '') + `(${args}) => ` + (
|
||||
const decl = node.is_arrow
|
||||
? `(${args}) => `
|
||||
: `function ${node.name}(${args})`
|
||||
|
||||
const call = (node.is_async ? 'async ' : '') + decl + (
|
||||
// TODO gensym __obj, __fn
|
||||
(node.body.type == 'do')
|
||||
? '{ let __obj, __fn; ' + do_codegen(node.body) + '}'
|
||||
@@ -145,10 +149,14 @@ const codegen = (node, cxt, parent) => {
|
||||
].includes(node.type)){
|
||||
return node.value
|
||||
} else if(node.type == 'do'){
|
||||
return node.stmts.reduce(
|
||||
(result, stmt) => result + (do_codegen(stmt)) + ';\n',
|
||||
''
|
||||
)
|
||||
return [
|
||||
// hoist function decls to the top
|
||||
...node.stmts.filter(s => s.type == 'function_decl'),
|
||||
...node.stmts.filter(s => s.type != 'function_decl'),
|
||||
].reduce(
|
||||
(result, stmt) => result + (do_codegen(stmt)) + ';\n',
|
||||
''
|
||||
)
|
||||
} else if(node.type == 'return') {
|
||||
return 'return ' + do_codegen(node.expr) + ';'
|
||||
} else if(node.type == 'throw') {
|
||||
@@ -266,6 +274,9 @@ ${JSON.stringify(errormessage)}, true)`
|
||||
return do_codegen(node.binding)
|
||||
+
|
||||
`Object.assign(__exports, {${identifiers.join(',')}});`
|
||||
} else if(node.type == 'function_decl') {
|
||||
const expr = node.children[0]
|
||||
return `const ${expr.name} = ${codegen_function_expr(expr, cxt)};`
|
||||
} else {
|
||||
console.error(node)
|
||||
throw new Error('unknown node type: ' + node.type)
|
||||
@@ -1030,7 +1041,7 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
calls: callsleft,
|
||||
children: node.children,
|
||||
}
|
||||
} else if(node.type == 'ternary'){
|
||||
} else if(node.type == 'ternary') {
|
||||
const {node: cond_evaled, calls: calls_after_cond} = eval_frame_expr(
|
||||
node.cond,
|
||||
scope,
|
||||
@@ -1168,14 +1179,14 @@ const eval_children = (node, scope, calls, context) => {
|
||||
({ok, children, calls}, child) => {
|
||||
let next_child, next_ok, next_calls
|
||||
if(!ok) {
|
||||
next_child = child;
|
||||
next_ok = false;
|
||||
next_calls = calls;
|
||||
next_child = child
|
||||
next_ok = false
|
||||
next_calls = calls
|
||||
} else {
|
||||
const result = eval_frame_expr(child, scope, calls, context)
|
||||
next_child = result.node;
|
||||
next_calls = result.calls;
|
||||
next_ok = next_child.result.ok;
|
||||
next_child = result.node
|
||||
next_calls = result.calls
|
||||
next_ok = next_child.result.ok
|
||||
}
|
||||
return {ok: next_ok, children: [...children, next_child], calls: next_calls}
|
||||
},
|
||||
@@ -1247,14 +1258,47 @@ const apply_assignments = (do_node, assignments) => {
|
||||
return {node, scope}
|
||||
}
|
||||
|
||||
|
||||
const eval_statement = (s, scope, calls, context) => {
|
||||
if(s.type == 'do') {
|
||||
const node = s
|
||||
const {ok, assignments, returned, stmts, calls: nextcalls} = node.stmts.reduce(
|
||||
// hoist function decls to the top
|
||||
const function_decls = node.stmts
|
||||
.filter(s => s.type == 'function_decl')
|
||||
.map(s => {
|
||||
const {ok, children, calls: next_calls} = eval_children(s, scope, calls, context)
|
||||
if(!ok) {
|
||||
// Function decl can never fail
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
if(next_calls != calls) {
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
return {...s, children, result: {ok: true}}
|
||||
})
|
||||
|
||||
const hoisted_functions_scope = Object.fromEntries(
|
||||
function_decls.map(decl =>
|
||||
[decl.children[0].name, decl.children[0].result.value]
|
||||
)
|
||||
)
|
||||
|
||||
const initial_scope = {...scope, ...hoisted_functions_scope}
|
||||
|
||||
const {ok, assignments, returned, stmts, calls: next_calls} = node.stmts.reduce(
|
||||
({ok, returned, stmts, scope, calls, assignments}, s) => {
|
||||
if(returned || !ok) {
|
||||
return {ok, returned, scope, calls, stmts: [...stmts, s], assignments}
|
||||
} else if(s.type == 'function_decl') {
|
||||
const node = function_decls.find(decl => decl.index == s.index)
|
||||
return {
|
||||
ok: true,
|
||||
returned: false,
|
||||
node,
|
||||
assignments,
|
||||
scope,
|
||||
calls,
|
||||
stmts: [...stmts, node],
|
||||
}
|
||||
} else {
|
||||
const {
|
||||
ok,
|
||||
@@ -1274,7 +1318,7 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
{ok: true, returned: false, stmts: [], scope, calls, assignments: {}}
|
||||
{ok: true, returned: false, stmts: [], scope: initial_scope, calls, assignments: {}}
|
||||
)
|
||||
const {node: next_node, scope: next_scope} =
|
||||
apply_assignments({...node, children: stmts, result: {ok}}, assignments)
|
||||
@@ -1284,7 +1328,7 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
scope: {...scope, ...next_scope},
|
||||
returned,
|
||||
assignments,
|
||||
calls: nextcalls,
|
||||
calls: next_calls,
|
||||
}
|
||||
} else if(s.type == 'const' || s.type == 'assignment') {
|
||||
// TODO default values for destructuring can be function calls
|
||||
@@ -1365,6 +1409,7 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
)
|
||||
: null
|
||||
}
|
||||
|
||||
} else if(s.type == 'return') {
|
||||
|
||||
const {node, calls: next_calls} =
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import {set_push, set_diff, set_union, map_object, map_find, uniq} from './utils.js'
|
||||
import {collect_destructuring_identifiers, collect_imports, ancestry, find_leaf} from './ast_utils.js'
|
||||
|
||||
// TODO get complete list of globals (borrow from eslint?)
|
||||
import {globals} from './globals.js'
|
||||
|
||||
const map_find_definitions = (nodes, mapper) => {
|
||||
@@ -20,7 +19,6 @@ const map_find_definitions = (nodes, mapper) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const scope_from_node = n => {
|
||||
if(n.type == 'import') {
|
||||
return Object.fromEntries(
|
||||
@@ -34,6 +32,10 @@ const scope_from_node = n => {
|
||||
node.value, node
|
||||
])
|
||||
)
|
||||
} else if(n.type == 'function_decl') {
|
||||
// Return null because of hoisting. We take function decls into account
|
||||
// first before processing statements one by one
|
||||
return null
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
@@ -94,14 +96,20 @@ export const find_definitions = (ast, scope = {}, closure_scope = {}, module_nam
|
||||
}
|
||||
}
|
||||
} else if(ast.type == 'do'){
|
||||
const children_with_scope = ast.children.reduce(
|
||||
({scope, children}, node) => ({
|
||||
scope: {...scope, ...scope_from_node(node)},
|
||||
children: children.concat([{node, scope}]),
|
||||
})
|
||||
,
|
||||
{scope: {}, children: []}
|
||||
const hoisted_functions_scope = Object.fromEntries(
|
||||
ast.children
|
||||
.filter(s => s.type == 'function_decl')
|
||||
.map(s => [s.children[0].name, s.children[0]])
|
||||
)
|
||||
const children_with_scope = ast.children
|
||||
.reduce(
|
||||
({scope, children}, node) => ({
|
||||
scope: {...scope, ...scope_from_node(node)},
|
||||
children: children.concat([{node, scope}]),
|
||||
})
|
||||
,
|
||||
{scope: hoisted_functions_scope, children: []}
|
||||
)
|
||||
const local_scope = children_with_scope.scope
|
||||
const {nodes, undeclared, closed} = map_find_definitions(children_with_scope.children, cs =>
|
||||
find_definitions(cs.node, {...scope, ...cs.scope}, local_scope, module_name)
|
||||
|
||||
@@ -905,7 +905,43 @@ const object_literal =
|
||||
)
|
||||
)
|
||||
|
||||
const function_expr =
|
||||
const block_function_body = if_ok(
|
||||
seq_select(1, [
|
||||
literal('{'),
|
||||
cxt => parse_do(cxt),
|
||||
literal('}'),
|
||||
]),
|
||||
|
||||
({value, ...node}) => ({...value, ...node}),
|
||||
)
|
||||
|
||||
const function_expr = must_have_name =>
|
||||
if_ok(
|
||||
seq([
|
||||
optional(literal('async')),
|
||||
literal('function'),
|
||||
must_have_name ? identifier : optional(identifier),
|
||||
list_destructuring(['(', ')'], 'function_args'),
|
||||
block_function_body,
|
||||
]),
|
||||
({value, ...node}) => {
|
||||
const [is_async, _fn, name, args, body] = value
|
||||
const function_args = {...args,
|
||||
not_evaluatable: args.children.length == 0
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
type: 'function_expr',
|
||||
is_async: is_async != null,
|
||||
is_arrow: false,
|
||||
name: name?.value,
|
||||
body,
|
||||
children: [function_args, body]
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const arrow_function_expr =
|
||||
if_ok(
|
||||
seq([
|
||||
optional(literal('async')),
|
||||
@@ -920,16 +956,7 @@ const function_expr =
|
||||
|
||||
either(
|
||||
// With curly braces
|
||||
if_ok(
|
||||
seq_select(1, [
|
||||
literal('{'),
|
||||
cxt => parse_do(cxt),
|
||||
literal('}'),
|
||||
]),
|
||||
|
||||
({value, ...node}) => ({...value, ...node}),
|
||||
),
|
||||
|
||||
block_function_body,
|
||||
// Just expression
|
||||
cxt => expr(cxt),
|
||||
)
|
||||
@@ -953,6 +980,7 @@ const function_expr =
|
||||
...node,
|
||||
type: 'function_expr',
|
||||
is_async: is_async != null,
|
||||
is_arrow: true,
|
||||
body,
|
||||
children: [function_args, body]
|
||||
}
|
||||
@@ -1008,7 +1036,8 @@ const primary = if_fail(
|
||||
new_expr,
|
||||
object_literal,
|
||||
array_literal,
|
||||
function_expr,
|
||||
function_expr(false),
|
||||
arrow_function_expr,
|
||||
|
||||
// not_followed_by for better error messages
|
||||
// y => { <garbage> } must parse as function expr, not as identifier `y`
|
||||
@@ -1046,6 +1075,12 @@ const expr =
|
||||
cxt => expr(cxt)
|
||||
)
|
||||
|
||||
const function_decl = if_ok(
|
||||
function_expr(true),
|
||||
// wrap function_expr with function_decl
|
||||
node => ({...node, type: 'function_decl', children: [node]})
|
||||
)
|
||||
|
||||
// TODO multiple decls, like `const x = 1, y = 2`
|
||||
const const_statement =
|
||||
if_ok(
|
||||
@@ -1246,6 +1281,7 @@ const do_statement = either(
|
||||
if_statement,
|
||||
throw_statement,
|
||||
return_statement,
|
||||
function_decl,
|
||||
)
|
||||
|
||||
const module_statement = either(
|
||||
@@ -1256,6 +1292,7 @@ const module_statement = either(
|
||||
throw_statement,
|
||||
import_statement,
|
||||
export_statement,
|
||||
function_decl,
|
||||
)
|
||||
|
||||
const parse_do_or_module = (is_module) =>
|
||||
@@ -1362,6 +1399,10 @@ const update_children_not_rec = (node, children = node.children) => {
|
||||
stmts: children,
|
||||
is_statement: true,
|
||||
}
|
||||
} else if(node.type == 'function_decl'){
|
||||
return {...node,
|
||||
is_statement: true,
|
||||
}
|
||||
} else if(node.type == 'unary') {
|
||||
return {...node,
|
||||
expr: children[0],
|
||||
@@ -1486,7 +1527,12 @@ const do_deduce_fn_names = (node, parent) => {
|
||||
}
|
||||
}
|
||||
|
||||
if(node_result.type == 'function_expr') {
|
||||
if(
|
||||
node_result.type == 'function_expr'
|
||||
&&
|
||||
// not a named function
|
||||
node_result.name == null
|
||||
) {
|
||||
let name
|
||||
if(parent?.type == 'const') {
|
||||
name = parent.name
|
||||
|
||||
64
test/test.js
64
test/test.js
@@ -110,6 +110,49 @@ export const tests = [
|
||||
return assert_code_evals_to('!false', true)
|
||||
}),
|
||||
|
||||
test('function expr', () => {
|
||||
assert_code_evals_to(
|
||||
`
|
||||
const x = function(){}
|
||||
x.name
|
||||
`,
|
||||
'x'
|
||||
)
|
||||
assert_code_evals_to(
|
||||
`
|
||||
const x = function foo(){}
|
||||
x.name
|
||||
`,
|
||||
'foo'
|
||||
)
|
||||
assert_code_evals_to(
|
||||
`
|
||||
(function foo(x) {
|
||||
return x*2
|
||||
}).name
|
||||
`,
|
||||
'foo'
|
||||
)
|
||||
assert_code_evals_to(
|
||||
`
|
||||
(function foo(x) {
|
||||
return x*2
|
||||
})(1)
|
||||
`,
|
||||
2
|
||||
)
|
||||
}),
|
||||
|
||||
test('function declaration', () => {
|
||||
assert_code_evals_to(
|
||||
`
|
||||
function x() {return 1}
|
||||
x()
|
||||
`,
|
||||
1
|
||||
)
|
||||
}),
|
||||
|
||||
test('More complex expression', () => {
|
||||
assert_code_evals_to(
|
||||
`
|
||||
@@ -1115,6 +1158,26 @@ export const tests = [
|
||||
return assert_equal(parse(code).problems[0].message, 'undeclared identifier: x')
|
||||
}),
|
||||
|
||||
test('function hoisting', () => {
|
||||
assert_code_evals_to(`
|
||||
function x() {
|
||||
return 1
|
||||
}
|
||||
x()
|
||||
`,
|
||||
1
|
||||
)
|
||||
assert_code_evals_to(`
|
||||
const y = x()
|
||||
function x() {
|
||||
return 1
|
||||
}
|
||||
y
|
||||
`,
|
||||
1
|
||||
)
|
||||
}),
|
||||
|
||||
/*
|
||||
TODO use before assignment
|
||||
test('no use before assignment', () => {
|
||||
@@ -2176,7 +2239,6 @@ const y = x()`
|
||||
assert_equal(s2.value_explorer.result.error.message, 'boom')
|
||||
}),
|
||||
|
||||
|
||||
test('frame follows cursor toplevel', () => {
|
||||
const code = `
|
||||
const x = () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ export const assert_code_evals_to = (codestring, expected) => {
|
||||
assert_equal(parse_result.ok, true)
|
||||
const tree = eval_tree(parse_result.node)
|
||||
const frame = eval_frame(tree)
|
||||
const result = frame.children[frame.children.length - 1].result
|
||||
const result = frame.children.at(-1).result
|
||||
assert_equal({ok: result.ok, value: result.value}, {ok: true, value: expected})
|
||||
return frame
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user