mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 13:04:30 -08:00
find call with respect of cursor position inside branch
This commit is contained in:
197
src/calltree.js
197
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 {is_eq, find_error_origin_node} from './ast_utils.js'
|
||||||
import {find_node, find_leaf, ancestry_inc} from './ast_utils.js'
|
import {find_node, find_leaf, ancestry_inc} from './ast_utils.js'
|
||||||
import {color} from './color.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 => ({
|
export const pp_calltree = tree => ({
|
||||||
id: tree.id,
|
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.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
|
// identify toplevel by index `-1`, because function and toplevel can
|
||||||
// have the same index (in case when module starts with function_expr)
|
// have the same index (in case when module starts with function_expr)
|
||||||
? -1
|
? -1
|
||||||
: node.index
|
: 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 {
|
return {...node_by_loc,
|
||||||
...state,
|
|
||||||
selected_calltree_node_by_loc:
|
|
||||||
{...state.selected_calltree_node_by_loc,
|
|
||||||
[loc.module]: {
|
[loc.module]: {
|
||||||
...state.selected_calltree_node_by_loc?.[loc.module],
|
...node_by_loc?.[loc.module],
|
||||||
[loc.index ?? -1]: node_id
|
[loc.index ?? -1]: node_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const set_selected_calltree_node_by_loc = (state, loc, node_id) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
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,
|
current_calltree_node = active_calltree_node,
|
||||||
) => {
|
) => {
|
||||||
let with_frame
|
let with_frame
|
||||||
if(state.frames?.[active_calltree_node.id] == null) {
|
let frame
|
||||||
const frame = eval_frame(active_calltree_node, state.modules)
|
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)
|
const coloring = color(frame)
|
||||||
with_frame = {...state,
|
with_frame = {...state,
|
||||||
frames: {...state.frames,
|
frames: {...state.frames,
|
||||||
[active_calltree_node.id]: {...frame, coloring}
|
[active_calltree_node.id]: {...frame, coloring, execution_paths}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
with_frame = state
|
with_frame = state
|
||||||
}
|
}
|
||||||
// TODO only add if it is not the same
|
|
||||||
const result = add_selected_calltree_node_by_loc(
|
const loc = calltree_node_loc(active_calltree_node)
|
||||||
with_frame,
|
|
||||||
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,
|
active_calltree_node.id,
|
||||||
)
|
)
|
||||||
return set_active_calltree_node(result, active_calltree_node, current_calltree_node)
|
}
|
||||||
|
|
||||||
|
return set_active_calltree_node(
|
||||||
|
with_colored_frames,
|
||||||
|
active_calltree_node,
|
||||||
|
current_calltree_node
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const replace_calltree_node = (root, node, replacement) => {
|
const replace_calltree_node = (root, node, replacement) => {
|
||||||
@@ -269,7 +294,13 @@ const jump_calltree_node = (_state, _current_calltree_node) => {
|
|||||||
// of body?
|
// of body?
|
||||||
: set_location(next, loc)
|
: 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
|
value_explorer: next.current_calltree_node.toplevel
|
||||||
? null
|
? null
|
||||||
: {
|
: {
|
||||||
@@ -537,20 +568,88 @@ export const initial_calltree_node = state => {
|
|||||||
|
|
||||||
export const default_expand_path = state => initial_calltree_node(state).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) => {
|
export const find_call_node = (state, index) => {
|
||||||
const module = state.parse_result.modules[state.current_module]
|
const module = state.parse_result.modules[state.current_module]
|
||||||
|
|
||||||
if(module == null) {
|
if(module == null) {
|
||||||
// Module is not executed
|
// 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) {
|
if(index < module.index || index >= module.index + module.length) {
|
||||||
// index is outside of module, it can happen because of whitespace and
|
// index is outside of module, it can happen because of whitespace and
|
||||||
// comments in the beginning and the end
|
// comments in the beginning and the end
|
||||||
node = module
|
node = module
|
||||||
|
path = null
|
||||||
} else {
|
} else {
|
||||||
const leaf = find_leaf(module, index)
|
const leaf = find_leaf(module, index)
|
||||||
const anc = ancestry_inc(leaf, module)
|
const anc = ancestry_inc(leaf, module)
|
||||||
@@ -558,44 +657,19 @@ export const find_call_node = (state, index) => {
|
|||||||
node = fn == null
|
node = fn == null
|
||||||
? module
|
? module
|
||||||
: fn
|
: fn
|
||||||
|
path = find_execution_path(node, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
return node
|
return {node, path}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const find_call = (state, index) => {
|
export const find_call = (state, index) => {
|
||||||
const node = find_call_node(state, index)
|
const {node, path} = find_call_node(state, index)
|
||||||
|
|
||||||
if(node == null) {
|
if(node == null) {
|
||||||
return state
|
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)]) {
|
if(node == state.parse_result.modules[root_calltree_module(state)]) {
|
||||||
const toplevel = root_calltree_node(state)
|
const toplevel = root_calltree_node(state)
|
||||||
return add_frame(
|
return add_frame(
|
||||||
@@ -610,19 +684,42 @@ export const find_call = (state, index) => {
|
|||||||
return state
|
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)
|
const ct_node_id = get_calltree_node_by_loc(state, node.index)
|
||||||
|
|
||||||
if(ct_node_id == null) {
|
if(ct_node_id == null) {
|
||||||
return set_active_calltree_node(state, 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(
|
const ct_node = find_node(
|
||||||
state.calltree,
|
state.calltree,
|
||||||
n => n.id == ct_node_id
|
n => n.id == path_ct_node_id
|
||||||
)
|
)
|
||||||
if(ct_node == null) {
|
if(ct_node == null) {
|
||||||
throw new Error('illegal state')
|
throw new Error('illegal state')
|
||||||
}
|
}
|
||||||
|
|
||||||
return add_frame(
|
return add_frame(
|
||||||
expand_path(state, ct_node),
|
expand_path(state, ct_node),
|
||||||
ct_node,
|
ct_node,
|
||||||
|
|||||||
10
src/cmd.js
10
src/cmd.js
@@ -11,9 +11,9 @@ import {
|
|||||||
root_calltree_node, root_calltree_module, make_calltree,
|
root_calltree_node, root_calltree_module, make_calltree,
|
||||||
get_deferred_calls,
|
get_deferred_calls,
|
||||||
calltree_commands,
|
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,
|
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,
|
set_cursor_position, current_cursor_position, set_location,
|
||||||
} from './calltree.js'
|
} from './calltree.js'
|
||||||
|
|
||||||
@@ -96,6 +96,7 @@ const run_code = (s, dirty_files) => {
|
|||||||
active_calltree_node: null,
|
active_calltree_node: null,
|
||||||
calltree_node_is_expanded: null,
|
calltree_node_is_expanded: null,
|
||||||
frames: null,
|
frames: null,
|
||||||
|
colored_frames: null,
|
||||||
calltree_node_by_loc: null,
|
calltree_node_by_loc: null,
|
||||||
selected_calltree_node_by_loc: null,
|
selected_calltree_node_by_loc: null,
|
||||||
selection_state: null,
|
selection_state: null,
|
||||||
@@ -223,14 +224,11 @@ const eval_modules_finished = (state, prev_state, result) => {
|
|||||||
const {node, state: next2} = initial_calltree_node(next)
|
const {node, state: next2} = initial_calltree_node(next)
|
||||||
result_state = set_active_calltree_node(next2, null, node)
|
result_state = set_active_calltree_node(next2, null, node)
|
||||||
} else {
|
} else {
|
||||||
result_state = add_frame(
|
result_state = default_expand_path(
|
||||||
default_expand_path(
|
|
||||||
expand_path(
|
expand_path(
|
||||||
next,
|
next,
|
||||||
next.active_calltree_node
|
next.active_calltree_node
|
||||||
)
|
)
|
||||||
),
|
|
||||||
next.active_calltree_node,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,8 +209,7 @@ export const color = frame => {
|
|||||||
|
|
||||||
export const color_file = (state, file) =>
|
export const color_file = (state, file) =>
|
||||||
Object
|
Object
|
||||||
.values(state.selected_calltree_node_by_loc?.[file] ?? {})
|
.values(state.colored_frames?.[file] ?? {})
|
||||||
// node_id == null means it is unreachable, so do not color
|
|
||||||
.filter(node_id => node_id != null)
|
.filter(node_id => node_id != null)
|
||||||
.map(node_id => state.frames[node_id].coloring)
|
.map(node_id => state.frames[node_id].coloring)
|
||||||
.flat()
|
.flat()
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ const normalize_events = (ace_editor, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ace_editor.on('changeSelection', (...args) => {
|
ace_editor.on('changeSelection', (...args) => {
|
||||||
|
// TODO debounce changeSelection?
|
||||||
if(is_change_selection_supressed()) {
|
if(is_change_selection_supressed()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/effects.js
vendored
2
src/effects.js
vendored
@@ -249,7 +249,7 @@ export const apply_side_effects = (prev, next, command, ui) => {
|
|||||||
ui.calltree.render_select_node(prev, next)
|
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)
|
render_coloring(ui, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
53
src/eval.js
53
src/eval.js
@@ -72,11 +72,13 @@ const codegen_function_expr = (node, node_cxt) => {
|
|||||||
? `(${args}) => `
|
? `(${args}) => `
|
||||||
: `function(${args})`
|
: `function(${args})`
|
||||||
|
|
||||||
|
// TODO gensym __obj, __fn, __call_id
|
||||||
|
const prolog = '{const __call_id = __cxt.call_counter;'
|
||||||
|
|
||||||
const call = (node.is_async ? 'async ' : '') + decl + (
|
const call = (node.is_async ? 'async ' : '') + decl + (
|
||||||
// TODO gensym __obj, __fn
|
|
||||||
(node.body.type == 'do')
|
(node.body.type == 'do')
|
||||||
? '{ let __obj, __fn; ' + do_codegen(node.body) + '}'
|
? prolog + do_codegen(node.body) + '}'
|
||||||
: '{ let __obj, __fn; return ' + do_codegen(node.body) + '}'
|
: prolog + 'return ' + do_codegen(node.body) + '}'
|
||||||
)
|
)
|
||||||
|
|
||||||
const argscount = node
|
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 codegen = (node, node_cxt, parent) => {
|
||||||
|
|
||||||
const do_codegen = (n, parent) => codegen(n, 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') {
|
} else if(node.type == 'throw') {
|
||||||
return 'throw ' + do_codegen(node.expr) + ';'
|
return 'throw ' + do_codegen(node.expr) + ';'
|
||||||
} else if(node.type == 'if') {
|
} else if(node.type == 'if') {
|
||||||
const left = 'if(' + do_codegen(node.cond) + '){' +
|
const codegen_branch = branch =>
|
||||||
do_codegen(node.branches[0]) + ' } '
|
`{ __save_ct_node_for_path(__cxt, __calltree_node_by_loc, ${branch.index}, __call_id);`
|
||||||
return node.branches[1] == null
|
+ do_codegen(branch)
|
||||||
|
+ '}'
|
||||||
|
const left = 'if(' + do_codegen(node.cond) + ')'
|
||||||
|
+ codegen_branch(node.branches[0])
|
||||||
|
const result = node.branches[1] == null
|
||||||
? left
|
? 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'){
|
} else if(node.type == 'array_literal'){
|
||||||
return '[' + node.elements.map(c => do_codegen(c)).join(', ') + ']'
|
return '[' + node.elements.map(c => do_codegen(c)).join(', ') + ']'
|
||||||
} else if(node.type == 'object_literal'){
|
} else if(node.type == 'object_literal'){
|
||||||
@@ -189,13 +205,18 @@ const codegen = (node, node_cxt, parent) => {
|
|||||||
} else if(node.type == 'function_expr'){
|
} else if(node.type == 'function_expr'){
|
||||||
return codegen_function_expr(node, node_cxt)
|
return codegen_function_expr(node, node_cxt)
|
||||||
} else if(node.type == 'ternary'){
|
} 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 ''
|
return ''
|
||||||
+ '('
|
+ '('
|
||||||
+ do_codegen(node.cond)
|
+ do_codegen(node.cond)
|
||||||
+ ')\n? '
|
+ ')\n? '
|
||||||
+ do_codegen(node.branches[0])
|
+ branches[0]
|
||||||
+'\n: '
|
+'\n: '
|
||||||
+ do_codegen(node.branches[1])
|
+ branches[1]
|
||||||
} else if(node.type == 'const'){
|
} else if(node.type == 'const'){
|
||||||
const res = 'const ' + do_codegen(node.name_node) + ' = ' + do_codegen(node.expr, node) + ';'
|
const res = 'const ' + do_codegen(node.name_node) + ' = ' + do_codegen(node.expr, node) + ';'
|
||||||
if(node.name_node.type == 'identifier' && node.expr.type == 'function_call') {
|
if(node.name_node.type == 'identifier' && node.expr.type == 'function_call') {
|
||||||
@@ -297,7 +318,7 @@ export const eval_modules = (
|
|||||||
io_trace,
|
io_trace,
|
||||||
location
|
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
|
// 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 => (
|
const module_fns = parse_result.sorted.map(module => (
|
||||||
{
|
{
|
||||||
module,
|
module,
|
||||||
|
// TODO refactor, instead of multiple args prefixed with '__', pass
|
||||||
|
// single arg called `runtime`
|
||||||
fn: new Function(
|
fn: new Function(
|
||||||
'__cxt',
|
'__cxt',
|
||||||
|
'__calltree_node_by_loc',
|
||||||
'__trace',
|
'__trace',
|
||||||
'__trace_call',
|
'__trace_call',
|
||||||
'__do_await',
|
'__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})
|
codegen(parse_result.modules[module], {module})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -354,7 +385,7 @@ export const eval_modules = (
|
|||||||
logs: result.logs,
|
logs: result.logs,
|
||||||
eval_cxt: result.eval_cxt,
|
eval_cxt: result.eval_cxt,
|
||||||
calltree,
|
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,
|
io_trace: result.eval_cxt.io_trace,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ export const with_code_execution = (action, state = get_state()) => {
|
|||||||
*/
|
*/
|
||||||
if(state.eval_cxt != null) {
|
if(state.eval_cxt != null) {
|
||||||
state.eval_cxt.is_recording_deferred_calls = false
|
state.eval_cxt.is_recording_deferred_calls = false
|
||||||
|
state.eval_cxt.skip_save_ct_node_for_path = true
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -253,6 +254,7 @@ export const with_code_execution = (action, state = get_state()) => {
|
|||||||
} finally {
|
} finally {
|
||||||
if(state.eval_cxt != null) {
|
if(state.eval_cxt != null) {
|
||||||
state.eval_cxt.is_recording_deferred_calls = true
|
state.eval_cxt.is_recording_deferred_calls = true
|
||||||
|
state.eval_cxt.skip_save_ct_node_for_path = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,20 +38,24 @@ const make_promise_with_rejector = cxt => {
|
|||||||
const do_run = function*(module_fns, cxt, io_trace){
|
const do_run = function*(module_fns, cxt, io_trace){
|
||||||
let calltree
|
let calltree
|
||||||
|
|
||||||
|
const calltree_node_by_loc = new Map(
|
||||||
|
module_fns.map(({module}) => [module, new Map()])
|
||||||
|
)
|
||||||
|
|
||||||
const [replay_aborted_promise, io_trace_abort_replay] =
|
const [replay_aborted_promise, io_trace_abort_replay] =
|
||||||
make_promise_with_rejector(cxt)
|
make_promise_with_rejector(cxt)
|
||||||
|
|
||||||
cxt = (io_trace == null || io_trace.length == 0)
|
cxt = (io_trace == null || io_trace.length == 0)
|
||||||
// TODO group all io_trace_ properties to single object?
|
// TODO group all io_trace_ properties to single object?
|
||||||
? {...cxt,
|
? {...cxt,
|
||||||
|
calltree_node_by_loc,
|
||||||
logs: [],
|
logs: [],
|
||||||
calltree_node_by_loc: new Map(),
|
|
||||||
io_trace_is_recording: true,
|
io_trace_is_recording: true,
|
||||||
io_trace: [],
|
io_trace: [],
|
||||||
}
|
}
|
||||||
: {...cxt,
|
: {...cxt,
|
||||||
|
calltree_node_by_loc,
|
||||||
logs: [],
|
logs: [],
|
||||||
calltree_node_by_loc: new Map(),
|
|
||||||
io_trace_is_recording: false,
|
io_trace_is_recording: false,
|
||||||
io_trace,
|
io_trace,
|
||||||
io_trace_is_replay_aborted: false,
|
io_trace_is_replay_aborted: false,
|
||||||
@@ -79,7 +83,14 @@ const do_run = function*(module_fns, cxt, io_trace){
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
cxt.modules[module] = {}
|
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) {
|
if(result instanceof cxt.window.Promise) {
|
||||||
yield cxt.window.Promise.race([replay_aborted_promise, result])
|
yield cxt.window.Promise.race([replay_aborted_promise, result])
|
||||||
} else {
|
} else {
|
||||||
@@ -108,6 +119,7 @@ const do_run = function*(module_fns, cxt, io_trace){
|
|||||||
calltree,
|
calltree,
|
||||||
logs: _logs,
|
logs: _logs,
|
||||||
eval_cxt: cxt,
|
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
|
const call_id = ++cxt.call_counter
|
||||||
|
|
||||||
// populate calltree_node_by_loc only for entrypoint module
|
// 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)
|
let nodes_of_module = cxt.calltree_node_by_loc.get(__location.module)
|
||||||
if(nodes_of_module == null) {
|
if(nodes_of_module == null) {
|
||||||
nodes_of_module = new Map()
|
nodes_of_module = new Map()
|
||||||
@@ -392,7 +404,6 @@ const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(is_log) {
|
if(is_log) {
|
||||||
// TODO do not collect logs on find_call?
|
|
||||||
cxt.logs.push(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
297
test/test.js
297
test/test.js
@@ -9,6 +9,7 @@ import {
|
|||||||
pp_calltree,
|
pp_calltree,
|
||||||
get_deferred_calls,
|
get_deferred_calls,
|
||||||
current_cursor_position,
|
current_cursor_position,
|
||||||
|
get_execution_paths,
|
||||||
} from '../src/calltree.js'
|
} from '../src/calltree.js'
|
||||||
import {color_file} from '../src/color.js'
|
import {color_file} from '../src/color.js'
|
||||||
import {
|
import {
|
||||||
@@ -1802,14 +1803,10 @@ const y = x()`
|
|||||||
countdown(10)
|
countdown(10)
|
||||||
`)
|
`)
|
||||||
const first = root_calltree_node(s).children[0]
|
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)
|
assert_equal(first.value, 10)
|
||||||
const s2 = COMMANDS.calltree.click(s, first.id)
|
const s2 = COMMANDS.calltree.click(s, first.id)
|
||||||
const first2 = root_calltree_node(s2).children[0]
|
const first2 = root_calltree_node(s2).children[0]
|
||||||
assert_equal(first2.children[0].value, 9)
|
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)
|
assert_equal(first2.code, first2.children[0].code)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -2544,6 +2541,279 @@ const y = x()`
|
|||||||
assert_equal(root_calltree_node(s2).module, '')
|
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', () => {
|
test('stale id in frame function_call.result.calls bug', () => {
|
||||||
const code = `
|
const code = `
|
||||||
const x = () => {/*x*/
|
const x = () => {/*x*/
|
||||||
@@ -2846,6 +3116,25 @@ const y = x()`
|
|||||||
expanded.modules[''].fn(10)
|
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 () => {
|
test('async/await await non promise', async () => {
|
||||||
await assert_code_evals_to_async(
|
await assert_code_evals_to_async(
|
||||||
`
|
`
|
||||||
|
|||||||
Reference in New Issue
Block a user