From adbca074c2c79abe8a1ef8fa2b8581f5f2ed441e Mon Sep 17 00:00:00 2001 From: Dmitry Vasilev Date: Wed, 2 Aug 2023 05:59:49 +0300 Subject: [PATCH] find_call eagerly --- src/calltree.js | 117 ++++++++++-------------------- src/cmd.js | 84 +++++++--------------- src/color.js | 2 +- src/effects.js | 4 +- src/eval.js | 23 +----- src/runtime.js | 149 ++++++++++----------------------------- test/self_hosted_test.js | 3 +- test/test.js | 57 +++++++++------ test/utils.js | 9 +-- 9 files changed, 144 insertions(+), 304 deletions(-) diff --git a/src/calltree.js b/src/calltree.js index 8c70c76..e8f12cc 100644 --- a/src/calltree.js +++ b/src/calltree.js @@ -2,7 +2,7 @@ import {map_accum, map_find, map_object, stringify, findLast} from './utils.js' import {is_eq, find_error_origin_node} from './ast_utils.js' import {find_node, find_leaf, ancestry_inc} from './ast_utils.js' import {color} from './color.js' -import {eval_frame, eval_expand_calltree_node, eval_find_call} from './eval.js' +import {eval_frame, eval_expand_calltree_node} from './eval.js' export const pp_calltree = tree => ({ id: tree.id, @@ -82,8 +82,17 @@ export const is_native_fn = calltree_node => export const active_frame = state => state.frames[state.active_calltree_node.id] -const get_calltree_node_by_loc = (state, node) => - state.calltree_node_by_loc +export const get_calltree_node_by_loc = (state, index) => { + const nodes_of_module = state.calltree_node_by_loc.get(state.current_module) + if(nodes_of_module == null) { + return null + } else { + return nodes_of_module.get(index) + } +} + +const get_selected_calltree_node_by_loc = (state, node) => + state.selected_calltree_node_by_loc ?.[state.current_module] ?.[ state.parse_result.modules[state.current_module] == node @@ -93,13 +102,13 @@ const get_calltree_node_by_loc = (state, node) => : node.index ] -const add_calltree_node_by_loc = (state, loc, node_id) => { +const add_selected_calltree_node_by_loc = (state, loc, node_id) => { return { ...state, - calltree_node_by_loc: - {...state.calltree_node_by_loc, + selected_calltree_node_by_loc: + {...state.selected_calltree_node_by_loc, [loc.module]: { - ...state.calltree_node_by_loc?.[loc.module], + ...state.selected_calltree_node_by_loc?.[loc.module], [loc.index ?? -1]: node_id } } @@ -111,18 +120,11 @@ export const set_active_calltree_node = ( active_calltree_node, current_calltree_node = state.current_calltree_node, ) => { - const result = { + return { ...state, active_calltree_node, current_calltree_node, } - // TODO currently commented, required to implement livecoding second and - // subsequent fn calls - /* - // Record last_good_state every time active_calltree_node changes - return {...result, last_good_state: result} - */ - return result } export const add_frame = ( @@ -142,7 +144,8 @@ export const add_frame = ( } else { with_frame = state } - const result = add_calltree_node_by_loc( + // TODO only add if it is not the same + const result = add_selected_calltree_node_by_loc( with_frame, calltree_node_loc(active_calltree_node), active_calltree_node.id, @@ -571,18 +574,21 @@ export const find_call = (state, index) => { return state } - const ct_node_id = get_calltree_node_by_loc(state, node) + const selected_ct_node_id = get_selected_calltree_node_by_loc(state, node) - if(ct_node_id === null) { + /* + TODO remove because it interferes with find_call deferred calls + if(selected_ct_node_id === null) { // strict compare (===) with null, to check if we put null earlier to // designate that fn is not reachable return set_active_calltree_node(state, null) } + */ - if(ct_node_id != null) { + if(selected_ct_node_id != null) { const ct_node = find_node( state.calltree, - n => n.id == ct_node_id + n => n.id == selected_ct_node_id ) if(ct_node == null) { throw new Error('illegal state') @@ -604,69 +610,22 @@ export const find_call = (state, index) => { return state } - const loc = {index: node.index, module: state.current_module} - - // First try to find node among existing calltree nodes - const call = find_node(state.calltree, node => - true - && node.fn != null - && node.fn.__location != null - && node.fn.__location.index == loc.index - && node.fn.__location.module == loc.module - ) + const ct_node_id = get_calltree_node_by_loc(state, node.index) - let next_calltree, active_calltree_node - - if(call != null) { - if(call.has_more_children) { - active_calltree_node = eval_expand_calltree_node( - // TODO copy eval_cxt? - state.eval_cxt, - state.parse_result, - call - ) - next_calltree = replace_calltree_node( - state.calltree, - call, - active_calltree_node - ) - } else { - active_calltree_node = call - next_calltree = state.calltree - } - } else { - const find_result = eval_find_call( - // TODO copy eval_cxt? - state.eval_cxt, - state.parse_result, - state.calltree, - loc - ) - if(find_result == null) { - return add_calltree_node_by_loc( - // Remove active_calltree_node - // current_calltree_node may stay not null, because it is calltree node - // explicitly selected by user in calltree view - set_active_calltree_node(state, null), - loc, - null - ) - } - - active_calltree_node = find_result.call - next_calltree = replace_calltree_node( - state.calltree, - find_node(state.calltree, n => n.id == find_result.node.id), - find_result.node, - ) + if(ct_node_id == null) { + return set_active_calltree_node(state, null) } + const ct_node = find_node( + state.calltree, + n => n.id == ct_node_id + ) + if(ct_node == null) { + throw new Error('illegal state') + } return add_frame( - expand_path( - {...state, calltree: next_calltree}, - active_calltree_node - ), - active_calltree_node, + expand_path(state, ct_node), + ct_node, ) } diff --git a/src/cmd.js b/src/cmd.js index e789e06..bc3e5f3 100644 --- a/src/cmd.js +++ b/src/cmd.js @@ -11,10 +11,10 @@ import { root_calltree_node, root_calltree_module, make_calltree, get_deferred_calls, calltree_commands, - add_frame, calltree_node_loc, expand_path, + add_frame, calltree_node_loc, get_calltree_node_by_loc, expand_path, initial_calltree_node, default_expand_path, toggle_expanded, active_frame, find_call, find_call_node, set_active_calltree_node, - set_cursor_position, current_cursor_position, set_location + set_cursor_position, current_cursor_position, set_location, } from './calltree.js' const collect_logs = (logs, call) => { @@ -45,6 +45,8 @@ const apply_eval_result = (state, eval_result) => { return { ...state, calltree: make_calltree(eval_result.calltree, null), + calltree_node_by_loc: eval_result.calltree_node_by_loc, + // TODO copy eval_cxt? eval_cxt: eval_result.eval_cxt, logs: { logs: collect_logs(eval_result.logs, eval_result.calltree), @@ -95,6 +97,7 @@ const run_code = (s, dirty_files) => { calltree_node_is_expanded: null, frames: null, calltree_node_by_loc: null, + selected_calltree_node_by_loc: null, selection_state: null, loading_external_imports_state: null, value_explorer: null, @@ -185,75 +188,38 @@ const external_imports_loaded = ( } } - const node = find_call_node(state, current_cursor_position(state)) - - let toplevel, result - - if( - // edit module that is not imported (maybe recursively by state.entrypoint) - // TODO if module not imported, then do not run code on edit at all - node == null - || - node.type == 'do' /* toplevel AST node */ - ) { - result = eval_modules( - state.parse_result, - external_imports, - state.on_deferred_call, - state.calltree_changed_token, - state.io_trace, - ) - toplevel = true - } else { - result = eval_modules( - state.parse_result, - external_imports, - state.on_deferred_call, - state.calltree_changed_token, - state.io_trace, - {index: node.index, module: state.current_module}, - ) - toplevel = false - } + // TODO if module not imported, then do not run code on edit at all + const result = eval_modules( + state.parse_result, + external_imports, + state.on_deferred_call, + state.calltree_changed_token, + state.io_trace, + ) if(result.then != null) { return {...state, - eval_modules_state: { - promise: result, node, toplevel, - } + eval_modules_state: { promise: result } } } else { - return eval_modules_finished(state, state, result, node, toplevel) + return eval_modules_finished(state, state, result) } } -const eval_modules_finished = (state, prev_state, result, node, toplevel) => { +const eval_modules_finished = (state, prev_state, result) => { if(state.calltree_changed_token != prev_state.calltree_changed_token) { // code was modified after prev vesion of code was executed, discard return state } - const next = apply_eval_result(state, result) - let active_calltree_node - - if(toplevel) { - if(node == state.parse_result.modules[root_calltree_module(next)]) { - active_calltree_node = root_calltree_node(next) - } else { - active_calltree_node = null - } - } else { - if(result.call == null) { - // Unreachable call - active_calltree_node = null - } else { - active_calltree_node = result.call - } - } + const next = find_call( + apply_eval_result(state, result), + current_cursor_position(state) + ) let result_state - if(active_calltree_node == null) { + if(next.active_calltree_node == null) { const {node, state: next2} = initial_calltree_node(next) result_state = set_active_calltree_node(next2, null, node) } else { @@ -261,10 +227,10 @@ const eval_modules_finished = (state, prev_state, result, node, toplevel) => { default_expand_path( expand_path( next, - active_calltree_node + next.active_calltree_node ) ), - active_calltree_node, + next.active_calltree_node, ) } @@ -883,7 +849,7 @@ const open_app_window = (state, globals) => { }) } -const get_initial_state = (state, entrypoint_settings) => { +const get_initial_state = (state, entrypoint_settings, cursor_pos = 0) => { const with_files = state.project_dir == null ? state : load_files(state, state.project_dir) @@ -892,7 +858,7 @@ const get_initial_state = (state, entrypoint_settings) => { return { ...with_settings, - cursor_position_by_file: {[with_settings.current_module]: 0}, + cursor_position_by_file: {[with_settings.current_module]: cursor_pos}, } } diff --git a/src/color.js b/src/color.js index ec8e78c..c4ea553 100644 --- a/src/color.js +++ b/src/color.js @@ -209,7 +209,7 @@ export const color = frame => { export const color_file = (state, file) => Object - .values(state.calltree_node_by_loc?.[file] ?? {}) + .values(state.selected_calltree_node_by_loc?.[file] ?? {}) // node_id == null means it is unreachable, so do not color .filter(node_id => node_id != null) .map(node_id => state.frames[node_id].coloring) diff --git a/src/effects.js b/src/effects.js index aca0efd..78b4d6f 100644 --- a/src/effects.js +++ b/src/effects.js @@ -185,8 +185,6 @@ export const apply_side_effects = (prev, next, command, ui) => { exec('eval_modules_finished', next, /* becomes prev_state */ result, - s.node, - s.toplevel ) }) } @@ -251,7 +249,7 @@ export const apply_side_effects = (prev, next, command, ui) => { ui.calltree.render_select_node(prev, next) } - if(prev.calltree_node_by_loc != next.calltree_node_by_loc) { + if(prev.selected_calltree_node_by_loc != next.selected_calltree_node_by_loc) { render_coloring(ui, next) } diff --git a/src/eval.js b/src/eval.js index 19a27a4..5a7c975 100644 --- a/src/eval.js +++ b/src/eval.js @@ -17,7 +17,7 @@ import {has_toplevel_await} from './find_definitions.js' // import runtime as external because it has non-functional code // external -import {run, do_eval_expand_calltree_node, do_eval_find_call} from './runtime.js' +import {run, do_eval_expand_calltree_node} from './runtime.js' // TODO: fix error messages. For example, "__fn is not a function" @@ -331,9 +331,6 @@ export const eval_modules = ( // TODO use native array for stack for perf? stack contains booleans stack: new Array(), - searched_location: location, - found_call: null, - is_recording_deferred_calls: false, on_deferred_call: (call, calltree_changed_token, logs) => { return on_deferred_call( @@ -352,15 +349,12 @@ export const eval_modules = ( const make_result = result => { const calltree = assign_code(parse_result.modules, result.calltree) - const call = result.call == null - ? null - : find_node(calltree, node => node.id == result.call.id) return { modules: result.modules, logs: result.logs, eval_cxt: result.eval_cxt, calltree, - call, + calltree_node_by_loc: result.eval_cxt.calltree_node_by_loc, io_trace: result.eval_cxt.io_trace, } } @@ -372,19 +366,6 @@ export const eval_modules = ( } } -export const eval_find_call = (cxt, parse_result, calltree, location) => { - const result = do_eval_find_call(cxt, calltree, location) - if(result == null) { - return null - } - const {node, call} = result - const node_with_code = assign_code(parse_result.modules, node) - const call_with_code = find_node(node_with_code, n => n.id == call.id) - return { - node: node_with_code, - call: call_with_code, - } -} export const eval_expand_calltree_node = (cxt, parse_result, node) => { return assign_code( diff --git a/src/runtime.js b/src/runtime.js index b5822b8..9dc2bf3 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -45,11 +45,13 @@ const do_run = function*(module_fns, cxt, io_trace){ // TODO group all io_trace_ properties to single object? ? {...cxt, logs: [], + calltree_node_by_loc: new Map(), io_trace_is_recording: true, io_trace: [], } : {...cxt, logs: [], + calltree_node_by_loc: new Map(), io_trace_is_recording: false, io_trace, io_trace_is_replay_aborted: false, @@ -63,13 +65,16 @@ const do_run = function*(module_fns, cxt, io_trace){ apply_promise_patch(cxt) set_current_context(cxt) - for(let {module, fn} of module_fns) { - cxt.found_call = null + for(let i = 0; i < module_fns.length; i++) { + const {module, fn} = module_fns[i] + + cxt.is_entrypoint = i == module_fns.length - 1 + cxt.children = null calltree = { toplevel: true, module, - id: cxt.call_counter++ + id: ++cxt.call_counter, } try { @@ -98,14 +103,9 @@ const do_run = function*(module_fns, cxt, io_trace){ remove_promise_patch(cxt) - cxt.searched_location = null - const call = cxt.found_call - cxt.found_call = null - return { modules: cxt.modules, calltree, - call, logs: _logs, eval_cxt: cxt, } @@ -170,23 +170,6 @@ export const set_record_call = cxt => { } } -const do_expand_calltree_node = (cxt, node) => { - if(node.fn.__location != null) { - // fn is hosted, it created call, this time with children - const result = cxt.children[0] - result.id = node.id - result.children = cxt.prev_children - result.has_more_children = false - return result - } else { - // fn is native, it did not created call, only its child did - return {...node, - children: cxt.children, - has_more_children: false, - } - } -} - export const do_eval_expand_calltree_node = (cxt, node) => { cxt.is_recording_deferred_calls = false cxt.children = null @@ -199,80 +182,29 @@ export const do_eval_expand_calltree_node = (cxt, node) => { } catch(e) { // do nothing. Exception was caught and recorded inside '__trace' } + cxt.is_recording_deferred_calls = true - const result = do_expand_calltree_node(cxt, node) - cxt.children = null - return result -} - - -/* - Try to find call of function with given 'location' - - Function is synchronous, because we recorded calltree nodes for all async - function calls. Here we walk over calltree, find leaves that have - 'has_more_children' set to true, and rerunning fns in these leaves with - 'searched_location' being set, until we find find call or no children - left. - - We dont rerun entire execution because we want find_call to be - synchronous for simplicity -*/ -export const do_eval_find_call = (cxt, calltree, location) => { - // TODO always cleanup children when work finished, not before it started + const children = cxt.children cxt.children = null - const do_find = node => { - if(node.children != null) { - for(let c of node.children) { - const result = do_find(c) - if(result != null) { - return result - } - } - // call was not find in children, return null - return null + if(node.fn.__location != null) { + // fn is hosted, it created call, this time with children + const result = children[0] + result.id = node.id + result.children = cxt.prev_children + result.has_more_children = false + return result + } else { + // fn is native, it did not created call, only its child did + return {...node, + children: children, + has_more_children: false, } - - - if(node.has_more_children) { - try { - if(node.is_new) { - new node.fn(...node.args) - } else { - node.fn.apply(node.context, node.args) - } - } catch(e) { - // do nothing. Exception was caught and recorded inside '__trace' - } - - if(cxt.found_call != null) { - return { - node: do_expand_calltree_node(cxt, node), - call: cxt.found_call, - } - } else { - cxt.children = null - } - } - - // node has no children, return null - return null } - - cxt.is_recording_deferred_calls = false - cxt.searched_location = location - - const result = do_find(calltree) - - cxt.children = null - cxt.searched_location = null - cxt.found_call = null - cxt.is_recording_deferred_calls = true - - return result } + + const __do_await = async (cxt, value) => { // children is an array of child calls for current function call. But it // can be null to save one empty array allocation in case it has no child @@ -309,19 +241,19 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure) => { cxt.children = null cxt.stack.push(false) - const is_found_call = - (cxt.searched_location != null && cxt.found_call == null) - && - ( - __location.index == cxt.searched_location.index - && - __location.module == cxt.searched_location.module - ) + const call_id = ++cxt.call_counter - if(is_found_call) { - // Assign temporary value to prevent nested calls from populating - // found_call - cxt.found_call = {} + // populate calltree_node_by_loc only for entrypoint module + if(cxt.is_entrypoint) { + let nodes_of_module = cxt.calltree_node_by_loc.get(__location.module) + if(nodes_of_module == null) { + nodes_of_module = new Map() + cxt.calltree_node_by_loc.set(__location.module, nodes_of_module) + } + if(nodes_of_module.get(__location.index) == null) { + set_record_call(cxt) + nodes_of_module.set(__location.index, call_id) + } } let ok, value, error @@ -351,7 +283,7 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure) => { cxt.prev_children = cxt.children const call = { - id: cxt.call_counter++, + id: call_id, ok, value, error, @@ -362,11 +294,6 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure) => { : args.slice(0, argscount), } - if(is_found_call) { - cxt.found_call = call - set_record_call(cxt) - } - const should_record_call = cxt.stack.pop() if(should_record_call) { @@ -453,7 +380,7 @@ const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => { cxt.prev_children = cxt.children const call = { - id: cxt.call_counter++, + id: ++cxt.call_counter, ok, value, error, diff --git a/test/self_hosted_test.js b/test/self_hosted_test.js index 19aca18..c8f05e1 100644 --- a/test/self_hosted_test.js +++ b/test/self_hosted_test.js @@ -69,8 +69,9 @@ console.time('run') const i = test_initial_state( {}, // files + undefined, + {project_dir: dir}, {entrypoint: 'test/run.js'}, - {project_dir: dir} ) assert_equal(i.loading_external_imports_state != null, true) diff --git a/test/test.js b/test/test.js index 2a54477..4096a07 100644 --- a/test/test.js +++ b/test/test.js @@ -1426,7 +1426,8 @@ export const tests = [ ] ) - const step_into = COMMANDS.calltree.click(initial, 1) + const x_call = root_calltree_node(initial).children[0] + const step_into = COMMANDS.calltree.click(initial, x_call.id) assert_equal( color_file(step_into, '').sort((a,b) => a.index - b.index), [ @@ -1479,7 +1480,8 @@ export const tests = [ x()` const initial = test_initial_state(code) - const step_into = COMMANDS.calltree.click(initial, 1) + const x_call = root_calltree_node(initial).children[0] + const step_into = COMMANDS.calltree.click(initial, x_call.id) assert_equal( color_file(step_into, '').sort((c1, c2) => c1.index - c2.index), @@ -1563,6 +1565,20 @@ const y = x()` ) }), + test('coloring function body after move inside', () => { + const code = ` + const x = () => { + 1 + } + x() + ` + const i = test_initial_state(code) + const moved = COMMANDS.move_cursor(i, code.indexOf('1')) + const coloring = color_file(moved, '') + const color_body = coloring.find(c => c.index == code.indexOf('(')) + assert_equal(color_body.result.ok, true) + }), + test('better parse errors', () => { const code = ` const x = z => { @@ -1634,7 +1650,7 @@ const y = x()` assert_equal(res.result.value, 6) assert_equal( - n.calltree_node_by_loc[''][edited.indexOf('foo =>')] == null, + n.calltree_node_by_loc.get('').get(edited.indexOf('foo =>')) == null, false ) }), @@ -1786,8 +1802,8 @@ const y = x()` countdown(10) `) const first = root_calltree_node(s).children[0] - assert_equal(first.children, undefined) - assert_equal(first.has_more_children, true) + assert_equal(first.children[0].children, undefined) + assert_equal(first.children[0].has_more_children, true) assert_equal(first.value, 10) const s2 = COMMANDS.calltree.click(s, first.id) const first2 = root_calltree_node(s2).children[0] @@ -1816,7 +1832,6 @@ const y = x()` test('expand_calltree_node native', () => { const s = test_initial_state(`[1,2,3].map(x => x + 1)`) const map = root_calltree_node(s).children[0] - assert_equal(map.children, null) const s2 = COMMANDS.calltree.click(s, map.id) const map_expanded = root_calltree_node(s2).children[0] assert_equal(map_expanded.children.length, 3) @@ -1854,18 +1869,11 @@ const y = x()` y([1,2,3]) ` - const assert_loc = (s, substring, is_assert_node_by_loc) => { + const assert_loc = (s, substring) => { const state = COMMANDS.calltree.arrow_right(s) const index = code.indexOf(substring) assert_equal(current_cursor_position(state), index) - if(is_assert_node_by_loc) { - assert_equal( - state.calltree_node_by_loc[''][index] == null, - false - ) - } assert_equal(active_frame(state) != null, true) - return state } @@ -1876,7 +1884,7 @@ const y = x()` const s2 = assert_loc(s1, 'y([') // Expand call of `y()` - const s3 = assert_loc(s2, 'arr =>', true) + const s3 = assert_loc(s2, 'arr =>') // Select call of arr.map const s4 = assert_loc(s3, 'arr.map') @@ -1886,8 +1894,7 @@ const y = x()` const s5 = assert_loc(s4, 'arr.map') // Select call of x - const s6 = assert_loc(s5, 'foo =>', true) - + const s6 = assert_loc(s5, 'foo =>') }), test('jump_calltree select callsite', () => { @@ -1918,6 +1925,14 @@ const y = x()` assert_equal(good.code.index, code.indexOf('() => {/*good')) }), + test('jump_calltree select another call of the same fn', () => { + const code = '[1,2].map(x => x*10)' + const i = test_initial_state(code, code.indexOf('10')) + assert_equal(i.value_explorer.result.value, 10) + const second_iter = COMMANDS.calltree.arrow_down(i) + const moved = COMMANDS.move_cursor(second_iter, code.indexOf('x*10')) + assert_equal(moved.value_explorer.result.value, 20) + }), test('unwind_stack', () => { const s = test_initial_state(` @@ -2079,10 +2094,6 @@ const y = x()` assert_equal(node.children.length, 3) assert_equal(node.code != null, true) - // Siblings are not expanded - assert_equal(node.children[0].has_more_children, true) - assert_equal(node.children[2].has_more_children, true) - return find_target(node.children[1], i + 1) } @@ -2091,8 +2102,6 @@ const y = x()` assert_equal(target.args, [10]) const target2 = target.children[0] - // Target is expanded, but only one level deep - assert_equal(target2.has_more_children, true) }), test('find_call error', () => { @@ -2567,6 +2576,8 @@ const y = x()` '' : `import {x} from 'x'; x()`, 'x' : `export const x = () => 1; x()`, }, + undefined, + undefined, { entrypoint: '', current_module: 'x', diff --git a/test/utils.js b/test/utils.js index 527b232..7725440 100644 --- a/test/utils.js +++ b/test/utils.js @@ -102,7 +102,7 @@ export const assert_code_error_async = async (codestring, error) => { assert_equal(result.error, error) } -export const test_initial_state = (code, entrypoint_settings, other) => { +export const test_initial_state = (code, cursor_pos, other, entrypoint_settings) => { return COMMANDS.open_app_window( COMMANDS.get_initial_state( { @@ -113,7 +113,8 @@ export const test_initial_state = (code, entrypoint_settings, other) => { entrypoint: '', current_module: '', ...entrypoint_settings, - } + }, + cursor_pos ), new Set(Object.getOwnPropertyNames(globalThis.app_window)) ) @@ -127,8 +128,6 @@ export const test_initial_state_async = async code => { s, s, result, - s.eval_modules_state.node, - s.eval_modules_state.toplevel ) } @@ -139,8 +138,6 @@ export const command_input_async = async (...args) => { after_input, after_input, result, - after_input.eval_modules_state.node, - after_input.eval_modules_state.toplevel ) }