From 4b32433748cb0153a361146a3e4bfaeeac8b3b1f Mon Sep 17 00:00:00 2001 From: Dmitry Vasilev Date: Fri, 17 Nov 2023 12:44:12 +0800 Subject: [PATCH] mutable closures (let variables) --- README.md | 12 +- src/analyze_versioned_let_vars.js | 150 ++++++++ src/ast_utils.js | 28 +- src/eval.js | 418 +++++++++++++------- src/find_definitions.js | 1 - src/parse_js.js | 51 ++- src/runtime.js | 146 ++++++- test/self_hosted_test.js | 5 + test/test.js | 611 +++++++++++++++++++++++++++++- test/utils.js | 7 +- 10 files changed, 1272 insertions(+), 157 deletions(-) create mode 100644 src/analyze_versioned_let_vars.js diff --git a/README.md b/README.md index 1ca361c..ba38dcf 100644 --- a/README.md +++ b/README.md @@ -33,17 +33,7 @@ Leporello.js source code is developed within Leporello.js itself ## Supported javascript subset -Variables are declared using the `const` declaration. The use of `var` is not supported. `let` variables can be declared without an initial assignment, which is useful in cases where the value depends on a condition. Here's an example: -``` -let result -if (n == 0 || n == 1) { - result = n -} else { - result = fib(n - 1) + fib(n - 2) -} -``` - -Currently, only a single declaration for a single `const` statement is supported (TODO). +Variables are declared using the `const` or 'let' declaration. The use of `var` is not supported. Loops of any kind are not supported. Instead, consider using recursion or array functions as alternatives. diff --git a/src/analyze_versioned_let_vars.js b/src/analyze_versioned_let_vars.js new file mode 100644 index 0000000..7749bbd --- /dev/null +++ b/src/analyze_versioned_let_vars.js @@ -0,0 +1,150 @@ +import {collect_destructuring_identifiers} from './ast_utils.js' + +const set_versioned_let_vars = (node, closed_let_vars, assigned_vars) => { + if( + node.type == 'identifier' + && closed_let_vars.find(index => node.index == index) != null + && assigned_vars.find(index => node.index == index) != null + ) { + return {...node, is_versioned_let_var: true} + } else if(node.children != null) { + return { + ...node, + children: node.children.map(c => + set_versioned_let_vars(c, closed_let_vars, assigned_vars) + ) + } + } else { + return node + } +} + +const do_find_versioned_let_vars = (node, current_fn) => { + const children_result = node + .children + .map(c => find_versioned_let_vars(c, current_fn)) + const children = children_result.map(r => r.node) + const closed_let_vars = children_result + .flatMap(r => r.closed_let_vars) + .filter(r => r != null) + const assigned_vars = children_result + .flatMap(r => r.assigned_vars) + .filter(r => r != null) + return { + node: {...node, children}, + closed_let_vars, + assigned_vars, + } +} + +const has_versioned_let_vars = (node, is_root = true) => { + if(node.type == 'identifier' && node.is_versioned_let_var) { + return true + } else if(node.type == 'function_expr' && !is_root) { + return false + } else if(node.children != null) { + return node.children.find(c => has_versioned_let_vars(c, false)) != null + } else { + return false + } +} + +// TODO function args +export const find_versioned_let_vars = (node, current_fn = node) => { + /* + Assigns 'is_versioned_let_var: true' to let variables that are + - assigned after declaration + - closed in nested function + and sets 'has_versioned_let_vars: true' for functions that have versioned + let vars. + + - Traverse AST + - collects closed_let_vars and assigned_vars going from AST bottom to root: + - For every assignment, add assigned var to assigned_vars + - For every use of identifier, check if it used in function where it + declared, and populate assigned_vars otherwise + - for 'do' node, find 'let' declarations and set is_versioned_let_var. + */ + if(node.type == 'do') { + const {node: result, closed_let_vars, assigned_vars} + = do_find_versioned_let_vars(node, current_fn) + const next_node = { + ...result, + children: result.children.map(c => { + if(c.type != 'let') { + return c + } else { + const children = c.children.map(decl => { + if(decl.type == 'identifier') { + return set_versioned_let_vars(decl, closed_let_vars, assigned_vars) + } else if(decl.type == 'decl_pair') { + const [left, right] = decl.children + return { + ...decl, + children: [ + set_versioned_let_vars(left, closed_let_vars, assigned_vars), + right + ] + } + } else { + throw new Error('illegal state') + } + }) + return {...c, children} + } + }) + } + return { + node: node == current_fn + // toplevel + ? {...next_node, has_versioned_let_vars: has_versioned_let_vars(next_node)} + : next_node, + closed_let_vars, + assigned_vars, + } + } else if(node.type == 'assignment') { + const {node: next_node, closed_let_vars, assigned_vars} + = do_find_versioned_let_vars(node, current_fn) + const next_assigned_vars = node.children.flatMap(decl_pair => + collect_destructuring_identifiers(decl_pair).map(id => { + if(id.definition.index == null) { + throw new Error('illegal state') + } + return id.definition.index + }) + ) + return { + node: next_node, + closed_let_vars, + assigned_vars: [...(assigned_vars ?? []), ...next_assigned_vars], + } + } else if(node.type == 'function_expr') { + const result = do_find_versioned_let_vars(node, node) + return { + ...result, + node: { + ...result.node, + has_versioned_let_vars: has_versioned_let_vars(result.node) + } + } + } else if(node.children != null) { + return do_find_versioned_let_vars(node, current_fn) + } else if(node.type == 'identifier') { + if(node.definition == 'self') { + return {node, closed_let_vars: null, assigned_vars: null} + } + const index = node.definition.index + if(!(index >= current_fn.index && index < current_fn.index + current_fn.length)) { + // used let var from parent function scope + return { + node, + closed_let_vars: [index], + assigned_vars: null, + } + } else { + return {node, closed_let_vars: null, assigned_vars: null} + } + } else if(node.children == null) { + return {node, closed_let_vars: null, assigned_vars: null} + } +} diff --git a/src/ast_utils.js b/src/ast_utils.js index 536a9a9..b9e0610 100644 --- a/src/ast_utils.js +++ b/src/ast_utils.js @@ -1,4 +1,4 @@ -import {uniq} from './utils.js' +import {uniq, map_find} from './utils.js' export const collect_destructuring_identifiers = node => { if(Array.isArray(node)) { @@ -76,6 +76,32 @@ export const find_leaf = (node, index) => { } } +// Finds node declaring identifier with given index +export const find_declaration = (node, index) => { + if(node.type == 'let' || node.type == 'const') { + return node + } + if(node.type == 'function_decl' && node.index == index) { + return node + } + if(node.type == 'function_args') { + return node + } + if(node.type == 'import') { + return node + } + if(node.children == null) { + return null + } + const child = node.children.find(node => is_index_within_node(node, index)) + if(child == null) { + return null + } + return find_declaration(child, index) +} + +export const is_index_within_node = (node, index) => + node.index <= index && node.index + node.length > index export const is_child = (child, parent) => { return parent.index <= child.index && diff --git a/src/eval.js b/src/eval.js index 789a2f3..a242d91 100644 --- a/src/eval.js +++ b/src/eval.js @@ -7,7 +7,8 @@ import { import { find_fn_by_location, - find_node, + find_declaration, + find_leaf, collect_destructuring_identifiers, map_destructuring_identifiers, map_tree, @@ -17,7 +18,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} from './runtime.js' +import {run, do_eval_expand_calltree_node, Multiversion} from './runtime.js' // TODO: fix error messages. For example, "__fn is not a function" @@ -72,8 +73,14 @@ const codegen_function_expr = (node, node_cxt) => { ? `(${args}) => ` : `function(${args})` - // TODO gensym __obj, __fn, __call_id - const prolog = '{const __call_id = __cxt.call_counter;' + // TODO gensym __obj, __fn, __call_id, __let_vars + const prolog = + '{const __call_id = __cxt.call_counter;' + + ( + node.has_versioned_let_vars + ? 'const __let_vars = __cxt.let_vars;' + : '' + ) const call = (node.is_async ? 'async ' : '') + decl + ( (node.body.type == 'do') @@ -96,7 +103,7 @@ const codegen_function_expr = (node, node_cxt) => { const get_closure = `() => ({${[...node.closed].join(',')}})` return `__trace(__cxt, ${call}, "${node.name}", ${argscount}, ${location}, \ -${get_closure})` +${get_closure}, ${node.has_versioned_let_vars})` } /* @@ -142,12 +149,28 @@ ${JSON.stringify(errormessage)})` // marker export const get_after_if_path = node => node.index + 1 -const codegen = (node, node_cxt, parent) => { +const codegen = (node, node_cxt) => { - const do_codegen = (n, parent) => codegen(n, node_cxt, parent) + const do_codegen = n => codegen(n, node_cxt) - if([ - 'identifier', + if(node.type == 'identifier') { + + if(node.definition == 'self' || node.definition == 'global') { + return node.value + } + + if(node.definition.index == null) { + throw new Error('illegal state') + } + if( + !node_cxt.literal_identifiers && + find_leaf(node_cxt.toplevel, node.definition.index).is_versioned_let_var + ) { + return node.value + '.get()' + } + + return node.value + } else if([ 'number', 'string_literal', 'builtin_identifier', @@ -190,7 +213,8 @@ const codegen = (node, node_cxt, parent) => { if(el.type == 'object_spread'){ return do_codegen(el) } else if(el.type == 'identifier') { - return el.value + const value = do_codegen(el) + return el.value + ': ' + value } else if(el.type == 'key_value_pair') { return '[' + do_codegen(el.key.type == 'computed_property' ? el.key.expr : el.key) + ']' + ': (' + do_codegen(el.value, el) + ')' @@ -223,10 +247,53 @@ const codegen = (node, node_cxt, parent) => { : node.type + ' ' return prefix + node .children - .map(c => c.type == 'identifier' - ? c.value - : do_codegen(c.children[0]) + ' = ' + do_codegen(c.children[1]) - ) + .map(c => { + if(node.type == 'let') { + let lefthand, righthand + if(c.type == 'identifier') { + // let decl without assignment, like 'let x' + lefthand = c + if(!lefthand.is_versioned_let_var) { + return lefthand.value + } + } else if(c.type == 'decl_pair') { + lefthand = c.children[0], righthand = c.children[1] + if(lefthand.type != 'identifier') { + // TODO + // See comment for 'simple_decl_pair' in 'src/parse_js.js' + // Currently it is unreachable + throw new Error('illegal state') + } + } else { + throw new Error('illegal state') + } + if(lefthand.is_versioned_let_var) { + const name = lefthand.value + const symbol = symbol_for_let_var(lefthand) + return name + ( + righthand == null + ? ` = __let_vars['${symbol}'] = new __Multiversion(__cxt)` + : ` = __let_vars['${symbol}'] = new __Multiversion(__cxt, ${do_codegen(righthand)})` + ) + } // Otherwise goes to the end of the func + } + + if(node.type == 'assignment') { + const [lefthand, righthand] = c.children + if(lefthand.type != 'identifier') { + // TODO + // See comment for 'simple_decl_pair' in 'src/parse_js.js' + // Currently it is unreachable + throw new Error('TODO: illegal state') + } + const let_var = find_leaf(node_cxt.toplevel, lefthand.definition.index) + if(let_var.is_versioned_let_var){ + return lefthand.value + '.set(' + do_codegen(righthand, node) + ')' + } + } + + return do_codegen(c.children[0]) + ' = ' + do_codegen(c.children[1]) + }) .join(',') + ';' + node.children.map(decl => { @@ -338,7 +405,8 @@ export const eval_modules = ( io_trace, location ) => { - // TODO gensym __cxt, __trace, __trace_call, __calltree_node_by_loc, __do_await + // TODO gensym __cxt, __trace, __trace_call, __calltree_node_by_loc, + // __do_await, __Multiversion // TODO bug if module imported twice, once as external and as regular @@ -348,28 +416,37 @@ export const eval_modules = ( ? globalThis.app_window.eval('(async function(){})').constructor : globalThis.app_window.Function - const module_fns = parse_result.sorted.map(module => ( - { + const module_fns = parse_result.sorted.map(module => { + const code = codegen( + parse_result.modules[module], + { + module, + toplevel: parse_result.modules[module], + } + ) + return { module, // TODO refactor, instead of multiple args prefixed with '__', pass // single arg called `runtime` fn: new Function( '__cxt', + '__let_vars', '__calltree_node_by_loc', '__trace', '__trace_call', '__do_await', '__save_ct_node_for_path', + '__Multiversion', /* 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}) + 'const __call_id = __cxt.call_counter;' + + code ) } - )) + }) const cxt = { modules: external_imports == null @@ -482,7 +559,10 @@ const get_args_scope = (fn_node, args, closure) => { collect_destructuring_identifiers(fn_node.function_args) .map(i => i.value) - const destructuring = fn_node.function_args.children.map(n => codegen(n)).join(',') + const destructuring = fn_node + .function_args + .children.map(n => codegen(n, {literal_identifiers: true})) + .join(',') /* // TODO gensym __args. Or @@ -516,22 +596,72 @@ const get_args_scope = (fn_node, args, closure) => { } const eval_binary_expr = (node, scope, callsleft, context) => { - const {ok, children, calls} = eval_children(node, scope, callsleft, context) + const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context) if(!ok) { - return {ok, children, calls} + return {ok, children, calls, scope: nextscope} } const op = node.operator const a = children[0].result.value const b = children[1].result.value const value = (new Function('a', 'b', ' return a ' + op + ' b'))(a, b) - return {ok, children, calls, value} + return {ok, children, calls, value, scope: nextscope} } +const is_symbol_for_let_var = symbol => + symbol.startsWith('!') +const symbol_for_let_var = let_var_node => + '!' + let_var_node.value + '_' + let_var_node.index + +const symbol_for_closed_let_var = name => + '!' + name + '_' + 'closed' + +/* + For versioned let vars, any function call within expr can mutate let vars, so + it can change current scope and outer scopes. Since current scope and outer + scope can have let var with the same name, we need to address them with + different names. Prepend '!' symbol because it is not a valid js identifier, + so it will not be mixed with other variables +*/ +const symbol_for_identifier = (node, context) => { + if(node.definition == 'global') { + return node.value + } + + const index = node.definition == 'self' ? node.index : node.definition.index + const declaration = find_declaration(context.calltree_node.code, index) + + const variable = node.definition == 'self' + ? node + : find_leaf(context.calltree_node.code, node.definition.index) + if(declaration == null) { + /* + Variable was declared in outer scope. Since there can be only one variable + with given name in outer scope, generate a name for it + */ + return symbol_for_closed_let_var(node.value) + } + if(declaration.type == 'let'){ + return symbol_for_let_var(variable) + } else { + return node.value + } +} + const do_eval_frame_expr = (node, scope, callsleft, context) => { - if([ - 'identifier', + if(node.type == 'identifier') { + if(node.definition == 'global') { + return {...eval_codestring(node.value, scope), calls: callsleft, scope} + } else { + return { + ok: true, + value: scope[symbol_for_identifier(node, context)], + calls: callsleft, + scope + } + } + } else if([ 'builtin_identifier', 'number', 'string_literal', @@ -539,7 +669,7 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { ].includes(node.type)){ // TODO exprs inside backtick string // Pass scope for backtick string - return {...eval_codestring(node.value, scope), calls: callsleft} + return {...eval_codestring(node.value, scope), calls: callsleft, scope} } else if(node.type == 'array_spread') { const result = eval_children(node, scope, callsleft, context) if(!result.ok) { @@ -553,6 +683,7 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { ok: false, children: result.children, calls: result.calls, + scope: result.scope, error: new TypeError(child.string + ' is not iterable'), is_error_origin: true, } @@ -564,9 +695,9 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { ].includes(node.type)) { return eval_children(node, scope, callsleft, context) } else if(node.type == 'array_literal' || node.type == 'call_args'){ - const {ok, children, calls} = eval_children(node, scope, callsleft, context) + const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context) if(!ok) { - return {ok, children, calls} + return {ok, children, calls, scope: nextscope} } const value = children.reduce( (arr, el) => { @@ -578,11 +709,11 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { }, [], ) - return {ok, children, calls, value} + return {ok, children, calls, value, scope: nextscope} } else if(node.type == 'object_literal'){ - const {ok, children, calls} = eval_children(node, scope, callsleft, context) + const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context) if(!ok) { - return {ok, children, calls} + return {ok, children, calls, scope: nextscope} } const value = children.reduce( (value, el) => { @@ -609,11 +740,11 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { }, {} ) - return {ok, children, value, calls} + return {ok, children, value, calls, scope: nextscope} } else if(node.type == 'function_call' || node.type == 'new'){ - const {ok, children, calls} = eval_children(node, scope, callsleft, context) + const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context) if(!ok) { - return {ok: false, children, calls} + return {ok: false, children, calls, scope: nextscope} } else { if(typeof(children[0].result.value) != 'function') { return { @@ -622,12 +753,43 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { is_error_origin: true, children, calls, + scope: nextscope, } } - const c = calls[0] + const [c, ...next_calls] = calls if(c == null) { throw new Error('illegal state') } + + const closure = context.calltree_node.fn?.__closure + const closure_let_vars = closure == null + ? null + : Object.fromEntries( + Object.entries(closure) + .filter(([k,value]) => value instanceof Multiversion) + .map(([k,value]) => [symbol_for_closed_let_var(k), value]) + ) + + const let_vars = { + ...context.calltree_node.let_vars, + ...closure_let_vars, + } + const changed_vars = filter_object(let_vars, (name, v) => + v.last_version_number() >= c.id + ) + + const next_id = next_calls.length == 0 + ? context.calltree_node.next_id + : next_calls[0].id + + const updated_let_scope = map_object(changed_vars, (name, v) => + /* + We can't just use c.next_id here because it will break in async + context + */ + v.get_version(next_id) + ) + return { ok: c.ok, call: c, @@ -635,7 +797,8 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { error: c.error, is_error_origin: !c.ok, children, - calls: calls.slice(1) + calls: next_calls, + scope: {...nextscope, ...updated_let_scope}, } } } else if(node.type == 'function_expr'){ @@ -650,10 +813,11 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { ok: true, value: fn_placeholder, calls: callsleft, + scope, children: node.children, } } else if(node.type == 'ternary') { - const {node: cond_evaled, calls: calls_after_cond} = eval_frame_expr( + const {node: cond_evaled, calls: calls_after_cond, scope: scope_after_cond} = eval_frame_expr( node.cond, scope, callsleft, @@ -666,11 +830,13 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { ok: false, children: [cond_evaled, branches[0], branches[1]], calls: calls_after_cond, + scope: scope_after_cond, } } else { - const {node: branch_evaled, calls: calls_after_branch} = eval_frame_expr( + const {node: branch_evaled, calls: calls_after_branch, scope: scope_after_branch} + = eval_frame_expr( branches[value ? 0 : 1], - scope, + scope_after_cond, calls_after_cond, context ) @@ -679,15 +845,16 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { : [cond_evaled, branches[0], branch_evaled] const ok = branch_evaled.result.ok if(ok) { - return {ok, children, calls: calls_after_branch, value: branch_evaled.result.value} + return {ok, children, calls: calls_after_branch, scope: scope_after_branch, + value: branch_evaled.result.value} } else { - return {ok, children, calls: calls_after_branch} + return {ok, children, calls: calls_after_branch, scope: scope_after_branch} } } } else if(node.type == 'member_access'){ - const {ok, children, calls} = eval_children(node, scope, callsleft, context) + const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context) if(!ok) { - return {ok: false, children, calls} + return {ok: false, children, calls, scope: nextscope} } const [obj, prop] = children @@ -702,12 +869,13 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { }), children, calls, + scope: nextscope } } else if(node.type == 'unary') { - const {ok, children, calls} = eval_children(node, scope, callsleft, context) + const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context) if(!ok) { - return {ok: false, children, calls} + return {ok: false, children, calls, scope: nextscope} } else { const expr = children[0] let ok, value, error, is_error_origin @@ -739,14 +907,14 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { } else { throw new Error('unknown op') } - return {ok, children, calls, value, error, is_error_origin} + return {ok, children, calls, value, error, is_error_origin, scope: nextscope} } } else if(node.type == 'binary' && !['&&', '||', '??'].includes(node.operator)){ return eval_binary_expr(node, scope, callsleft, context) } else if(node.type == 'binary' && ['&&', '||', '??'].includes(node.operator)){ - const {node: left_evaled, calls} = eval_frame_expr( + const {node: left_evaled, calls, scope: nextscope} = eval_frame_expr( node.children[0], scope, callsleft, @@ -768,17 +936,18 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { value, children: [left_evaled, node.children[1]], calls, + scope: nextscope, } } else { return eval_binary_expr(node, scope, callsleft, context) } } else if(node.type == 'grouping'){ - const {ok, children, calls} = eval_children(node, scope, callsleft, context) + const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context) if(!ok) { - return {ok, children, calls} + return {ok, children, calls, scope: nextscope} } else { - return {ok: true, children, calls, value: children[0].result.value} + return {ok: true, children, calls, scope: nextscope, value: children[0].result.value} } } else { console.error(node) @@ -788,26 +957,33 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { const eval_children = (node, scope, calls, context) => { return node.children.reduce( - ({ok, children, calls}, child) => { - let next_child, next_ok, next_calls + ({ok, children, calls, scope}, child) => { + let next_child, next_ok, next_calls, next_scope if(!ok) { next_child = child next_ok = false next_calls = calls + next_scope = scope } else { const result = eval_frame_expr(child, scope, calls, context) next_child = result.node next_calls = result.calls next_ok = next_child.result.ok + next_scope = result.scope + } + return { + ok: next_ok, + children: [...children, next_child], + calls: next_calls, + scope: next_scope, } - return {ok: next_ok, children: [...children, next_child], calls: next_calls} }, - {ok: true, children: [], calls} + {ok: true, children: [], calls, scope} ) } const eval_frame_expr = (node, scope, callsleft, context) => { - const {ok, error, is_error_origin, value, call, children, calls} + const {ok, error, is_error_origin, value, call, children, calls, scope: nextscope} = do_eval_frame_expr(node, scope, callsleft, context) if(callsleft != null && calls == null) { // TODO remove it, just for debug @@ -821,35 +997,18 @@ const eval_frame_expr = (node, scope, callsleft, context) => { // Add `call` for step_into result: {ok, error, value, call, is_error_origin} }, + scope: nextscope, calls, } } -const apply_assignments = (node, assignments) => { - const let_ids = node - .children - .filter(c => c.type == 'let') - .map(l => l.children) - .flat() - .map(c => c.index) - - const parent_assignments = Object.entries(assignments).filter(([index, v]) => - let_ids.find(i => i.toString() == index) == null - ) - // Scope we return to parent block - const scope = Object.fromEntries( - parent_assignments.map(([k, {name, value}]) => [name, value]) - ) - return {node, scope, assignments: Object.fromEntries(parent_assignments)} -} - -const eval_decl_pair = (s, scope, calls, context, is_assignment) => { +const eval_decl_pair = (s, scope, calls, context) => { if(s.type != 'decl_pair') { throw new Error('illegal state') } // TODO default values for destructuring can be function calls - const {node, calls: next_calls} + const {node, calls: next_calls, scope: scope_after_expr} = eval_frame_expr(s.expr, scope, calls, context) const s_expr_evaled = {...s, children: [s.name_node, node]} if(!node.result.ok) { @@ -858,23 +1017,28 @@ const eval_decl_pair = (s, scope, calls, context, is_assignment) => { node: {...s_expr_evaled, result: {ok: false}}, scope, calls: next_calls, + scope: scope_after_expr, } } const name_nodes = collect_destructuring_identifiers(s.name_node) const names = name_nodes.map(n => n.value) - const destructuring = codegen(s.name_node) + const destructuring = codegen(s.name_node, {literal_identifiers: true}) // TODO unique name for __value (gensym) const codestring = ` const ${destructuring} = __value; ({${names.join(',')}}); ` - const {ok, value: next_scope, error} = eval_codestring( + const {ok, value: values, error} = eval_codestring( codestring, - {...scope, __value: node.result.value} + {...scope_after_expr, __value: node.result.value} ) + const next_scope = Object.fromEntries(name_nodes.map(n => + [symbol_for_identifier(n, context), values[n.value]] + )) + // TODO fine-grained destructuring error, only for identifiers that failed // destructuring const name_node_with_result = map_tree( @@ -884,7 +1048,7 @@ const eval_decl_pair = (s, scope, calls, context, is_assignment) => { result: { ok, error: ok ? null : error, - value: !ok ? null : next_scope[node.value], + value: !ok ? null : next_scope[symbol_for_identifier(node, context)], } }) ), @@ -903,7 +1067,7 @@ const eval_decl_pair = (s, scope, calls, context, is_assignment) => { 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, + scope: scope_after_expr, calls, } } @@ -911,19 +1075,8 @@ const eval_decl_pair = (s, scope, calls, context, is_assignment) => { return { ok: true, node: {...s_evaled, result: {ok: true}}, - scope: {...scope, ...next_scope}, + scope: {...scope_after_expr, ...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 } } @@ -954,17 +1107,16 @@ const eval_statement = (s, scope, calls, context) => { const initial_scope = {...scope, ...hoisted_functions_scope} - const {ok, assignments, returned, children, calls: next_calls} = s.children.reduce( - ({ok, returned, children, scope, calls, assignments}, s) => { + const {ok, returned, children, calls: next_calls, scope: next_scope} = + s.children.reduce( ({ok, returned, children, scope, calls}, s) => { if(returned || !ok) { - return {ok, returned, scope, calls, children: [...children, s], assignments} + return {ok, returned, scope, calls, children: [...children, s]} } else if(s.type == 'function_decl') { const node = function_decls.find(decl => decl.index == s.index) return { ok: true, returned: false, node, - assignments, scope, calls, children: [...children, node], @@ -974,39 +1126,37 @@ const eval_statement = (s, scope, calls, context) => { ok, returned, node, - assignments: next_assignments, scope: nextscope, calls: next_calls, } = eval_statement(s, scope, calls, context) return { ok, returned, - assignments: {...assignments, ...next_assignments}, scope: nextscope, calls: next_calls, children: [...children, node], } } }, - {ok: true, returned: false, children: [], scope: initial_scope, calls, assignments: {}} + {ok: true, returned: false, children: [], scope: initial_scope, calls} + ) + const let_vars_scope = filter_object(next_scope, (k, v) => + is_symbol_for_let_var(k) ) - const {node: next_node, scope: next_scope, assignments: parent_assignments} = - apply_assignments({...s, children: children, result: {ok}}, assignments) return { ok, - node: next_node, - scope: {...scope, ...next_scope}, + node: {...s, children: children, result: {ok}}, + scope: {...scope, ...let_vars_scope}, returned, - parent_assignments, calls: next_calls, } } else if(['let', 'const', 'assignment'].includes(s.type)) { const stmt = s - const initial = {ok: true, children: [], scope, calls, assignments: null} + const initial = {ok: true, children: [], scope, calls} - const {ok, children, calls: next_calls, scope: next_scope, assignments} = s.children.reduce( - ({ok, children, scope, calls, assignments}, s) => { + const {ok, children, calls: next_calls, scope: next_scope} = s.children.reduce( + ({ok, children, scope, calls}, s) => { if(!ok) { return {ok, scope, calls, children: [...children, s]} } @@ -1015,7 +1165,7 @@ const eval_statement = (s, scope, calls, context) => { return { ok, children: [...children, node], - scope: {...scope, [s.value]: undefined}, + scope: {...scope, [symbol_for_identifier(s, context)]: undefined}, calls } } @@ -1024,16 +1174,12 @@ const eval_statement = (s, scope, calls, context) => { node, scope: nextscope, calls: next_calls, - assignments: next_assignments, - } = eval_decl_pair(s, scope, calls, context, stmt.type == 'assignment') + } = eval_decl_pair(s, scope, calls, context) return { ok: next_ok, scope: nextscope, calls: next_calls, children: [...children, node], - assignments: stmt.type == 'assignment' - ? {...assignments, ...next_assignments} - : null } }, initial @@ -1043,18 +1189,17 @@ const eval_statement = (s, scope, calls, context) => { node: {...s, children, result: {ok}}, scope: {...scope, ...next_scope}, calls: next_calls, - assignments, } } else if(s.type == 'return') { - const {node, calls: next_calls} = + const {node, calls: next_calls, scope: nextscope} = eval_frame_expr(s.expr, scope, calls, context) return { ok: node.result.ok, returned: node.result.ok, node: {...s, children: [node], result: {ok: node.result.ok}}, - scope, + scope: nextscope, calls: next_calls, } @@ -1090,7 +1235,8 @@ const eval_statement = (s, scope, calls, context) => { } } else if(s.type == 'if') { - const {node, calls: next_calls} = eval_frame_expr(s.cond, scope, calls, context) + const {node, calls: next_calls, scope: scope_after_cond} = + eval_frame_expr(s.cond, scope, calls, context) if(!node.result.ok) { return { @@ -1098,6 +1244,7 @@ const eval_statement = (s, scope, calls, context) => { node: {...s, children: [node, ...s.branches], result: {ok: false}}, scope, calls: next_calls, + scope: scope_after_cond, } } @@ -1108,19 +1255,17 @@ const eval_statement = (s, scope, calls, context) => { const { node: evaled_branch, returned, - assignments, scope: next_scope, calls: next_calls2, } = eval_statement( s.branches[0], - scope, + scope_after_cond, next_calls, context ) return { ok: evaled_branch.result.ok, returned, - assignments, node: {...s, children: [node, evaled_branch], result: {ok: evaled_branch.result.ok} @@ -1144,12 +1289,11 @@ const eval_statement = (s, scope, calls, context) => { const { node: evaled_branch, returned, - assignments, scope: next_scope, calls: next_calls2 } = eval_statement( active_branch, - scope, + scope_after_cond, next_calls, context, ) @@ -1161,7 +1305,6 @@ const eval_statement = (s, scope, calls, context) => { return { ok: evaled_branch.result.ok, returned, - assignments, node: {...s, children, result: {ok: evaled_branch.result.ok}}, scope: next_scope, calls: next_calls2, @@ -1170,7 +1313,8 @@ const eval_statement = (s, scope, calls, context) => { } else if(s.type == 'throw') { - const {node, calls: next_calls} = eval_frame_expr(s.expr, scope, calls, context) + const {node, calls: next_calls, scope: next_scope} = + eval_frame_expr(s.expr, scope, calls, context) return { ok: false, @@ -1182,17 +1326,17 @@ const eval_statement = (s, scope, calls, context) => { error: node.result.ok ? node.result.value : null, } }, - scope, + scope: next_scope, calls: next_calls, } } else { // stmt type is expression - const {node, calls: next_calls} = eval_frame_expr(s, scope, calls, context) + const {node, calls: next_calls, scope: next_scope} = eval_frame_expr(s, scope, calls, context) return { ok: node.result.ok, node, - scope, + scope: next_scope, calls: next_calls, } } @@ -1205,6 +1349,7 @@ export const eval_frame = (calltree_node, modules) => { const node = calltree_node.code const context = {calltree_node, modules} if(node.type == 'do') { + // eval module toplevel return eval_statement( node, {}, @@ -1214,10 +1359,15 @@ export const eval_frame = (calltree_node, modules) => { } else { // TODO default values for destructuring can be function calls + const closure = map_object(calltree_node.fn.__closure, (_key, value) => { + return value instanceof Multiversion + ? value.get_version(calltree_node.id) + : value + }) const args_scope_result = get_args_scope( node, calltree_node.args, - calltree_node.fn.__closure + closure ) // TODO fine-grained destructuring error, only for identifiers that @@ -1254,8 +1404,16 @@ export const eval_frame = (calltree_node, modules) => { } } - const scope = {...calltree_node.fn.__closure, ...args_scope_result.value} + const closure_scope = Object.fromEntries( + Object.entries(closure).map(([key, value]) => { + return [ + symbol_for_closed_let_var(key), + value, + ] + }) + ) + const scope = {...closure_scope, ...args_scope_result.value} let nextbody diff --git a/src/find_definitions.js b/src/find_definitions.js index b6e66cb..cb5b4a4 100644 --- a/src/find_definitions.js +++ b/src/find_definitions.js @@ -82,7 +82,6 @@ const add_trivial_definition = node => { export const find_definitions = (ast, globals, scope = {}, closure_scope = {}, module_name) => { - // sanity check if(!(globals instanceof Set)) { throw new Error('not a set') diff --git a/src/parse_js.js b/src/parse_js.js index f9b6285..f476876 100644 --- a/src/parse_js.js +++ b/src/parse_js.js @@ -7,6 +7,8 @@ import { analyze, } from './find_definitions.js' +import { find_versioned_let_vars } from './analyze_versioned_let_vars.js' + import {reserved} from './reserved.js' import {collect_imports} from './ast_utils.js' @@ -1104,13 +1106,53 @@ const decl_pair = if_ok( } ) +/* +Like decl_pair, but lefthand can be only an identifier. + +The reason for having this is that currently we don't compile correctly code +like this: + + let {x} = ... + +If we have just + + let x = ... + +Then we compile it into + + const x = new Multiversion(cxt, ...) + +For 'let {x} = ...' we should compile it to something like + + const {x} = ... + const __x_multiversion = x; + +And then inside eval.js access x value only through __x_multiversion + +See branch 'let_vars_destructuring' + +Same for assignment +*/ +const simple_decl_pair = if_ok( + seq([identifier, literal('='), expr]), + ({value, ...node}) => { + const [lefthand, _eq, expr] = value + return { + ...node, + type: 'decl_pair', + not_evaluatable: true, + children: [lefthand, expr], + } + } +) + const const_or_let = is_const => if_ok( seq_select(1, [ literal(is_const ? 'const' : 'let'), comma_separated_1( is_const ? decl_pair - : either(decl_pair, identifier) + : either(simple_decl_pair, identifier) ) ]), ({value, ...node}) => ({ @@ -1125,8 +1167,9 @@ const const_statement = const_or_let(true) const let_declaration = const_or_let(false) // TODO object assignment required braces, like ({foo} = {foo: 1}) +// TODO +=, *= etc const assignment = if_ok( - comma_separated_1(decl_pair), + comma_separated_1(simple_decl_pair), ({value, ...node}) => ({ ...node, type: 'assignment', @@ -1644,7 +1687,9 @@ export const parse = (str, globals, is_module = false, module_name) => { // property to some nodes, and children no more equal to other properties // of nodes by idenitity, which somehow breaks code (i dont remember how // exactly). Refactor it? - const fixed_node = update_children(deduce_fn_names(populate_string(node))) + const fixed_node = update_children( + find_versioned_let_vars(deduce_fn_names(populate_string(node))).node + ) const problems = analyze(fixed_node) if(problems.length != 0) { return {ok: false, problems} diff --git a/src/runtime.js b/src/runtime.js index 7f49b0a..b8e8f0b 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -79,17 +79,20 @@ const do_run = function*(module_fns, cxt, io_trace){ toplevel: true, module, id: ++cxt.call_counter, + let_vars: {}, } try { cxt.modules[module] = {} const result = fn( cxt, + calltree.let_vars, calltree_node_by_loc.get(module), __trace, __trace_call, __do_await, __save_ct_node_for_path, + Multiversion, ) if(result instanceof cxt.window.Promise) { yield cxt.window.Promise.race([replay_aborted_promise, result]) @@ -102,6 +105,7 @@ const do_run = function*(module_fns, cxt, io_trace){ calltree.error = error } calltree.children = cxt.children + calltree.next_id = cxt.call_counter + 1 if(!calltree.ok) { break } @@ -184,6 +188,19 @@ export const set_record_call = cxt => { export const do_eval_expand_calltree_node = (cxt, node) => { cxt.is_recording_deferred_calls = false + cxt.is_expanding_calltree_node = true + cxt.touched_multiversions = new Set() + + // Save call counter and set it to the value it had when executed 'fn' for + // the first time + const call_counter = cxt.call_counter + cxt.call_counter = node.fn.__location == null + // Function is native, set call_counter to node.id + ? node.id + // call_counter will be incremented inside __trace and produce the same id + // as node.id + : node.id - 1 + cxt.children = null try { if(node.is_new) { @@ -195,6 +212,21 @@ export const do_eval_expand_calltree_node = (cxt, node) => { // do nothing. Exception was caught and recorded inside '__trace' } + // Restore call counter + cxt.call_counter = call_counter + + // Recover multiversions affected by expand_calltree_node + for(let m of cxt.touched_multiversions) { + if(m.is_expanding_calltree_node) { + delete m.is_expanding_calltree_node + } + if(m.latest_copy != null) { + m.latest = m.latest_copy.value + } + } + delete cxt.touched_multiversions + + cxt.is_expanding_calltree_node = false cxt.is_recording_deferred_calls = true const children = cxt.children cxt.children = null @@ -243,7 +275,7 @@ const __do_await = async (cxt, value) => { } } -const __trace = (cxt, fn, name, argscount, __location, get_closure) => { +const __trace = (cxt, fn, name, argscount, __location, get_closure, has_versioned_let_vars) => { const result = (...args) => { if(result.__closure == null) { result.__closure = get_closure() @@ -268,6 +300,11 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure) => { } } + let let_vars + if(has_versioned_let_vars) { + let_vars = cxt.let_vars = {} + } + let ok, value, error const is_toplevel_call_copy = cxt.is_toplevel_call @@ -296,6 +333,8 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure) => { const call = { id: call_id, + next_id: cxt.call_counter + 1, + let_vars, ok, value, error, @@ -358,6 +397,8 @@ const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => { cxt.children = null cxt.stack.push(false) + const call_id = ++cxt.call_counter + // TODO: other console fns const is_log = fn == cxt.window.console.log || fn == cxt.window.console.error @@ -392,7 +433,8 @@ const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => { cxt.prev_children = cxt.children const call = { - id: ++cxt.call_counter, + id: call_id, + next_id: cxt.call_counter + 1, ok, value, error, @@ -431,9 +473,107 @@ const __save_ct_node_for_path = (cxt, __calltree_node_by_loc, index, __call_id) 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) } } + +// https://stackoverflow.com/a/29018745 +function binarySearch(arr, el, compare_fn) { + let m = 0; + let n = arr.length - 1; + while (m <= n) { + let k = (n + m) >> 1; + let cmp = compare_fn(el, arr[k]); + if (cmp > 0) { + m = k + 1; + } else if(cmp < 0) { + n = k - 1; + } else { + return k; + } + } + return ~m; +} + +// 'let' variable recording the history of its values +export class Multiversion { + constructor(cxt, initial) { + this.cxt = cxt + this.is_expanding_calltree_node = cxt.is_expanding_calltree_node + this.latest = initial + this.versions = [{call_id: cxt.call_counter, value: initial}] + } + + get() { + const call_id = this.cxt.call_counter + + if(!this.cxt.is_expanding_calltree_node) { + return this.latest + } else { + if(this.is_expanding_calltree_node) { + // var was created during current expansion, use its latest value + return this.latest + } else { + if(this.latest_copy != null) { + // value was set during expand_calltree_node, use this value + return this.latest + } + // TODO on first read, set latest and latest_copy? + return this.get_version(call_id) + } + } + } + + get_version(call_id) { + const idx = binarySearch(this.versions, call_id, (id, el) => id - el.call_id) + if(idx == 0) { + // This branch is unreachable. get_version will be never called for a + // call_id where let variable was declared. + throw new Error('illegal state') + } else if(idx > 0) { + return this.versions[idx - 1].value + } else if(idx == -1) { + throw new Error('illegal state') + } else { + return this.versions[-idx - 2].value + } + } + + set(value) { + const call_id = this.cxt.call_counter + if(this.cxt.is_expanding_calltree_node) { + if(this.is_expanding_calltree_node) { + this.latest = value + this.set_version(call_id, value) + this.cxt.touched_multiversions.add(this) + } else { + if(this.latest_copy == null) { + this.latest_copy = {value: this.latest} + } + this.cxt.touched_multiversions.add(this) + this.latest = value + } + } else { + this.latest = value + this.set_version(call_id, value) + } + } + + last_version_number() { + return this.versions.at(-1).call_id + } + + set_version(call_id, value) { + const last_version = this.versions.at(-1) + if(last_version.call_id > call_id) { + throw new Error('illegal state') + } + if(last_version.call_id == call_id) { + last_version.value = value + return + } + this.versions.push({call_id, value}) + } +} diff --git a/test/self_hosted_test.js b/test/self_hosted_test.js index c8f05e1..919d2a3 100644 --- a/test/self_hosted_test.js +++ b/test/self_hosted_test.js @@ -74,6 +74,11 @@ const i = test_initial_state( {entrypoint: 'test/run.js'}, ) +if(!i.parse_result.ok) { + console.error('Parse errors:', i.parse_result.problems) + throw new Error('parse error') +} + assert_equal(i.loading_external_imports_state != null, true) const external_imports = await load_external_modules(i) const loaded = COMMANDS.external_imports_loaded(i, i, external_imports) diff --git a/test/test.js b/test/test.js index fb8a8ae..9c1200b 100644 --- a/test/test.js +++ b/test/test.js @@ -105,6 +105,14 @@ export const tests = [ ) }), + // TODO + // test('backtick_string let vars', () => { + // assert_code_evals_to( + // 'let x = `b`; `a${x}a`', + // 'aba', + // ) + // }), + test('Simple expression', () => { return assert_code_evals_to('1+1;', 2) }), @@ -320,6 +328,7 @@ export const tests = [ ) }), + /* test('let variable', () => { const code = ` let x, y = 2, unused, [z,q] = [3,4] @@ -328,6 +337,7 @@ export const tests = [ const i = test_initial_state(code, code.indexOf('x')) assert_equal(i.value_explorer.result.value, {y: 2, z: 3, q: 4}) }), + */ test('let variable not initialized bug', () => { const code = ` @@ -352,11 +362,6 @@ export const tests = [ }; x ` - const parse_result = do_parse(code) - const assignment = find_leaf( - parse_result.node, - code.indexOf('x = 0') - ) assert_code_evals_to( code, 1 @@ -1476,6 +1481,18 @@ export const tests = [ }), test('multiple assignments', () => { + assert_code_evals_to( + ` + let x, y + x = 1, y = 2 + {x,y} + `, + {x: 1, y: 2} + ) + }), + + /* TODO assignments destructuring + test('multiple assignments destructuring', () => { assert_code_evals_to( ` let x, y @@ -1485,6 +1502,7 @@ export const tests = [ {x: 1, y: 2} ) }), + */ test('assigments value explorer', () => { const code = ` @@ -1504,7 +1522,8 @@ export const tests = [ assert_equal(i.value_explorer.result.value, {x: 1, y: 2}) }), - test('assigments destructuring value explorer', () => { + /* TODO + test('assignments destructuring value explorer', () => { const code = ` let x, y x = 1, {y} = {y:2} @@ -1512,6 +1531,7 @@ export const tests = [ 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 = ` @@ -1522,6 +1542,19 @@ export const tests = [ assert_equal(i.value_explorer.result.ok, false) }), + test('block scoping const', () => { + assert_code_evals_to( + ` + const x = 0 + if(true) { + const x = 1 + } + x + `, + 0 + ) + }), + test('block scoping', () => { assert_code_evals_to( ` @@ -4079,4 +4112,570 @@ const y = x()` 'Map {foo: "bar", baz: "qux"}' ) }), + + test('let_versions find_versioned_lets toplevel', () => { + const result = do_parse(` + let x + x = 1 + function foo() { + x + } + `) + assert_equal(result.node.has_versioned_let_vars, true) + }), + + test('let_versions find_versioned_lets', () => { + function assert_is_versioned_let(code, is_versioned) { + const result = do_parse(code) + const root = find_node(result.node, + n => n.name == 'root' && n.type == 'function_expr' + ) + assert_equal(root.has_versioned_let_vars, is_versioned) + const node = find_node(result.node, n => n.index == code.indexOf('x')) + assert_equal(!(!node.is_versioned_let_var), is_versioned) + } + + assert_is_versioned_let( + ` + function root() { + let x + x = 1 + function foo() { + x + } + } + `, + true + ) + + // closed but constant + assert_is_versioned_let( + ` + function root() { + let x + function foo() { + x + } + } + `, + false + ) + + // assigned but not closed + assert_is_versioned_let( + ` + function root() { + let x + x = 1 + } + `, + false + ) + + // not closed, var has the same name + assert_is_versioned_let( + ` + function root() { + let x + x = 1 + function foo() { + let x + x + } + } + `, + false + ) + + // not closed, var has the same name + assert_is_versioned_let( + ` + function root() { + let x + x = 1 + if(true) { + let x + function foo() { + x + } + } + } + `, + false + ) + }), + + test('let_versions', () => { + const code = ` + let x + [1,2].forEach(y => { + x /*x*/ + x = y + }) + ` + const x_pos = code.indexOf('x /*x*/') + const i = test_initial_state(code, x_pos) + const second_iter = COMMANDS.calltree.arrow_down(i) + const select_x = COMMANDS.move_cursor(second_iter, x_pos) + assert_equal(select_x.value_explorer.result.value, 1) + }), + + test('let_versions close let var bug', () => { + const code = ` + let x + x = 1 + function y() { + return {x} + } + y() /*y()*/ + ` + const i = test_initial_state(code, code.indexOf('y() /*y()*/')) + assert_equal(i.value_explorer.result.value, {x: 1}) + }), + + test('let_versions initial let value', () => { + const code = ` + let x + function y() { + x /*x*/ + } + y() + ` + const x_pos = code.indexOf('x /*x*/') + const i = test_initial_state(code, x_pos) + assert_equal(i.value_explorer.result, {ok: true, value: undefined}) + }), + + test('let_versions save version bug', () => { + const code = ` + let x = 0 + + function set_x(value) { + x = value + } + + function get_x() { + x /* result */ + } + + get_x() + + set_x(10) + x = 10 + set_x(10) + x = 10 + ` + const i = test_initial_state(code, code.indexOf('x /* result */')) + assert_equal(i.value_explorer.result.value, 0) + }), + + test('let_versions expand_calltree_node', () => { + const code = ` + let y + + function foo(x) { + y /*y*/ + bar(y) + } + + function bar(arg) { + } + + foo(0) + y = 11 + foo(0) + y = 12 + ` + const i = test_initial_state(code) + const second_foo_call = root_calltree_node(i).children[1] + assert_equal(second_foo_call.has_more_children, true) + const expanded = COMMANDS.calltree.click(i, second_foo_call.id) + const bar_call = root_calltree_node(expanded).children[1].children[0] + assert_equal(bar_call.fn.name, 'bar') + assert_equal(bar_call.args, [11]) + const moved = COMMANDS.move_cursor(expanded, code.indexOf('y /*y*/')) + assert_equal(moved.value_explorer.result.value, 11) + }), + + test('let_versions expand_calltree_node 2', () => { + const code = ` + let y + + function deep(x) { + if(x < 10) { + y /*y*/ + y = x + deep(x + 1) + } + } + + deep(0) + y = 11 + deep(0) + y = 12 + ` + const i = test_initial_state(code) + const second_deep_call = root_calltree_node(i).children[1] + assert_equal(second_deep_call.has_more_children, true) + const expanded = COMMANDS.calltree.click(i, second_deep_call.id) + const moved = COMMANDS.move_cursor(expanded, code.indexOf('y /*y*/')) + assert_equal(moved.value_explorer.result.value, 11) + }), + + test('let_versions create multiversion within expand_calltree_node', () => { + const code = ` + function x() { + let y + function set(value) { + y = value + } + set(1) + y /*result*/ + set(2) + } + + x() + x() + + ` + const i = test_initial_state(code) + const second_x_call = root_calltree_node(i).children[1] + assert_equal(second_x_call.has_more_children, true) + const expanded = COMMANDS.calltree.click(i, second_x_call.id) + const moved = COMMANDS.move_cursor(expanded, code.indexOf('y /*result*/')) + assert_equal(moved.value_explorer.result.value, 1) + }), + + test('let_versions mutable closure', () => { + const code = ` + const holder = (function() { + let value + return { + get: () => value, + set: (v) => { + value /*value*/ + value = v + } + } + })() + Array.from({length: 10}).map((_, i) => { + holder.set(i) + }) + holder.get() + ` + const i = test_initial_state(code, code.indexOf('holder.get')) + assert_equal(i.value_explorer.result.value, 9) + + const map_expanded = COMMANDS.calltree.click( + i, + root_calltree_node(i).children[2].id + ) + const expanded = COMMANDS.calltree.click( + map_expanded, + root_calltree_node(map_expanded).children[2].children[5].id + ) + const set_call = COMMANDS.calltree.arrow_right( + COMMANDS.calltree.arrow_right( + expanded + ) + ) + assert_equal( + set_call.active_calltree_node.code.index, + code.indexOf('(v) =>') + ) + const moved = COMMANDS.move_cursor(set_call, code.indexOf('value /*value*/')) + assert_equal(moved.value_explorer.result.value, 4) + }), + + test('let_versions forEach', () => { + const code = ` + let sum = 0 + [1,2,3].forEach(v => { + sum = sum + v + }) + sum /*first*/ + [1,2,3].forEach(v => { + sum = sum + v + }) + sum /*second*/ + ` + const i = test_initial_state(code, code.indexOf('sum /*first*/')) + assert_equal(i.value_explorer.result.value, 6) + const second = COMMANDS.move_cursor(i, code.indexOf('sum /*second*/')) + assert_equal(second.value_explorer.result.value, 12) + }), + + test('let_versions scope', () => { + assert_code_evals_to(` + let x = 1 + let y = 1 + function change_x() { + x = 2 + } + function change_y() { + y = 2 + } + function unused() { + return {} + } + if(false) { + } else { + if((change_y() || true) ? true : null) { + const a = [...[{...{ + y: unused()[!(1 + (true ? {y: [change_x()]} : null))] + }}]] + } + } + {x,y} /*result*/ + `, + {x: 2, y: 2} + ) + }), + + test('let_versions expr', () => { + assert_code_evals_to(` + let x = 0 + function inc() { + x = x + 1 + return 0 + } + x + inc() + x + inc() + x + `, + 3 + ) + }), + + test('let_versions update in assignment', () => { + assert_code_evals_to(` + let x + function set(value) { + x = 1 + return 0 + } + x = set() + x + `, + 0 + ) + }), + + test('let_versions update in assignment closed', () => { + const code = ` + function test() { + let x + function set(value) { + x = 1 + return 0 + } + x = set() + return x + } + test() + ` + const i = test_initial_state(code, code.indexOf('return x')) + assert_equal(i.value_explorer.result.value, 0) + }), + + test('let_versions multiple vars with same name', () => { + const code = ` + let x + function x_1() { + x = 1 + } + if(true) { + let x = 0 + function x_2() { + x = 2 + } + x /* result 0 */ + x_1() + x /* result 1 */ + x_2() + x /* result 2 */ + } + ` + const i = test_initial_state(code, code.indexOf('x /* result 0 */')) + const frame = active_frame(i) + const result_0 = find_node(frame, n => n.index == code.indexOf('x /* result 0 */')).result + assert_equal(result_0.value, 0) + const result_1 = find_node(frame, n => n.index == code.indexOf('x /* result 1 */')).result + assert_equal(result_1.value, 0) + const result_2 = find_node(frame, n => n.index == code.indexOf('x /* result 2 */')).result + assert_equal(result_2.value, 2) + }), + + test('let_versions closed let vars bug', () => { + const code = ` + let x = 0 + function inc() { + x = x + 1 + } + function test() { + inc() + x /*x*/ + } + test() + ` + const i = test_initial_state(code, code.indexOf('x /*x*/')) + assert_equal(i.value_explorer.result.value, 1) + }), + + test('let_versions assign and read variable multiple times within call', () => { + const code = ` + let x; + (() => { + x = 1 + console.log(x) + x = 2 + console.log(x) + })() + ` + }), + + test('let_versions let assigned undefined bug', () => { + const code = ` + let x = 1 + function set(value) { + x = value + } + set(2) + set(undefined) + x /*x*/ + ` + const i = test_initial_state(code, code.indexOf('x /*x*/')) + assert_equal(i.value_explorer.result.value, undefined) + }), + + // TODO function args should have multiple versions same as let vars + + // test('let_versions function args closure', () => { + // const code = ` + // (function(x) { + // function y() { + // x /*x*/ + // } + // y() + // x = 1 + // y() + // })(0) + // ` + // const i = test_initial_state(code) + // const second_y_call = root_calltree_node(i).children[0].children[1] + // const selected = COMMANDS.calltree.click(i, second_y_call.id) + // const moved = COMMANDS.move_cursor(selected, code.indexOf('x /*x*/')) + // assert_equal(moved.value_explorer.result.value, 1) + // }), + + test('let_versions async/await', async () => { + const code = ` + let x + function set(value) { + x = value + } + await set(1) + x /*x*/ + ` + const i = await test_initial_state_async(code, code.indexOf('x /*x*/')) + assert_equal(i.value_explorer.result.value, 1) + }), + + test('let_versions async/await 2', async () => { + const code = ` + let x + function set(value) { + x = value + Promise.resolve().then(() => { + x = 10 + }) + } + await set(1) + x /*x*/ + ` + const i = await test_initial_state_async(code, code.indexOf('x /*x*/')) + assert_equal(i.value_explorer.result.value, 10) + }), + + // Test that expand_calltree_node produces correct id for expanded nodes + test('let_versions native call', () => { + const code = ` + function x() {} + [1,2].map(x) + [1,2].map(x) + ` + const i = test_initial_state(code) + const second_map_call = i.calltree.children[0].children[1] + assert_equal(second_map_call.has_more_children, true) + const expanded = COMMANDS.calltree.click(i, second_map_call.id) + const second_map_call_exp = expanded.calltree.children[0].children[1] + assert_equal(second_map_call.id == second_map_call_exp.id, true) + assert_equal(second_map_call_exp.children[0].id == second_map_call_exp.id + 1, true) + }), + + test('let_versions expand twice', () => { + const code = ` + function test() { + let x = 0 + function test2() { + function foo() { + x /*x*/ + } + x = x + 1 + foo() + } + test2() + } + test() + test() + ` + const i = test_initial_state(code) + const test_call = root_calltree_node(i).children[1] + assert_equal(test_call.has_more_children , true) + + const expanded = COMMANDS.calltree.click(i, test_call.id) + const test2_call = root_calltree_node(expanded).children[1].children[0] + assert_equal(test2_call.has_more_children, true) + + const expanded2 = COMMANDS.calltree.click(expanded, test2_call.id) + const foo_call = root_calltree_node(expanded2).children[1].children[0].children[0] + + const expanded3 = COMMANDS.calltree.click(expanded2, foo_call.id) + + const moved = COMMANDS.move_cursor(expanded3, code.indexOf('x /*x*/')) + assert_equal(moved.value_explorer.result.value, 1) + }), + + test('let_versions deferred calls', () => { + const code = ` + let x = 0 + export const inc = () => { + return do_inc() + } + const do_inc = () => { + x = x + 1 + return x + } + inc() + ` + + const {state: i, on_deferred_call} = test_deferred_calls_state(code) + + // Make deferred call + i.modules[''].inc() + + const state = on_deferred_call(i) + const call = get_deferred_calls(state)[0] + assert_equal(call.has_more_children, true) + assert_equal(call.value, 2) + + // Expand call + // first arrow rights selects do_inc call, second steps into it + const expanded = COMMANDS.calltree.arrow_right( + COMMANDS.calltree.arrow_right( + COMMANDS.calltree.click(state, call.id) + ) + ) + // Move cursor + const moved = COMMANDS.move_cursor(expanded, code.indexOf('return x')) + assert_equal(moved.value_explorer.result.value, 2) + }), ] diff --git a/test/utils.js b/test/utils.js index 4833950..b19ac86 100644 --- a/test/utils.js +++ b/test/utils.js @@ -107,6 +107,9 @@ export const assert_code_error_async = async (codestring, error) => { } export const test_initial_state = (code, cursor_pos, other, entrypoint_settings) => { + if(cursor_pos < 0) { + throw new Error('illegal cursor_pos') + } return COMMANDS.open_app_window( COMMANDS.get_initial_state( { @@ -124,8 +127,8 @@ export const test_initial_state = (code, cursor_pos, other, entrypoint_settings) ) } -export const test_initial_state_async = async code => { - const s = test_initial_state(code) +export const test_initial_state_async = async (code, ...args) => { + const s = test_initial_state(code, ...args) assert_equal(s.eval_modules_state != null, true) const result = await s.eval_modules_state.promise return COMMANDS.eval_modules_finished(