From 54d502396dc13e9ce2e43eb0ef1e153e8a5b5a5d Mon Sep 17 00:00:00 2001 From: Dmitry Vasilev Date: Wed, 2 Aug 2023 06:00:08 +0300 Subject: [PATCH] find call with respect of cursor position inside branch --- src/calltree.js | 203 +++++++++++++++++++++-------- src/cmd.js | 18 ++- src/color.js | 3 +- src/editor/editor.js | 1 + src/effects.js | 2 +- src/eval.js | 53 ++++++-- src/index.js | 2 + src/runtime.js | 35 ++++- test/test.js | 297 ++++++++++++++++++++++++++++++++++++++++++- 9 files changed, 528 insertions(+), 86 deletions(-) diff --git a/src/calltree.js b/src/calltree.js index e8f12cc..36f0b8c 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} from './eval.js' +import {eval_frame, eval_expand_calltree_node, get_after_if_path} from './eval.js' export const pp_calltree = tree => ({ id: tree.id, @@ -91,27 +91,38 @@ export const get_calltree_node_by_loc = (state, index) => { } } -const get_selected_calltree_node_by_loc = (state, node) => +const get_selected_calltree_node_by_loc = ( + state, + node, + module = state.current_module +) => state.selected_calltree_node_by_loc - ?.[state.current_module] + ?.[module] ?.[ - state.parse_result.modules[state.current_module] == node + state.parse_result.modules[module] == node // identify toplevel by index `-1`, because function and toplevel can // have the same index (in case when module starts with function_expr) ? -1 : node.index ] -const add_selected_calltree_node_by_loc = (state, loc, node_id) => { +const set_node_by_loc = (node_by_loc, loc, node_id) => { + return {...node_by_loc, + [loc.module]: { + ...node_by_loc?.[loc.module], + [loc.index ?? -1]: node_id + } + } +} + +const set_selected_calltree_node_by_loc = (state, loc, node_id) => { return { ...state, - selected_calltree_node_by_loc: - {...state.selected_calltree_node_by_loc, - [loc.module]: { - ...state.selected_calltree_node_by_loc?.[loc.module], - [loc.index ?? -1]: node_id - } - } + selected_calltree_node_by_loc: set_node_by_loc( + state.selected_calltree_node_by_loc, + loc, + node_id, + ) } } @@ -133,24 +144,38 @@ export const add_frame = ( current_calltree_node = active_calltree_node, ) => { let with_frame - if(state.frames?.[active_calltree_node.id] == null) { - const frame = eval_frame(active_calltree_node, state.modules) + let frame + frame = state.frames?.[active_calltree_node.id] + if(frame == null) { + frame = eval_frame(active_calltree_node, state.modules) + const execution_paths = active_calltree_node.toplevel + ? null + : get_execution_paths(frame) const coloring = color(frame) with_frame = {...state, frames: {...state.frames, - [active_calltree_node.id]: {...frame, coloring} + [active_calltree_node.id]: {...frame, coloring, execution_paths} } } } else { with_frame = state } - // 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, + + const loc = calltree_node_loc(active_calltree_node) + + const with_colored_frames = {...with_frame, + colored_frames: set_node_by_loc( + with_frame.colored_frames, + loc, + active_calltree_node.id, + ) + } + + return set_active_calltree_node( + with_colored_frames, + active_calltree_node, + current_calltree_node ) - return set_active_calltree_node(result, active_calltree_node, current_calltree_node) } const replace_calltree_node = (root, node, replacement) => { @@ -269,7 +294,13 @@ const jump_calltree_node = (_state, _current_calltree_node) => { // of body? : set_location(next, loc) - return {...with_location, + const with_selected_calltree_node = set_selected_calltree_node_by_loc( + with_location, + calltree_node_loc(active_calltree_node), + with_location.active_calltree_node.id, + ) + + return {...with_selected_calltree_node, value_explorer: next.current_calltree_node.toplevel ? null : { @@ -537,20 +568,88 @@ export const initial_calltree_node = state => { export const default_expand_path = state => initial_calltree_node(state).state +export const get_execution_paths = frame => { + /* + - depth-first search tree. if 'result == null', then stop. Do not descend + into function_expr + - look for 'if' and ternary + - Get executed branch (branch.result.ok != null). There may be no executed + branch if cond executed with error + - for 'if' statement we also add 'get_after_if_path(if_node.index)' + */ + const do_get = (node, next_node) => { + if(node.type == 'function_expr' || node.result == null) { + return [] + } + let after_if, branch + if(node.type == 'if' || node.type == 'ternary') { + const [cond, ...branches] = node.children + branch = branches.find(b => b.result != null) + } + if(node.type == 'if' && next_node != null && next_node.result != null) { + after_if = get_after_if_path(node) + } + const paths = [branch?.index, after_if].filter(i => i != null) + const children = node.children ?? [] + const child_paths = children + .map((c, i) => do_get(c, children[i + 1])) + .flat() + return [...paths, ...child_paths] + } + + const [args, body] = frame.children + + return new Set(do_get(body)) +} + +const find_execution_path = (node, index) => { + if(node.children == null) { + return [] + } + + const child = node.children.find(c => + c.index <= index && c.index + c.length > index + ) + + if(child == null) { + return [] + } + + const prev_ifs = node + .children + .filter(c => + c.index < child.index && c.type == 'if' + ) + .map(c => get_after_if_path(c)) + + const child_path = find_execution_path(child, index) + + // TODO other conditionals, like &&, ||, ??, ?., ?.[] + if(node.type == 'if' || node.type == 'ternary') { + const [cond, left, right] = node.children + if(child == left || child == right) { + return [...prev_ifs, child.index, ...child_path] + } + } + + return [...prev_ifs, ...child_path] +} + export const find_call_node = (state, index) => { const module = state.parse_result.modules[state.current_module] if(module == null) { // Module is not executed - return null + return {node: null, path: null} } - let node + let node, path if(index < module.index || index >= module.index + module.length) { // index is outside of module, it can happen because of whitespace and // comments in the beginning and the end node = module + path = null } else { const leaf = find_leaf(module, index) const anc = ancestry_inc(leaf, module) @@ -558,44 +657,19 @@ export const find_call_node = (state, index) => { node = fn == null ? module : fn + path = find_execution_path(node, index) } - return node + return {node, path} } export const find_call = (state, index) => { - const node = find_call_node(state, index) + const {node, path} = find_call_node(state, index) if(node == null) { return state } - if(state.active_calltree_node != null && is_eq(node, state.active_calltree_node.code)) { - return state - } - - const selected_ct_node_id = get_selected_calltree_node_by_loc(state, node) - - /* - 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(selected_ct_node_id != null) { - const ct_node = find_node( - state.calltree, - n => n.id == selected_ct_node_id - ) - if(ct_node == null) { - throw new Error('illegal state') - } - return set_active_calltree_node(state, ct_node, ct_node) - } - if(node == state.parse_result.modules[root_calltree_module(state)]) { const toplevel = root_calltree_node(state) return add_frame( @@ -610,19 +684,42 @@ export const find_call = (state, index) => { return state } + const selected_ct_node_id = get_selected_calltree_node_by_loc(state, node) + const execution_paths = selected_ct_node_id == null + ? null + : state.frames[selected_ct_node_id].execution_paths + const ct_node_id = get_calltree_node_by_loc(state, node.index) if(ct_node_id == null) { return set_active_calltree_node(state, null) } + const path_ct_node_id = [node.index, ...path] + .map(path_elem => { + const is_selected_node_hits_path = + selected_ct_node_id != null + && + (execution_paths.has(path_elem) || node.index == path_elem) + + if(is_selected_node_hits_path) { + return selected_ct_node_id + } + + // If there is no node selected for this fn, or it did not hit the path + // when executed, try to find node calltree_node_by_loc + return get_calltree_node_by_loc(state, path_elem) + }) + .findLast(node_id => node_id != null) + const ct_node = find_node( state.calltree, - n => n.id == ct_node_id + n => n.id == path_ct_node_id ) if(ct_node == null) { throw new Error('illegal state') } + return add_frame( expand_path(state, ct_node), ct_node, diff --git a/src/cmd.js b/src/cmd.js index bc3e5f3..0eb304a 100644 --- a/src/cmd.js +++ b/src/cmd.js @@ -11,9 +11,9 @@ import { root_calltree_node, root_calltree_module, make_calltree, get_deferred_calls, calltree_commands, - add_frame, calltree_node_loc, get_calltree_node_by_loc, expand_path, + add_frame, calltree_node_loc, expand_path, initial_calltree_node, default_expand_path, toggle_expanded, active_frame, - find_call, find_call_node, set_active_calltree_node, + find_call, set_active_calltree_node, set_cursor_position, current_cursor_position, set_location, } from './calltree.js' @@ -96,6 +96,7 @@ const run_code = (s, dirty_files) => { active_calltree_node: null, calltree_node_is_expanded: null, frames: null, + colored_frames: null, calltree_node_by_loc: null, selected_calltree_node_by_loc: null, selection_state: null, @@ -223,14 +224,11 @@ const eval_modules_finished = (state, prev_state, result) => { const {node, state: next2} = initial_calltree_node(next) result_state = set_active_calltree_node(next2, null, node) } else { - result_state = add_frame( - default_expand_path( - expand_path( - next, - next.active_calltree_node - ) - ), - next.active_calltree_node, + result_state = default_expand_path( + expand_path( + next, + next.active_calltree_node + ) ) } diff --git a/src/color.js b/src/color.js index c4ea553..2b4a0ea 100644 --- a/src/color.js +++ b/src/color.js @@ -209,8 +209,7 @@ export const color = frame => { export const color_file = (state, file) => Object - .values(state.selected_calltree_node_by_loc?.[file] ?? {}) - // node_id == null means it is unreachable, so do not color + .values(state.colored_frames?.[file] ?? {}) .filter(node_id => node_id != null) .map(node_id => state.frames[node_id].coloring) .flat() diff --git a/src/editor/editor.js b/src/editor/editor.js index 78bd403..c0f7794 100644 --- a/src/editor/editor.js +++ b/src/editor/editor.js @@ -52,6 +52,7 @@ const normalize_events = (ace_editor, { }) ace_editor.on('changeSelection', (...args) => { + // TODO debounce changeSelection? if(is_change_selection_supressed()) { return } diff --git a/src/effects.js b/src/effects.js index 78b4d6f..48eb077 100644 --- a/src/effects.js +++ b/src/effects.js @@ -249,7 +249,7 @@ export const apply_side_effects = (prev, next, command, ui) => { ui.calltree.render_select_node(prev, next) } - if(prev.selected_calltree_node_by_loc != next.selected_calltree_node_by_loc) { + if(prev.colored_frames != next.colored_frames) { render_coloring(ui, next) } diff --git a/src/eval.js b/src/eval.js index 5a7c975..bd710b1 100644 --- a/src/eval.js +++ b/src/eval.js @@ -72,11 +72,13 @@ const codegen_function_expr = (node, node_cxt) => { ? `(${args}) => ` : `function(${args})` + // TODO gensym __obj, __fn, __call_id + const prolog = '{const __call_id = __cxt.call_counter;' + const call = (node.is_async ? 'async ' : '') + decl + ( - // TODO gensym __obj, __fn (node.body.type == 'do') - ? '{ let __obj, __fn; ' + do_codegen(node.body) + '}' - : '{ let __obj, __fn; return ' + do_codegen(node.body) + '}' + ? prolog + do_codegen(node.body) + '}' + : prolog + 'return ' + do_codegen(node.body) + '}' ) const argscount = node @@ -135,6 +137,11 @@ ${JSON.stringify(errormessage)})` } +// Note that we use 'node.index + 1' as index, which +// does not correspond to any ast node, we just use it as a convenient +// marker +export const get_after_if_path = node => node.index + 1 + const codegen = (node, node_cxt, parent) => { const do_codegen = (n, parent) => codegen(n, node_cxt, parent) @@ -161,11 +168,20 @@ const codegen = (node, node_cxt, parent) => { } else if(node.type == 'throw') { return 'throw ' + do_codegen(node.expr) + ';' } else if(node.type == 'if') { - const left = 'if(' + do_codegen(node.cond) + '){' + - do_codegen(node.branches[0]) + ' } ' - return node.branches[1] == null + const codegen_branch = branch => + `{ __save_ct_node_for_path(__cxt, __calltree_node_by_loc, ${branch.index}, __call_id);` + + do_codegen(branch) + + '}' + const left = 'if(' + do_codegen(node.cond) + ')' + + codegen_branch(node.branches[0]) + const result = node.branches[1] == null ? left - : left + ' else { ' + do_codegen(node.branches[1]) + ' }' + : left + ' else ' + codegen_branch(node.branches[1]) + // add path also for point after if statement, in case there was a return + // inside if statement. + return result + + `__save_ct_node_for_path(__cxt, __calltree_node_by_loc, ` + + `${get_after_if_path(node)}, __call_id);` } else if(node.type == 'array_literal'){ return '[' + node.elements.map(c => do_codegen(c)).join(', ') + ']' } else if(node.type == 'object_literal'){ @@ -189,13 +205,18 @@ const codegen = (node, node_cxt, parent) => { } else if(node.type == 'function_expr'){ return codegen_function_expr(node, node_cxt) } else if(node.type == 'ternary'){ + const branches = node.branches.map(branch => + `(__save_ct_node_for_path(__cxt, __calltree_node_by_loc, ${branch.index}, __call_id), ` + + do_codegen(branch) + + ')' + ) return '' + '(' + do_codegen(node.cond) + ')\n? ' - + do_codegen(node.branches[0]) + + branches[0] +'\n: ' - + do_codegen(node.branches[1]) + + 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') { @@ -297,7 +318,7 @@ export const eval_modules = ( io_trace, location ) => { - // TODO gensym __cxt, __trace, __trace_call + // TODO gensym __cxt, __trace, __trace_call, __calltree_node_by_loc, __do_await // TODO bug if module imported twice, once as external and as regular @@ -310,11 +331,21 @@ export const eval_modules = ( const module_fns = parse_result.sorted.map(module => ( { module, + // TODO refactor, instead of multiple args prefixed with '__', pass + // single arg called `runtime` fn: new Function( '__cxt', + '__calltree_node_by_loc', '__trace', '__trace_call', '__do_await', + '__save_ct_node_for_path', + + /* Add dummy __call_id for toplevel. It does not make any sence + * (toplevel is executed only once unlike function), we only add it + * because we dont want to codegen differently for if statements in + * toplevel and if statements within functions*/ + 'const __call_id = "SOMETHING_WRONG_HAPPENED";' + codegen(parse_result.modules[module], {module}) ) } @@ -354,7 +385,7 @@ export const eval_modules = ( logs: result.logs, eval_cxt: result.eval_cxt, calltree, - calltree_node_by_loc: result.eval_cxt.calltree_node_by_loc, + calltree_node_by_loc: result.calltree_node_by_loc, io_trace: result.eval_cxt.io_trace, } } diff --git a/src/index.js b/src/index.js index 0315bd2..a227209 100644 --- a/src/index.js +++ b/src/index.js @@ -246,6 +246,7 @@ export const with_code_execution = (action, state = get_state()) => { */ if(state.eval_cxt != null) { state.eval_cxt.is_recording_deferred_calls = false + state.eval_cxt.skip_save_ct_node_for_path = true } try { @@ -253,6 +254,7 @@ export const with_code_execution = (action, state = get_state()) => { } finally { if(state.eval_cxt != null) { state.eval_cxt.is_recording_deferred_calls = true + state.eval_cxt.skip_save_ct_node_for_path = false } } } diff --git a/src/runtime.js b/src/runtime.js index 9dc2bf3..7f49b0a 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -38,20 +38,24 @@ const make_promise_with_rejector = cxt => { const do_run = function*(module_fns, cxt, io_trace){ let calltree + const calltree_node_by_loc = new Map( + module_fns.map(({module}) => [module, new Map()]) + ) + const [replay_aborted_promise, io_trace_abort_replay] = make_promise_with_rejector(cxt) cxt = (io_trace == null || io_trace.length == 0) // TODO group all io_trace_ properties to single object? ? {...cxt, + calltree_node_by_loc, logs: [], - calltree_node_by_loc: new Map(), io_trace_is_recording: true, io_trace: [], } : {...cxt, + calltree_node_by_loc, logs: [], - calltree_node_by_loc: new Map(), io_trace_is_recording: false, io_trace, io_trace_is_replay_aborted: false, @@ -79,7 +83,14 @@ const do_run = function*(module_fns, cxt, io_trace){ try { cxt.modules[module] = {} - const result = fn(cxt, __trace, __trace_call, __do_await) + const result = fn( + cxt, + calltree_node_by_loc.get(module), + __trace, + __trace_call, + __do_await, + __save_ct_node_for_path, + ) if(result instanceof cxt.window.Promise) { yield cxt.window.Promise.race([replay_aborted_promise, result]) } else { @@ -108,6 +119,7 @@ const do_run = function*(module_fns, cxt, io_trace){ calltree, logs: _logs, eval_cxt: cxt, + calltree_node_by_loc, } } @@ -244,7 +256,7 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure) => { const call_id = ++cxt.call_counter // populate calltree_node_by_loc only for entrypoint module - if(cxt.is_entrypoint) { + if(cxt.is_entrypoint && !cxt.skip_save_ct_node_for_path) { let nodes_of_module = cxt.calltree_node_by_loc.get(__location.module) if(nodes_of_module == null) { nodes_of_module = new Map() @@ -392,7 +404,6 @@ const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => { } if(is_log) { - // TODO do not collect logs on find_call? cxt.logs.push(call) } @@ -412,3 +423,17 @@ const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => { } } +const __save_ct_node_for_path = (cxt, __calltree_node_by_loc, index, __call_id) => { + if(!cxt.is_entrypoint) { + return + } + + if(cxt.skip_save_ct_node_for_path) { + return + } + + if(__calltree_node_by_loc.get(index) == null) { + __calltree_node_by_loc.set(index, __call_id) + set_record_call(cxt) + } +} diff --git a/test/test.js b/test/test.js index 4096a07..59bff91 100644 --- a/test/test.js +++ b/test/test.js @@ -9,6 +9,7 @@ import { pp_calltree, get_deferred_calls, current_cursor_position, + get_execution_paths, } from '../src/calltree.js' import {color_file} from '../src/color.js' import { @@ -1802,14 +1803,10 @@ const y = x()` countdown(10) `) const first = root_calltree_node(s).children[0] - 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] assert_equal(first2.children[0].value, 9) - assert_equal(first2.children[0].children, undefined) - assert_equal(first2.children[0].has_more_children, true) assert_equal(first2.code, first2.children[0].code) }), @@ -2544,6 +2541,279 @@ const y = x()` assert_equal(root_calltree_node(s2).module, '') }), + test('find branch initial', () => { + const code = ` + function x(cond) { + if(cond) { + return true + } else { + return false + } + } + + x(true) + x(false) + ` + const i = test_initial_state(code, code.indexOf('return false')) + assert_equal(i.value_explorer.result.value, false) + }), + + test('find branch empty branch', () => { + const code = ` + function x(cond) { + if(cond) { + /* label */ + } + } + + x(false) + x(true) + ` + const i = test_initial_state(code, code.indexOf('label')) + assert_equal(i.active_calltree_node.args[0], true) + }), + + test('find branch move_cursor', () => { + const code = ` + function x(cond) { + if(cond) { + return true + } else { + return false + } + } + + x(true) + x(false) + ` + const i = test_initial_state(code) + const moved = COMMANDS.move_cursor(i, code.indexOf('return false')) + assert_equal(moved.value_explorer.result.value, false) + assert_equal( + i.colored_frames != moved.colored_frames, + true + ) + }), + + test('find branch ternary', () => { + const code = ` + function x(cond) { + return cond ? true : false + } + + x(true) + x(false) + ` + const i = test_initial_state(code, code.indexOf('false')) + assert_equal(i.value_explorer.result.value, false) + }), + + test('find branch move cursor within fn', () => { + const code = ` + function x(cond) { + if(cond) { + return true + } else { + return false + } + } + + x(true) + x(false) + ` + const i = test_initial_state(code) + const s1 = COMMANDS.move_cursor(i, code.indexOf('return false')) + const s2 = COMMANDS.move_cursor(s1, code.indexOf('return true')) + assert_equal(s2.value_explorer.result.value, true) + assert_equal( + s1.colored_frames != s2.colored_frames, + true + ) + }), + + test('find branch fibonacci', () => { + const code = ` + function fib(n) { + if(n == 0 || n == 1) { + return n + } else { + return fib(n - 1) + fib(n - 2) + } + } + + fib(6) + ` + const i = test_initial_state(code) + const moved = COMMANDS.move_cursor(i, code.indexOf('return n')) + assert_equal(moved.value_explorer.result.value, 1) + }), + + test('find branch after if with return', () => { + const code = ` + function x(cond) { + if(cond) { + return true + } + 1 + } + x(true) + x(false) + ` + const i = test_initial_state(code, code.indexOf('1')) + assert_equal(i.value_explorer.result.value, 1) + }), + + test('find branch after if with return complex', () => { + const code = ` + function x(a, b) { + if(a) { + return true + } + if(a) { + return true + } + if(b) { + return true + } else { + if(false) { + return null + } + 1 + } + + } + x(true) + x(false, true) + x(false, false) + ` + const i = test_initial_state(code, code.indexOf('1')) + assert_equal(i.value_explorer.result.value, 1) + assert_equal(i.active_calltree_node.args, [false, false]) + }), + + test('find branch get_execution_paths', () => { + const code = ` + function x() { + if(true) {/*1*/ + } + if(false) { + } else {/*2*/ + if(true) {/*3*/ + true ? 4 : 5 + } + return null + } + // not executed + if(true) { + } + // not executed + true ? 6 : 7 + } + x() + ` + const i = test_initial_state(code, code.indexOf('if')) + assert_equal( + [...get_execution_paths(active_frame(i))].toSorted((a,b) => a - b), + [ + code.indexOf('if(true)') + 1, + code.indexOf('/*1*/') - 1, + code.indexOf('/*2*/') - 1, + code.indexOf('if(true) {/*3*/') + 1, + code.indexOf('/*3*/') - 1, + code.indexOf('4'), + ] + ) + }), + + test('find branch get_execution_paths consice body', () => { + const code = ` + const x = () => true ? 1 : 2 + x() + ` + const i = test_initial_state(code, code.indexOf('true')) + assert_equal( + get_execution_paths(active_frame(i)), + [code.indexOf('1')], + ) + }), + + test('find branch get_execution_paths nested fn', () => { + const code = ` + function x() { + function y() { + true ? 1 : 2 + } + } + x() + ` + const i = test_initial_state(code, code.indexOf('{')) + assert_equal( + get_execution_paths(active_frame(i)), + [], + ) + }), + + test('find branch jump_calltree_node', () => { + const code = ` + function test(x) { + if(x > 0) { + 'label' + } + } + test(1) + test(2) + ` + const i = test_initial_state(code, code.indexOf('label')) + assert_equal(i.active_calltree_node.args[0], 1) + // select second call + const second = COMMANDS.calltree.click(i, root_calltree_node(i).children[1].id) + assert_equal(second.active_calltree_node.args[0], 2) + }), + + test('find branch preserve selected calltree node when moving inside fn', () => { + const code = ` + function x(cond) { + if(cond) { + true + } else { + false + } + 'finish' + } + x(true) + x(false) + ` + const i = test_initial_state(code) + const first_call_id = root_calltree_node(i).children[0].id + // explicitly select first call + const selected = COMMANDS.calltree.click(i, first_call_id) + // implicitly select second call by moving cursor + const moved = COMMANDS.move_cursor(selected, code.indexOf('false')) + const finish = COMMANDS.move_cursor(moved, code.indexOf('finish')) + assert_equal(finish.active_calltree_node.id, first_call_id) + }), + + test('find branch select calltree node from logs', () => { + const code = ` + function f(x) { + if(x > 1) { + console.log(x) + } else { + console.log(x) + } + } + f(5) + f(10) + ` + const i = test_initial_state(code) + const log_selected = COMMANDS.calltree.navigate_logs_position(i, 1) + const moved = COMMANDS.move_cursor( + log_selected, + code.indexOf('console.log') + ) + assert_equal(moved.active_calltree_node.args, [10]) + }), + test('stale id in frame function_call.result.calls bug', () => { const code = ` const x = () => {/*x*/ @@ -2846,6 +3116,25 @@ const y = x()` expanded.modules[''].fn(10) }), + test('deferred_calls find call bug', () => { + const code = ` + export const fn = () => 1 + ` + + const {state: i, on_deferred_call} = test_deferred_calls_state(code) + + const moved = COMMANDS.move_cursor(i, code.indexOf('1')) + assert_equal(moved.active_calltree_node, null) + + // Make deferred call + moved.modules[''].fn(10) + + const after_call = on_deferred_call(moved) + const moved2 = COMMANDS.move_cursor(after_call, code.indexOf('1')) + + assert_equal(moved2.active_calltree_node.value, 1) + }), + test('async/await await non promise', async () => { await assert_code_evals_to_async( `