diff --git a/src/ast_utils.js b/src/ast_utils.js index 953da50..536a9a9 100644 --- a/src/ast_utils.js +++ b/src/ast_utils.js @@ -13,9 +13,11 @@ export const collect_destructuring_identifiers = node => { ['array_destructuring', 'object_destructuring', 'function_args'] .includes(node.type) ) { - return node.elements + return node.children .map(collect_destructuring_identifiers) .flat() + } else if (node.type == 'decl_pair') { + return collect_destructuring_identifiers(node.children[0]) } else { console.error(node) throw new Error('not implemented') diff --git a/src/cmd.js b/src/cmd.js index 28ccde7..3265e75 100644 --- a/src/cmd.js +++ b/src/cmd.js @@ -539,7 +539,9 @@ const goto_definition = (state, index) => { if(n.is_default && d.is_default) { return n.children[0] } else if(!n.is_default && !d.is_default) { - const ids = collect_destructuring_identifiers(n.binding.name_node) + const ids = n.binding.children.flatMap(c => + collect_destructuring_identifiers(c.name_node) + ) return ids.find(i => i.value == node.value) } }) @@ -592,6 +594,71 @@ const filter_calltree = (calltree, pred) => { } */ +const get_stmt_value_explorer = (state, stmt) => { + if(stmt.result == null) { + // statement was not evaluated + return null + } + + let result + + if(stmt.result.ok) { + if(stmt.type == 'return') { + result = stmt.children[0].result + } else if(['let', 'const', 'assignment'].includes(stmt.type)) { + const identifiers = stmt + .children + .flatMap( + collect_destructuring_identifiers + ) + .filter(id => id.result != null) + .map(id => [id.value, id.result.value]) + let value + if( + stmt.children.length == 1 + && + ( + stmt.children[0].type == 'identifier' + || + stmt.children[0].type == 'decl_pair' + && + stmt.children[0].name_node.type == 'identifier' + ) + ) { + // Just a single declaration + if(identifiers.length != 1) { + throw new Error('illegal state') + } + value = identifiers[0][1] + } else { + value = Object.fromEntries(identifiers) + } + + return { + index: stmt.index, + length: stmt.length, + result: {ok: true, value}, + } + } else if(stmt.type == 'if'){ + return null + } else if(stmt.type == 'import'){ + result = { + ok: true, + value: state.modules[stmt.full_import_path], + } + } else if (stmt.type == 'export') { + return get_stmt_value_explorer(state, stmt.children[0]) + } else { + result = stmt.result + } + } else { + result = find_error_origin_node(stmt).result + } + + return {index: stmt.index, length: stmt.length, result} +} + + const get_value_explorer = (state, index) => { if( state.active_calltree_node == null @@ -677,49 +744,8 @@ const get_value_explorer = (state, index) => { const do_node = anc[do_index] const stmt = anc[do_index - 1] - if(stmt.result == null) { - // statement was not evaluated - return null - } + return get_stmt_value_explorer(state, stmt) - let result - - if(stmt.result.ok) { - if(['const', 'assignment'].includes(stmt.type)) { - result = stmt.children[1].result - } else if(stmt.type == 'return') { - result = stmt.children[0].result - } else if(stmt.type == 'let') { - return { - index: stmt.index, - length: stmt.length, - result: - { - ok: true, - value: Object.fromEntries( - stmt.children.map(c => - [c.value, c.result.value] - ) - ) - } - } - } else if(stmt.type == 'if'){ - return null - } else if(stmt.type == 'import'){ - result = { - ok: true, - value: state.modules[stmt.full_import_path], - } - } else if (stmt.type == 'export') { - result = stmt.children[0].children[1].result - } else { - result = stmt.result - } - } else { - result = find_error_origin_node(stmt).result - } - - return {index: stmt.index, length: stmt.length, result} } const do_move_cursor = (state, index) => { diff --git a/src/eval.js b/src/eval.js index 3c9c363..27e7246 100644 --- a/src/eval.js +++ b/src/eval.js @@ -217,32 +217,49 @@ const codegen = (node, node_cxt, parent) => { + branches[0] +'\n: ' + branches[1] - } else if(node.type == 'const'){ - const res = 'const ' + do_codegen(node.name_node) + ' = ' + do_codegen(node.expr, node) + ';' - if(node.name_node.type == 'identifier' && node.expr.type == 'function_call') { - // deduce function name from variable it was assigned to if anonymous - // works for point-free programming, like - // const parse_statement = either(parse_import, parse_assignment, ...) - return res + ` + } else if(['const', 'let', 'assignment'].includes(node.type)) { + const prefix = node.type == 'assignment' + ? '' + : node.type + ' ' + return prefix + node + .children + .map(c => c.type == 'identifier' + ? c.value + : do_codegen(c.children[0]) + ' = ' + do_codegen(c.children[1]) + ) + .join(',') + + ';' + + node.children.map(decl => { if( - typeof(${node.name_node.value}) == 'function' + node.type != 'assignment' + && + decl.type != 'identifier' && - ${node.name_node.value}.name == 'anonymous' + decl.name_node.type == 'identifier' + && + decl.expr.type == 'function_call' ) { - Object.defineProperty( - ${node.name_node.value}, - "name", - {value: "${node.name_node.value}"} - ); + // deduce function name from variable it was assigned to if anonymous + // works for point-free programming, like + // const parse_statement = either(parse_import, parse_assignment, ...) + return ` + if( + typeof(${decl.name_node.value}) == 'function' + && + ${decl.name_node.value}.name == 'anonymous' + ) { + Object.defineProperty( + ${decl.name_node.value}, + "name", + {value: "${decl.name_node.value}"} + ); + } + ` + } else { + return '' } - ` - } else { - return res - } - } else if(node.type == 'let') { - return 'let ' + node.names.join(',') + ';'; - } else if(node.type == 'assignment') { - return node.name + ' = ' + do_codegen(node.expr, node) + ';'; + }) + .join('') } else if(node.type == 'member_access'){ return '(' + do_codegen(node.object) @@ -295,7 +312,10 @@ ${JSON.stringify(errormessage)}, true)` if(node.is_default) { return `__cxt.modules['${node_cxt.module}'].default = ${do_codegen(node.children[0])};` } else { - const identifiers = collect_destructuring_identifiers(node.binding.name_node) + const identifiers = node + .binding + .children + .flatMap(n => collect_destructuring_identifiers(n.name_node)) .map(i => i.value) return do_codegen(node.binding) + @@ -805,8 +825,8 @@ const eval_frame_expr = (node, scope, callsleft, context) => { } } -const apply_assignments = (do_node, assignments) => { - const let_ids = do_node +const apply_assignments = (node, assignments) => { + const let_ids = node .children .filter(c => c.type == 'let') .map(l => l.children) @@ -823,34 +843,99 @@ const apply_assignments = (do_node, assignments) => { .map(([k, {name, value}]) => [name, value]) ) - const node = {...do_node, - children: do_node.children.map(_let => { - if(_let.type != 'let') { - return _let - } - const children = _let.children.map(id => { - const a = assignments[id.index] - if(a == null) { - return id - } else { - return {...id, result: {ok: true, value: a.value}} - } - }) - return {..._let, - result: children.every(c => c.result != null) ? {ok: true} : null, - children - } - }) - } - return {node, scope} } +const eval_decl_pair = (s, scope, calls, context, is_assignment) => { + if(s.type != 'decl_pair') { + throw new Error('illegal state') + } + // TODO default values for destructuring can be function 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 { + ok: false, + node: {...s_expr_evaled, result: {ok: false}}, + scope, + calls: next_calls, + } + } + + const name_nodes = collect_destructuring_identifiers(s.name_node) + const names = name_nodes.map(n => n.value) + const destructuring = codegen(s.name_node) + + // TODO unique name for __value (gensym) + const codestring = ` + const ${destructuring} = __value; + ({${names.join(',')}}); + ` + const {ok, value: next_scope, error} = eval_codestring( + codestring, + {...scope, __value: node.result.value} + ) + + // TODO fine-grained destructuring error, only for identifiers that failed + // destructuring + const name_node_with_result = map_tree( + map_destructuring_identifiers( + s.name_node, + node => ({...node, + result: { + ok, + error: ok ? null : error, + value: !ok ? null : next_scope[node.value], + } + }) + ), + n => n.result == null + ? {...n, result: {ok}} + : n + ) + + const s_evaled = {...s_expr_evaled, children: [ + name_node_with_result, + s_expr_evaled.children[1], + ]} + + if(!ok) { + return { + ok: false, + // TODO assign error to node where destructuring failed, not to every node + node: {...s_evaled, result: {ok, error, is_error_origin: true}}, + scope, + calls, + } + } + + return { + ok: true, + node: {...s_evaled, result: {ok: true}}, + scope: {...scope, ...next_scope}, + calls: next_calls, + assignments: is_assignment + ? Object.fromEntries( + name_nodes.map(n => [ + n.definition.index, + { + value: next_scope[n.value], + name: n.value, + } + ]) + ) + : null + } +} + + const eval_statement = (s, scope, calls, context) => { if(s.type == 'do') { - const node = s + const stmt = s // hoist function decls to the top - const function_decls = node.children + const function_decls = s.children .filter(s => s.type == 'function_decl') .map(s => { const {ok, children, calls: next_calls} = eval_children(s, scope, calls, context) @@ -872,7 +957,7 @@ const eval_statement = (s, scope, calls, context) => { const initial_scope = {...scope, ...hoisted_functions_scope} - const {ok, assignments, returned, children, calls: next_calls} = node.children.reduce( + const {ok, assignments, returned, children, calls: next_calls} = s.children.reduce( ({ok, returned, children, scope, calls, assignments}, s) => { if(returned || !ok) { return {ok, returned, scope, calls, children: [...children, s], assignments} @@ -909,7 +994,7 @@ const eval_statement = (s, scope, calls, context) => { {ok: true, returned: false, children: [], scope: initial_scope, calls, assignments: {}} ) const {node: next_node, scope: next_scope} = - apply_assignments({...node, children: children, result: {ok}}, assignments) + apply_assignments({...s, children: children, result: {ok}}, assignments) return { ok, node: next_node, @@ -918,86 +1003,46 @@ const eval_statement = (s, scope, calls, context) => { assignments, calls: next_calls, } - } else if(s.type == 'const' || s.type == 'assignment') { - // TODO default values for destructuring can be function calls + } else if(['let', 'const', 'assignment'].includes(s.type)) { + const stmt = s - 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 { - ok: false, - node: {...s_expr_evaled, result: {ok: false}}, - scope, - calls: next_calls, - } - } + const initial = {ok: true, children: [], scope, calls, assignments: null} - const name_nodes = collect_destructuring_identifiers(s.name_node) - const names = name_nodes.map(n => n.value) - const destructuring = codegen(s.name_node) - - // TODO unique name for __value (gensym) - const codestring = ` - const ${destructuring} = __value; - ({${names.join(',')}}); - ` - const {ok, value: next_scope, error} = eval_codestring( - codestring, - {...scope, __value: node.result.value} + const {ok, children, calls: next_calls, scope: next_scope, assignments} = s.children.reduce( + ({ok, children, scope, calls, assignments}, s) => { + if(!ok) { + return {ok, scope, calls, children: [...children, s]} + } + if(stmt.type == 'let' && s.type == 'identifier') { + const node = {...s, result: {ok: true}} + return {ok, children: [...children, node], scope, calls} + } + const { + ok: next_ok, + node, + scope: nextscope, + calls: next_calls, + assignments: next_assignments, + } = eval_decl_pair(s, scope, calls, context, stmt.type == 'assignment') + return { + ok: next_ok, + scope: nextscope, + calls: next_calls, + children: [...children, node], + assignments: stmt.type == 'assignment' + ? {...assignments, ...next_assignments} + : null + } + }, + initial ) - - // TODO fine-grained destructuring error, only for identifiers that failed - // destructuring - const name_node_with_result = map_tree( - map_destructuring_identifiers( - s.name_node, - node => ({...node, - result: { - ok, - error: ok ? null : error, - value: !ok ? null : next_scope[node.value], - } - }) - ), - n => n.result == null - ? {...n, result: {ok}} - : n - ) - - const s_evaled = {...s_expr_evaled, children: [ - name_node_with_result, - s_expr_evaled.children[1], - ]} - - if(!ok) { - return { - ok: false, - // TODO assign error to node where destructuring failed, not to every node - node: {...s_evaled, result: {ok, error, is_error_origin: true}}, - scope, - calls, - } - } - return { - ok: true, - node: {...s_evaled, result: {ok: true}}, + ok, + node: {...s, children, result: {ok}}, scope: {...scope, ...next_scope}, calls: next_calls, - assignments: s.type == 'assignment' - ? Object.fromEntries( - name_nodes.map(n => [ - n.definition.index, - { - value: next_scope[n.value], - name: n.value, - } - ]) - ) - : null + assignments, } - } else if(s.type == 'return') { const {node, calls: next_calls} = @@ -1121,10 +1166,6 @@ const eval_statement = (s, scope, calls, context) => { } } - } else if(s.type == 'let') { - - return { ok: true, node: s, scope, calls } - } else if(s.type == 'throw') { const {node, calls: next_calls} = eval_frame_expr(s.expr, scope, calls, context) diff --git a/src/find_definitions.js b/src/find_definitions.js index f6989ae..b6e66cb 100644 --- a/src/find_definitions.js +++ b/src/find_definitions.js @@ -26,11 +26,13 @@ const scope_from_node = n => { ) } else if(n.type == 'export'){ return scope_from_node(n.binding) - } else if(n.type == 'const' || n.type == 'let'){ + } else if(n.type == 'let' || n.type == 'const') { return Object.fromEntries( - collect_destructuring_identifiers(n.name_node).map(node => [ - node.value, node - ]) + n.children + .flatMap(collect_destructuring_identifiers) + .map(node => [ + node.value, node + ]) ) } else if(n.type == 'function_decl') { // Return null because of hoisting. We take function decls into account @@ -55,6 +57,11 @@ const add_trivial_definition = node => { ]} } else if(['array_destructuring', 'object_destructuring'].includes(node.type)) { return {...node, children: node.children.map(add_trivial_definition)} + } else if (node.type == 'decl_pair') { + return { + ...node, + children: node.children.with(0, add_trivial_definition(node.children[0])) + } } else { console.error(node) throw new Error('not implemented') @@ -150,10 +157,8 @@ export const find_definitions = (ast, globals, scope = {}, closure_scope = {}, m is_default: i == 0 && ast.default_import != null, } })) - } else if(ast.type == 'const') { - children = [add_trivial_definition(ast.name_node), ...ast.children.slice(1)] - } else if(ast.type == 'let') { - children = ast.name_node.map(add_trivial_definition) + } else if(ast.type == 'const' || ast.type == 'let') { + children = ast.children.map(add_trivial_definition) } else { children = ast.children } diff --git a/src/parse_js.js b/src/parse_js.js index 2ba5c22..aa807a5 100644 --- a/src/parse_js.js +++ b/src/parse_js.js @@ -1091,55 +1091,47 @@ const function_decl = if_ok( node => ({...node, type: 'function_decl', children: [node]}) ) -// TODO multiple decls, like `const x = 1, y = 2` -const const_statement = - if_ok( - seq([ - literal('const'), - destructuring, - literal('='), - expr, - ]), - ({value, ...node}) => { - const [_const, identifier, _eq, expr] = value - return { - ...node, - type: 'const', - name: identifier.value, - children: [identifier, expr], - } +const decl_pair = if_ok( + seq([destructuring, literal('='), expr]), + ({value, ...node}) => { + const [lefthand, _eq, expr] = value + return { + ...node, + type: 'decl_pair', + not_evaluatable: true, + children: [lefthand, expr], } - ) + } +) -const let_declaration = if_ok( +const const_or_let = is_const => if_ok( seq_select(1, [ - literal('let'), - comma_separated_1(identifier), + literal(is_const ? 'const' : 'let'), + comma_separated_1( + is_const + ? decl_pair + : either(decl_pair, identifier) + ) ]), ({value, ...node}) => ({ ...node, - type: 'let', + type: is_const ? 'const' : 'let', children: value.value, - names: value.value.map(n => n.value), }) ) +const const_statement = const_or_let(true) + +const let_declaration = const_or_let(false) + +// TODO object assignment required braces, like ({foo} = {foo: 1}) const assignment = if_ok( - seq([ - // TODO object assignment required braces, like ({foo} = {foo: 1}) - destructuring, - literal('='), - expr, - ]), - ({value, ...node}) => { - const [identifier, _eq, expr] = value - return { - ...node, - type: 'assignment', - name: identifier.value, - children: [identifier, expr], - } - }, + comma_separated_1(decl_pair), + ({value, ...node}) => ({ + ...node, + type: 'assignment', + children: value, + }) ) const return_statement = @@ -1422,21 +1414,14 @@ const update_children_not_rec = (node, children = node.children) => { } } else if(node.type == 'const'){ return {...node, - name_node: children[0], - expr: children[1], is_statement: true, } } else if(node.type == 'let'){ - return {...node, - name_node: children, - is_statement: true, - } + return {...node, is_statement: true } + } else if(node.type == 'decl_pair') { + return {...node, expr: node.children[1], name_node: node.children[0]} } else if(node.type == 'assignment'){ - return {...node, - name_node: children[0], - expr: children[1], - is_statement: true, - } + return {...node, is_statement: true} } else if(node.type == 'do'){ return {...node, is_statement: true} } else if(node.type == 'function_decl'){ @@ -1568,8 +1553,10 @@ const do_deduce_fn_names = (node, parent) => { node_result.name == null ) { let name - if(parent?.type == 'const') { - name = parent.name + if(parent?.type == 'decl_pair') { + if(parent.name_node.type == 'identifier') { + name = parent.name_node.value + } } else if(parent?.type == 'key_value_pair') { // unwrap quotes with JSON.parse name = JSON.parse(parent.key.value) diff --git a/test/test.js b/test/test.js index 2fa3a86..aab7308 100644 --- a/test/test.js +++ b/test/test.js @@ -320,6 +320,15 @@ export const tests = [ ) }), + test('let variable', () => { + const code = ` + let x, y = 2, unused, [z,q] = [3,4] + x = 1 + ` + const i = test_initial_state(code, code.indexOf('x')) + assert_equal(i.value_explorer.result.value, {y: 2, z: 3, q: 4}) + }), + test('else if', () => { const code = ` let x @@ -764,7 +773,10 @@ export const tests = [ const {calltree, modules} = eval_modules(parsed); const frame = eval_frame(calltree, modules) assert_equal(frame.children[1].result, {ok: true}) - assert_equal(frame.children[1].children[0].children[1].result, {ok: true, value: 2}) + assert_equal( + find_node(frame, n => n.string == 'b').result.value, + 2 + ) }), test('eval_frame error', () => { @@ -972,7 +984,6 @@ export const tests = [ assert_equal(result.ok, true) }), - test('module external', () => { const code = ` // external @@ -1195,6 +1206,12 @@ export const tests = [ assert_equal(active_frame(next).children.at(-1).result.value, 'foo_value') }), + test('export value explorer', () => { + const code = 'export const x = 1' + const i = test_initial_state(code) + assert_equal(i.value_explorer.result.value, 1) + }), + // Static analysis test('undeclared', () => { @@ -1446,8 +1463,53 @@ export const tests = [ ) // assert let has result assert_equal(frame.children[0].result, {ok: true}) - // assert x value - assert_equal(frame.children[0].children[0].result, {ok: true, value: 1}) + }), + + test('multiple assignments', () => { + assert_code_evals_to( + ` + let x, y + x = 1, {y} = {y: 2} + {x,y} + `, + {x: 1, y: 2} + ) + }), + + test('assigments value explorer', () => { + const code = ` + let x + x = 1 + ` + const i = test_initial_state(code, code.indexOf('x = 1')) + assert_equal(i.value_explorer.result.value, 1) + }), + + test('multiple assigments value explorer', () => { + const code = ` + let x, y + x = 1, y = 2 + ` + const i = test_initial_state(code, code.indexOf('x = 1')) + assert_equal(i.value_explorer.result.value, {x: 1, y: 2}) + }), + + test('assigments destructuring value explorer', () => { + const code = ` + let x, y + x = 1, {y} = {y:2} + ` + const i = test_initial_state(code, code.indexOf('x = 1')) + assert_equal(i.value_explorer.result.value, {x: 1, y: 2}) + }), + + test('assigments error', () => { + const code = ` + let x, y + x = 1, y = null.foo + ` + const i = test_initial_state(code, code.indexOf('x = 1')) + assert_equal(i.value_explorer.result.ok, false) }), test('block scoping', () => { @@ -2471,15 +2533,15 @@ const y = x()` test('move_cursor let', () => { const code = ` - let x - x = 1 + let x = 1 ` const s1 = test_initial_state(code) const s2 = COMMANDS.move_cursor(s1, code.indexOf('x')) + const lettext = 'let x = 1' assert_equal(s2.value_explorer, { - index: code.indexOf('let x'), - length: 5, - result: {ok: true, value: {x: 1}}, + index: code.indexOf(lettext), + length: lettext.length, + result: {ok: true, value: 1}, }) }),