diff --git a/src/eval.js b/src/eval.js index eacd534..45c0650 100644 --- a/src/eval.js +++ b/src/eval.js @@ -94,6 +94,14 @@ const codegen_function_expr = (node, cxt) => { return `__trace(${call}, "${node.name}", ${argscount}, ${location}, ${get_closure})` } +/* + in v8 `foo().bar().baz()` gives error `foo(...).bar(...).baz is not a function` +*/ +const not_a_function_error = node => node.string.replaceAll( + new RegExp('\\(.*\\)', 'g'), + '(...)' +) + // TODO if statically can prove that function is hosted, then do not codegen // __trace const codegen_function_call = (node, cxt) => { @@ -102,6 +110,8 @@ const codegen_function_call = (node, cxt) => { const args = `[${node.args.children.map(do_codegen).join(',')}]` + const errormessage = not_a_function_error(node.fn) + let call if(node.fn.type == 'member_access') { const op = node.fn.is_optional_chaining ? '?.' : '' @@ -113,10 +123,11 @@ const codegen_function_call = (node, cxt) => { return `( __obj = ${do_codegen(node.fn.object)}, __fn = __obj${op}[${do_codegen(node.fn.property)}], - __trace_call(__fn, __obj, ${args}) + __trace_call(__fn, __obj, ${args}, ${JSON.stringify(errormessage)}) )` } else { - return `__trace_call(${do_codegen(node.fn)}, null, ${args})` + return `__trace_call(${do_codegen(node.fn)}, null, ${args}, \ +${JSON.stringify(errormessage)})` } } @@ -227,7 +238,9 @@ const codegen = (node, cxt, parent) => { return '...(' + do_codegen(node.expr) + ')' } else if(node.type == 'new') { const args = `[${node.args.children.map(do_codegen).join(',')}]` - return `__trace_call(${do_codegen(node.constructor)}, null, ${args}, true)` + const errormessage = not_a_function_error(node.constructor) + return `__trace_call(${do_codegen(node.constructor)}, null, ${args}, \ +${JSON.stringify(errormessage)}, true)` } else if(node.type == 'grouping'){ return '(' + do_codegen(node.expr) + ')' } else if(node.type == 'array_destructuring') { @@ -591,7 +604,7 @@ export const eval_modules = ( return result } - const __trace_call = (fn, context, args, is_new = false) => { + const __trace_call = (fn, context, args, errormessage, is_new = false) => { if(fn != null && fn.__location != null && !is_new) { // Call will be traced, because tracing code is already embedded inside // fn @@ -599,12 +612,11 @@ export const eval_modules = ( } if(typeof(fn) != 'function') { - // Raise error - if(is_new) { - return new fn(...args) - } else { - return fn.apply(context, args) - } + throw new TypeError( + errormessage + + ' is not a ' + + (is_new ? 'constructor' : 'function') + ) } const children_copy = children @@ -900,8 +912,8 @@ const get_args_scope = (fn_node, args) => { } } -const eval_binary_expr = (node, scope, callsleft) => { - const {ok, children, calls} = eval_children(node, scope, callsleft) +const eval_binary_expr = (node, scope, callsleft, context) => { + const {ok, children, calls} = eval_children(node, scope, callsleft, context) if(!ok) { return {ok, children, calls} } @@ -914,7 +926,7 @@ const eval_binary_expr = (node, scope, callsleft) => { } -const do_eval_frame_expr = (node, scope, callsleft) => { +const do_eval_frame_expr = (node, scope, callsleft, context) => { if([ 'identifier', 'builtin_identifier', @@ -930,9 +942,9 @@ const do_eval_frame_expr = (node, scope, callsleft) => { 'key_value_pair', 'computed_property' ].includes(node.type)) { - return eval_children(node, scope, callsleft) + return eval_children(node, scope, callsleft, context) } else if(node.type == 'array_literal' || node.type == 'call_args'){ - const {ok, children, calls} = eval_children(node, scope, callsleft) + const {ok, children, calls} = eval_children(node, scope, callsleft, context) if(!ok) { return {ok, children, calls} } @@ -948,7 +960,7 @@ const do_eval_frame_expr = (node, scope, callsleft) => { ) return {ok, children, calls, value} } else if(node.type == 'object_literal'){ - const {ok, children, calls} = eval_children(node, scope, callsleft) + const {ok, children, calls} = eval_children(node, scope, callsleft, context) if(!ok) { return {ok, children, calls} } @@ -979,16 +991,14 @@ const do_eval_frame_expr = (node, scope, callsleft) => { ) return {ok, children, value, calls} } else if(node.type == 'function_call' || node.type == 'new'){ - const {ok, children, calls} = eval_children(node, scope, callsleft) + const {ok, children, calls} = eval_children(node, scope, callsleft, context) if(!ok) { return {ok: false, children, calls} } else { if(typeof(children[0].result.value) != 'function') { return { ok: false, - // TODO pass calltree_node here and extract error - // TODO fix error messages - error: new Error('is not a function'), + error: context.calltree_node.error, children, calls, } @@ -1024,7 +1034,8 @@ const do_eval_frame_expr = (node, scope, callsleft) => { const {node: cond_evaled, calls: calls_after_cond} = eval_frame_expr( node.cond, scope, - callsleft + callsleft, + context ) const {ok, value} = cond_evaled.result const branches = node.branches @@ -1038,7 +1049,8 @@ const do_eval_frame_expr = (node, scope, callsleft) => { const {node: branch_evaled, calls: calls_after_branch} = eval_frame_expr( branches[value ? 0 : 1], scope, - calls_after_cond + calls_after_cond, + context ) const children = value ? [cond_evaled, branch_evaled, branches[1]] @@ -1051,7 +1063,7 @@ const do_eval_frame_expr = (node, scope, callsleft) => { } } } else if(node.type == 'member_access'){ - const {ok, children, calls} = eval_children(node, scope, callsleft) + const {ok, children, calls} = eval_children(node, scope, callsleft, context) if(!ok) { return {ok: false, children, calls} } @@ -1071,7 +1083,7 @@ const do_eval_frame_expr = (node, scope, callsleft) => { } } else if(node.type == 'unary') { - const {ok, children, calls} = eval_children(node, scope, callsleft) + const {ok, children, calls} = eval_children(node, scope, callsleft, context) if(!ok) { return {ok: false, children, calls} } else { @@ -1108,13 +1120,14 @@ const do_eval_frame_expr = (node, scope, callsleft) => { } } else if(node.type == 'binary' && !['&&', '||', '??'].includes(node.operator)){ - return eval_binary_expr(node, scope, callsleft) + return eval_binary_expr(node, scope, callsleft, context) } else if(node.type == 'binary' && ['&&', '||', '??'].includes(node.operator)){ const {node: left_evaled, calls} = eval_frame_expr( node.children[0], scope, - callsleft + callsleft, + context ) const {ok, value} = left_evaled.result @@ -1134,11 +1147,11 @@ const do_eval_frame_expr = (node, scope, callsleft) => { calls, } } else { - return eval_binary_expr(node, scope, callsleft) + return eval_binary_expr(node, scope, callsleft, context) } } else if(node.type == 'grouping'){ - const {ok, children, calls} = eval_children(node, scope, callsleft) + const {ok, children, calls} = eval_children(node, scope, callsleft, context) if(!ok) { return {ok, children, calls} } else { @@ -1150,7 +1163,7 @@ const do_eval_frame_expr = (node, scope, callsleft) => { } } -const eval_children = (node, scope, calls) => { +const eval_children = (node, scope, calls, context) => { return node.children.reduce( ({ok, children, calls}, child) => { let next_child, next_ok, next_calls @@ -1159,7 +1172,7 @@ const eval_children = (node, scope, calls) => { next_ok = false; next_calls = calls; } else { - const result = eval_frame_expr(child, scope, calls); + const result = eval_frame_expr(child, scope, calls, context) next_child = result.node; next_calls = result.calls; next_ok = next_child.result.ok; @@ -1170,8 +1183,9 @@ const eval_children = (node, scope, calls) => { ) } -const eval_frame_expr = (node, scope, callsleft) => { - const {ok, error, value, call, children, calls} = do_eval_frame_expr(node, scope, callsleft) +const eval_frame_expr = (node, scope, callsleft, context) => { + const {ok, error, value, call, children, calls} + = do_eval_frame_expr(node, scope, callsleft, context) if(callsleft != null && calls == null) { // TODO remove it, just for debug console.error('node', node) @@ -1234,7 +1248,7 @@ const apply_assignments = (do_node, assignments) => { } -const eval_statement = (s, scope, calls, modules) => { +const eval_statement = (s, scope, calls, context) => { if(s.type == 'do') { const node = s const {ok, assignments, returned, stmts, calls: nextcalls} = node.stmts.reduce( @@ -1249,7 +1263,7 @@ const eval_statement = (s, scope, calls, modules) => { assignments: next_assignments, scope: nextscope, calls: next_calls, - } = eval_statement(s, scope, calls, modules) + } = eval_statement(s, scope, calls, context) return { ok, returned, @@ -1275,7 +1289,8 @@ const eval_statement = (s, scope, calls, modules) => { } else if(s.type == 'const' || s.type == 'assignment') { // TODO default values for destructuring can be function calls - const {node, calls: next_calls} = eval_frame_expr(s.expr, scope, calls) + const {node, calls: next_calls} + = eval_frame_expr(s.expr, scope, calls, context) const s_expr_evaled = {...s, children: [s.name_node, node]} if(!node.result.ok) { return { @@ -1352,7 +1367,8 @@ const eval_statement = (s, scope, calls, modules) => { } } else if(s.type == 'return') { - const {node, calls: next_calls} = eval_frame_expr(s.expr, scope, calls) + const {node, calls: next_calls} = + eval_frame_expr(s.expr, scope, calls, context) return { ok: node.result.ok, @@ -1363,7 +1379,8 @@ const eval_statement = (s, scope, calls, modules) => { } } else if(s.type == 'export') { - const {ok, scope: nextscope, calls: next_calls, node} = eval_statement(s.binding, scope, calls) + const {ok, scope: nextscope, calls: next_calls, node} + = eval_statement(s.binding, scope, calls, context) return { ok, scope: nextscope, @@ -1373,7 +1390,7 @@ const eval_statement = (s, scope, calls, modules) => { } else if(s.type == 'import') { const children = s.imports.map(i => ( {...i, - result: {ok: true, value: modules[s.full_import_path][i.value]} + result: {ok: true, value: context.modules[s.full_import_path][i.value]} } )) const imported_scope = Object.fromEntries(children.map(i => [i.value, i.result.value])) @@ -1385,7 +1402,7 @@ const eval_statement = (s, scope, calls, modules) => { } } else if(s.type == 'if') { - const {node, calls: next_calls} = eval_frame_expr(s.cond, scope, calls) + const {node, calls: next_calls} = eval_frame_expr(s.cond, scope, calls, context) if(!node.result.ok) { return { @@ -1410,6 +1427,7 @@ const eval_statement = (s, scope, calls, modules) => { s.branches[0], scope, next_calls, + context ) return { ok: evaled_branch.result.ok, @@ -1445,6 +1463,7 @@ const eval_statement = (s, scope, calls, modules) => { active_branch, scope, next_calls, + context, ) const children = node.result.value @@ -1467,7 +1486,7 @@ const eval_statement = (s, scope, calls, modules) => { } else if(s.type == 'throw') { - const {node, calls: next_calls} = eval_frame_expr(s.expr, scope, calls) + const {node, calls: next_calls} = eval_frame_expr(s.expr, scope, calls, context) return { ok: false, @@ -1484,11 +1503,7 @@ const eval_statement = (s, scope, calls, modules) => { } else { // stmt type is expression - const {node, calls: next_calls} = eval_frame_expr( - s, - scope, - calls, - ) + const {node, calls: next_calls} = eval_frame_expr(s, scope, calls, context) return { ok: node.result.ok, node, @@ -1503,12 +1518,13 @@ export const eval_frame = (calltree_node, modules) => { throw new Error('illegal state') } const node = calltree_node.code + const context = {calltree_node, modules} if(node.type == 'do') { return eval_statement( node, {}, calltree_node.children, - modules, + context, ).node } else { // TODO default values for destructuring can be function calls @@ -1558,9 +1574,10 @@ export const eval_frame = (calltree_node, modules) => { body, scope, calltree_node.children, + context, ).node } else { - nextbody = eval_frame_expr(body, scope, calltree_node.children) + nextbody = eval_frame_expr(body, scope, calltree_node.children, context) .node } diff --git a/test/test.js b/test/test.js index 65f2688..30f3ec4 100644 --- a/test/test.js +++ b/test/test.js @@ -462,22 +462,20 @@ export const tests = [ ) }), - test('undefined is not a function', () => { - const code = + test('error is not a function', () => { + assert_code_error( ` - const x = () => null(); - const unreachable = () => 1 - x(); + const x = null + x() + `, + 'TypeError: x is not a function' + ) + assert_code_error( ` - const s1 = test_initial_state(code) - // TODO fix error messages - const message = root_calltree_node(s1).error.message - assert_equal( - message == "Cannot read property 'apply' of null" - || - message == "Cannot read properties of null (reading 'apply')" - , - true + const foo = () => ([{bar: {}}]) + foo()[0].bar.baz() + `, + 'TypeError: foo(...)[0].bar.baz is not a function' ) }),