function declarations

This commit is contained in:
Dmitry Vasilev
2023-05-16 00:04:53 +03:00
parent 096ca5e495
commit 5b5938ee61
6 changed files with 202 additions and 41 deletions

View File

@@ -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.

View File

@@ -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} =

View File

@@ -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)

View File

@@ -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

View File

@@ -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 = () => {

View File

@@ -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
}