mutable closures (let variables)

This commit is contained in:
Dmitry Vasilev
2023-11-17 12:44:12 +08:00
parent 924e59f567
commit 4b32433748
10 changed files with 1272 additions and 157 deletions

View File

@@ -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.

View File

@@ -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}
}
}

View File

@@ -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 &&

View File

@@ -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

View File

@@ -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')

View File

@@ -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}

View File

@@ -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})
}
}

View File

@@ -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)

View File

@@ -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)
}),
]

View File

@@ -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(