diff --git a/README.md b/README.md index c19ce5a..2bb618e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/eval.js b/src/eval.js index 45c0650..ea0f9e8 100644 --- a/src/eval.js +++ b/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} = diff --git a/src/find_definitions.js b/src/find_definitions.js index 89bb2b7..11714bd 100644 --- a/src/find_definitions.js +++ b/src/find_definitions.js @@ -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) diff --git a/src/parse_js.js b/src/parse_js.js index 81cd5c9..fa99f9a 100644 --- a/src/parse_js.js +++ b/src/parse_js.js @@ -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 => { } 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 diff --git a/test/test.js b/test/test.js index 30f3ec4..1fcb700 100644 --- a/test/test.js +++ b/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 = () => { diff --git a/test/utils.js b/test/utils.js index 0ae8dc2..0ce48ec 100644 --- a/test/utils.js +++ b/test/utils.js @@ -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 }