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 ## 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: Variables are declared using the `const` or 'let' declaration. The use of `var` is not supported.
```
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).
Loops of any kind are not supported. Instead, consider using recursion or array functions as alternatives. 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 => { export const collect_destructuring_identifiers = node => {
if(Array.isArray(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) => { export const is_child = (child, parent) => {
return parent.index <= child.index && return parent.index <= child.index &&

View File

@@ -7,7 +7,8 @@ import {
import { import {
find_fn_by_location, find_fn_by_location,
find_node, find_declaration,
find_leaf,
collect_destructuring_identifiers, collect_destructuring_identifiers,
map_destructuring_identifiers, map_destructuring_identifiers,
map_tree, map_tree,
@@ -17,7 +18,7 @@ import {has_toplevel_await} from './find_definitions.js'
// import runtime as external because it has non-functional code // import runtime as external because it has non-functional code
// external // 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" // TODO: fix error messages. For example, "__fn is not a function"
@@ -72,8 +73,14 @@ const codegen_function_expr = (node, node_cxt) => {
? `(${args}) => ` ? `(${args}) => `
: `function(${args})` : `function(${args})`
// TODO gensym __obj, __fn, __call_id // TODO gensym __obj, __fn, __call_id, __let_vars
const prolog = '{const __call_id = __cxt.call_counter;' 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 + ( const call = (node.is_async ? 'async ' : '') + decl + (
(node.body.type == 'do') (node.body.type == 'do')
@@ -96,7 +103,7 @@ const codegen_function_expr = (node, node_cxt) => {
const get_closure = `() => ({${[...node.closed].join(',')}})` const get_closure = `() => ({${[...node.closed].join(',')}})`
return `__trace(__cxt, ${call}, "${node.name}", ${argscount}, ${location}, \ 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 // marker
export const get_after_if_path = node => node.index + 1 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([ if(node.type == 'identifier') {
'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', 'number',
'string_literal', 'string_literal',
'builtin_identifier', 'builtin_identifier',
@@ -190,7 +213,8 @@ const codegen = (node, node_cxt, parent) => {
if(el.type == 'object_spread'){ if(el.type == 'object_spread'){
return do_codegen(el) return do_codegen(el)
} else if(el.type == 'identifier') { } else if(el.type == 'identifier') {
return el.value const value = do_codegen(el)
return el.value + ': ' + value
} else if(el.type == 'key_value_pair') { } else if(el.type == 'key_value_pair') {
return '[' + do_codegen(el.key.type == 'computed_property' ? el.key.expr : el.key) + ']' return '[' + do_codegen(el.key.type == 'computed_property' ? el.key.expr : el.key) + ']'
+ ': (' + do_codegen(el.value, el) + ')' + ': (' + do_codegen(el.value, el) + ')'
@@ -223,10 +247,53 @@ const codegen = (node, node_cxt, parent) => {
: node.type + ' ' : node.type + ' '
return prefix + node return prefix + node
.children .children
.map(c => c.type == 'identifier' .map(c => {
? c.value if(node.type == 'let') {
: do_codegen(c.children[0]) + ' = ' + do_codegen(c.children[1]) 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(',') .join(',')
+ ';' + ';'
+ node.children.map(decl => { + node.children.map(decl => {
@@ -338,7 +405,8 @@ export const eval_modules = (
io_trace, io_trace,
location 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 // 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.eval('(async function(){})').constructor
: globalThis.app_window.Function : 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, module,
// TODO refactor, instead of multiple args prefixed with '__', pass // TODO refactor, instead of multiple args prefixed with '__', pass
// single arg called `runtime` // single arg called `runtime`
fn: new Function( fn: new Function(
'__cxt', '__cxt',
'__let_vars',
'__calltree_node_by_loc', '__calltree_node_by_loc',
'__trace', '__trace',
'__trace_call', '__trace_call',
'__do_await', '__do_await',
'__save_ct_node_for_path', '__save_ct_node_for_path',
'__Multiversion',
/* Add dummy __call_id for toplevel. It does not make any sence /* Add dummy __call_id for toplevel. It does not make any sence
* (toplevel is executed only once unlike function), we only add it * (toplevel is executed only once unlike function), we only add it
* because we dont want to codegen differently for if statements in * because we dont want to codegen differently for if statements in
* toplevel and if statements within functions*/ * toplevel and if statements within functions*/
'const __call_id = "SOMETHING_WRONG_HAPPENED";' + 'const __call_id = __cxt.call_counter;' +
codegen(parse_result.modules[module], {module}) code
) )
} }
)) })
const cxt = { const cxt = {
modules: external_imports == null modules: external_imports == null
@@ -482,7 +559,10 @@ const get_args_scope = (fn_node, args, closure) => {
collect_destructuring_identifiers(fn_node.function_args) collect_destructuring_identifiers(fn_node.function_args)
.map(i => i.value) .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 // 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 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) { if(!ok) {
return {ok, children, calls} return {ok, children, calls, scope: nextscope}
} }
const op = node.operator const op = node.operator
const a = children[0].result.value const a = children[0].result.value
const b = children[1].result.value const b = children[1].result.value
const value = (new Function('a', 'b', ' return a ' + op + ' b'))(a, b) 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) => { const do_eval_frame_expr = (node, scope, callsleft, context) => {
if([ if(node.type == 'identifier') {
'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', 'builtin_identifier',
'number', 'number',
'string_literal', 'string_literal',
@@ -539,7 +669,7 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
].includes(node.type)){ ].includes(node.type)){
// TODO exprs inside backtick string // TODO exprs inside backtick string
// Pass scope for 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') { } else if(node.type == 'array_spread') {
const result = eval_children(node, scope, callsleft, context) const result = eval_children(node, scope, callsleft, context)
if(!result.ok) { if(!result.ok) {
@@ -553,6 +683,7 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
ok: false, ok: false,
children: result.children, children: result.children,
calls: result.calls, calls: result.calls,
scope: result.scope,
error: new TypeError(child.string + ' is not iterable'), error: new TypeError(child.string + ' is not iterable'),
is_error_origin: true, is_error_origin: true,
} }
@@ -564,9 +695,9 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
].includes(node.type)) { ].includes(node.type)) {
return eval_children(node, scope, callsleft, context) return eval_children(node, scope, callsleft, context)
} else if(node.type == 'array_literal' || node.type == 'call_args'){ } 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) { if(!ok) {
return {ok, children, calls} return {ok, children, calls, scope: nextscope}
} }
const value = children.reduce( const value = children.reduce(
(arr, el) => { (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'){ } 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) { if(!ok) {
return {ok, children, calls} return {ok, children, calls, scope: nextscope}
} }
const value = children.reduce( const value = children.reduce(
(value, el) => { (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'){ } 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) { if(!ok) {
return {ok: false, children, calls} return {ok: false, children, calls, scope: nextscope}
} else { } else {
if(typeof(children[0].result.value) != 'function') { if(typeof(children[0].result.value) != 'function') {
return { return {
@@ -622,12 +753,43 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
is_error_origin: true, is_error_origin: true,
children, children,
calls, calls,
scope: nextscope,
} }
} }
const c = calls[0] const [c, ...next_calls] = calls
if(c == null) { if(c == null) {
throw new Error('illegal state') 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 { return {
ok: c.ok, ok: c.ok,
call: c, call: c,
@@ -635,7 +797,8 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
error: c.error, error: c.error,
is_error_origin: !c.ok, is_error_origin: !c.ok,
children, children,
calls: calls.slice(1) calls: next_calls,
scope: {...nextscope, ...updated_let_scope},
} }
} }
} else if(node.type == 'function_expr'){ } else if(node.type == 'function_expr'){
@@ -650,10 +813,11 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
ok: true, ok: true,
value: fn_placeholder, value: fn_placeholder,
calls: callsleft, calls: callsleft,
scope,
children: node.children, children: node.children,
} }
} else if(node.type == 'ternary') { } 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, node.cond,
scope, scope,
callsleft, callsleft,
@@ -666,11 +830,13 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
ok: false, ok: false,
children: [cond_evaled, branches[0], branches[1]], children: [cond_evaled, branches[0], branches[1]],
calls: calls_after_cond, calls: calls_after_cond,
scope: scope_after_cond,
} }
} else { } 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], branches[value ? 0 : 1],
scope, scope_after_cond,
calls_after_cond, calls_after_cond,
context context
) )
@@ -679,15 +845,16 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
: [cond_evaled, branches[0], branch_evaled] : [cond_evaled, branches[0], branch_evaled]
const ok = branch_evaled.result.ok const ok = branch_evaled.result.ok
if(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 { } 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'){ } 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) { if(!ok) {
return {ok: false, children, calls} return {ok: false, children, calls, scope: nextscope}
} }
const [obj, prop] = children const [obj, prop] = children
@@ -702,12 +869,13 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
}), }),
children, children,
calls, calls,
scope: nextscope
} }
} else if(node.type == 'unary') { } 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) { if(!ok) {
return {ok: false, children, calls} return {ok: false, children, calls, scope: nextscope}
} else { } else {
const expr = children[0] const expr = children[0]
let ok, value, error, is_error_origin let ok, value, error, is_error_origin
@@ -739,14 +907,14 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
} else { } else {
throw new Error('unknown op') 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)){ } else if(node.type == 'binary' && !['&&', '||', '??'].includes(node.operator)){
return eval_binary_expr(node, scope, callsleft, context) return eval_binary_expr(node, scope, callsleft, context)
} else if(node.type == 'binary' && ['&&', '||', '??'].includes(node.operator)){ } 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], node.children[0],
scope, scope,
callsleft, callsleft,
@@ -768,17 +936,18 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
value, value,
children: [left_evaled, node.children[1]], children: [left_evaled, node.children[1]],
calls, calls,
scope: nextscope,
} }
} else { } else {
return eval_binary_expr(node, scope, callsleft, context) return eval_binary_expr(node, scope, callsleft, context)
} }
} else if(node.type == 'grouping'){ } 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) { if(!ok) {
return {ok, children, calls} return {ok, children, calls, scope: nextscope}
} else { } else {
return {ok: true, children, calls, value: children[0].result.value} return {ok: true, children, calls, scope: nextscope, value: children[0].result.value}
} }
} else { } else {
console.error(node) console.error(node)
@@ -788,26 +957,33 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
const eval_children = (node, scope, calls, context) => { const eval_children = (node, scope, calls, context) => {
return node.children.reduce( return node.children.reduce(
({ok, children, calls}, child) => { ({ok, children, calls, scope}, child) => {
let next_child, next_ok, next_calls let next_child, next_ok, next_calls, next_scope
if(!ok) { if(!ok) {
next_child = child next_child = child
next_ok = false next_ok = false
next_calls = calls next_calls = calls
next_scope = scope
} else { } else {
const result = eval_frame_expr(child, scope, calls, context) const result = eval_frame_expr(child, scope, calls, context)
next_child = result.node next_child = result.node
next_calls = result.calls next_calls = result.calls
next_ok = next_child.result.ok 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 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) = do_eval_frame_expr(node, scope, callsleft, context)
if(callsleft != null && calls == null) { if(callsleft != null && calls == null) {
// TODO remove it, just for debug // TODO remove it, just for debug
@@ -821,35 +997,18 @@ const eval_frame_expr = (node, scope, callsleft, context) => {
// Add `call` for step_into // Add `call` for step_into
result: {ok, error, value, call, is_error_origin} result: {ok, error, value, call, is_error_origin}
}, },
scope: nextscope,
calls, calls,
} }
} }
const apply_assignments = (node, assignments) => { const eval_decl_pair = (s, scope, calls, context) => {
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) => {
if(s.type != 'decl_pair') { if(s.type != 'decl_pair') {
throw new Error('illegal state') throw new Error('illegal state')
} }
// TODO default values for destructuring can be function calls // 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) = eval_frame_expr(s.expr, scope, calls, context)
const s_expr_evaled = {...s, children: [s.name_node, node]} const s_expr_evaled = {...s, children: [s.name_node, node]}
if(!node.result.ok) { 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}}, node: {...s_expr_evaled, result: {ok: false}},
scope, scope,
calls: next_calls, calls: next_calls,
scope: scope_after_expr,
} }
} }
const name_nodes = collect_destructuring_identifiers(s.name_node) const name_nodes = collect_destructuring_identifiers(s.name_node)
const names = name_nodes.map(n => n.value) 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) // TODO unique name for __value (gensym)
const codestring = ` const codestring = `
const ${destructuring} = __value; const ${destructuring} = __value;
({${names.join(',')}}); ({${names.join(',')}});
` `
const {ok, value: next_scope, error} = eval_codestring( const {ok, value: values, error} = eval_codestring(
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 // TODO fine-grained destructuring error, only for identifiers that failed
// destructuring // destructuring
const name_node_with_result = map_tree( const name_node_with_result = map_tree(
@@ -884,7 +1048,7 @@ const eval_decl_pair = (s, scope, calls, context, is_assignment) => {
result: { result: {
ok, ok,
error: ok ? null : error, 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, ok: false,
// TODO assign error to node where destructuring failed, not to every node // TODO assign error to node where destructuring failed, not to every node
node: {...s_evaled, result: {ok, error, is_error_origin: true}}, node: {...s_evaled, result: {ok, error, is_error_origin: true}},
scope, scope: scope_after_expr,
calls, calls,
} }
} }
@@ -911,19 +1075,8 @@ const eval_decl_pair = (s, scope, calls, context, is_assignment) => {
return { return {
ok: true, ok: true,
node: {...s_evaled, result: {ok: true}}, node: {...s_evaled, result: {ok: true}},
scope: {...scope, ...next_scope}, scope: {...scope_after_expr, ...next_scope},
calls: next_calls, 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 initial_scope = {...scope, ...hoisted_functions_scope}
const {ok, assignments, returned, children, calls: next_calls} = s.children.reduce( const {ok, returned, children, calls: next_calls, scope: next_scope} =
({ok, returned, children, scope, calls, assignments}, s) => { s.children.reduce( ({ok, returned, children, scope, calls}, s) => {
if(returned || !ok) { 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') { } else if(s.type == 'function_decl') {
const node = function_decls.find(decl => decl.index == s.index) const node = function_decls.find(decl => decl.index == s.index)
return { return {
ok: true, ok: true,
returned: false, returned: false,
node, node,
assignments,
scope, scope,
calls, calls,
children: [...children, node], children: [...children, node],
@@ -974,39 +1126,37 @@ const eval_statement = (s, scope, calls, context) => {
ok, ok,
returned, returned,
node, node,
assignments: next_assignments,
scope: nextscope, scope: nextscope,
calls: next_calls, calls: next_calls,
} = eval_statement(s, scope, calls, context) } = eval_statement(s, scope, calls, context)
return { return {
ok, ok,
returned, returned,
assignments: {...assignments, ...next_assignments},
scope: nextscope, scope: nextscope,
calls: next_calls, calls: next_calls,
children: [...children, node], 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 { return {
ok, ok,
node: next_node, node: {...s, children: children, result: {ok}},
scope: {...scope, ...next_scope}, scope: {...scope, ...let_vars_scope},
returned, returned,
parent_assignments,
calls: next_calls, calls: next_calls,
} }
} else if(['let', 'const', 'assignment'].includes(s.type)) { } else if(['let', 'const', 'assignment'].includes(s.type)) {
const stmt = s 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( const {ok, children, calls: next_calls, scope: next_scope} = s.children.reduce(
({ok, children, scope, calls, assignments}, s) => { ({ok, children, scope, calls}, s) => {
if(!ok) { if(!ok) {
return {ok, scope, calls, children: [...children, s]} return {ok, scope, calls, children: [...children, s]}
} }
@@ -1015,7 +1165,7 @@ const eval_statement = (s, scope, calls, context) => {
return { return {
ok, ok,
children: [...children, node], children: [...children, node],
scope: {...scope, [s.value]: undefined}, scope: {...scope, [symbol_for_identifier(s, context)]: undefined},
calls calls
} }
} }
@@ -1024,16 +1174,12 @@ const eval_statement = (s, scope, calls, context) => {
node, node,
scope: nextscope, scope: nextscope,
calls: next_calls, calls: next_calls,
assignments: next_assignments, } = eval_decl_pair(s, scope, calls, context)
} = eval_decl_pair(s, scope, calls, context, stmt.type == 'assignment')
return { return {
ok: next_ok, ok: next_ok,
scope: nextscope, scope: nextscope,
calls: next_calls, calls: next_calls,
children: [...children, node], children: [...children, node],
assignments: stmt.type == 'assignment'
? {...assignments, ...next_assignments}
: null
} }
}, },
initial initial
@@ -1043,18 +1189,17 @@ const eval_statement = (s, scope, calls, context) => {
node: {...s, children, result: {ok}}, node: {...s, children, result: {ok}},
scope: {...scope, ...next_scope}, scope: {...scope, ...next_scope},
calls: next_calls, calls: next_calls,
assignments,
} }
} else if(s.type == 'return') { } 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) eval_frame_expr(s.expr, scope, calls, context)
return { return {
ok: node.result.ok, ok: node.result.ok,
returned: node.result.ok, returned: node.result.ok,
node: {...s, children: [node], result: {ok: node.result.ok}}, node: {...s, children: [node], result: {ok: node.result.ok}},
scope, scope: nextscope,
calls: next_calls, calls: next_calls,
} }
@@ -1090,7 +1235,8 @@ const eval_statement = (s, scope, calls, context) => {
} }
} else if(s.type == 'if') { } 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) { if(!node.result.ok) {
return { return {
@@ -1098,6 +1244,7 @@ const eval_statement = (s, scope, calls, context) => {
node: {...s, children: [node, ...s.branches], result: {ok: false}}, node: {...s, children: [node, ...s.branches], result: {ok: false}},
scope, scope,
calls: next_calls, calls: next_calls,
scope: scope_after_cond,
} }
} }
@@ -1108,19 +1255,17 @@ const eval_statement = (s, scope, calls, context) => {
const { const {
node: evaled_branch, node: evaled_branch,
returned, returned,
assignments,
scope: next_scope, scope: next_scope,
calls: next_calls2, calls: next_calls2,
} = eval_statement( } = eval_statement(
s.branches[0], s.branches[0],
scope, scope_after_cond,
next_calls, next_calls,
context context
) )
return { return {
ok: evaled_branch.result.ok, ok: evaled_branch.result.ok,
returned, returned,
assignments,
node: {...s, node: {...s,
children: [node, evaled_branch], children: [node, evaled_branch],
result: {ok: evaled_branch.result.ok} result: {ok: evaled_branch.result.ok}
@@ -1144,12 +1289,11 @@ const eval_statement = (s, scope, calls, context) => {
const { const {
node: evaled_branch, node: evaled_branch,
returned, returned,
assignments,
scope: next_scope, scope: next_scope,
calls: next_calls2 calls: next_calls2
} = eval_statement( } = eval_statement(
active_branch, active_branch,
scope, scope_after_cond,
next_calls, next_calls,
context, context,
) )
@@ -1161,7 +1305,6 @@ const eval_statement = (s, scope, calls, context) => {
return { return {
ok: evaled_branch.result.ok, ok: evaled_branch.result.ok,
returned, returned,
assignments,
node: {...s, children, result: {ok: evaled_branch.result.ok}}, node: {...s, children, result: {ok: evaled_branch.result.ok}},
scope: next_scope, scope: next_scope,
calls: next_calls2, calls: next_calls2,
@@ -1170,7 +1313,8 @@ const eval_statement = (s, scope, calls, context) => {
} else if(s.type == 'throw') { } 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 { return {
ok: false, ok: false,
@@ -1182,17 +1326,17 @@ const eval_statement = (s, scope, calls, context) => {
error: node.result.ok ? node.result.value : null, error: node.result.ok ? node.result.value : null,
} }
}, },
scope, scope: next_scope,
calls: next_calls, calls: next_calls,
} }
} else { } else {
// stmt type is expression // 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 { return {
ok: node.result.ok, ok: node.result.ok,
node, node,
scope, scope: next_scope,
calls: next_calls, calls: next_calls,
} }
} }
@@ -1205,6 +1349,7 @@ export const eval_frame = (calltree_node, modules) => {
const node = calltree_node.code const node = calltree_node.code
const context = {calltree_node, modules} const context = {calltree_node, modules}
if(node.type == 'do') { if(node.type == 'do') {
// eval module toplevel
return eval_statement( return eval_statement(
node, node,
{}, {},
@@ -1214,10 +1359,15 @@ export const eval_frame = (calltree_node, modules) => {
} else { } else {
// TODO default values for destructuring can be function calls // 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( const args_scope_result = get_args_scope(
node, node,
calltree_node.args, calltree_node.args,
calltree_node.fn.__closure closure
) )
// TODO fine-grained destructuring error, only for identifiers that // 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 let nextbody

View File

@@ -82,7 +82,6 @@ const add_trivial_definition = node => {
export const find_definitions = (ast, globals, scope = {}, closure_scope = {}, module_name) => { export const find_definitions = (ast, globals, scope = {}, closure_scope = {}, module_name) => {
// sanity check // sanity check
if(!(globals instanceof Set)) { if(!(globals instanceof Set)) {
throw new Error('not a set') throw new Error('not a set')

View File

@@ -7,6 +7,8 @@ import {
analyze, analyze,
} from './find_definitions.js' } from './find_definitions.js'
import { find_versioned_let_vars } from './analyze_versioned_let_vars.js'
import {reserved} from './reserved.js' import {reserved} from './reserved.js'
import {collect_imports} from './ast_utils.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( const const_or_let = is_const => if_ok(
seq_select(1, [ seq_select(1, [
literal(is_const ? 'const' : 'let'), literal(is_const ? 'const' : 'let'),
comma_separated_1( comma_separated_1(
is_const is_const
? decl_pair ? decl_pair
: either(decl_pair, identifier) : either(simple_decl_pair, identifier)
) )
]), ]),
({value, ...node}) => ({ ({value, ...node}) => ({
@@ -1125,8 +1167,9 @@ const const_statement = const_or_let(true)
const let_declaration = const_or_let(false) const let_declaration = const_or_let(false)
// TODO object assignment required braces, like ({foo} = {foo: 1}) // TODO object assignment required braces, like ({foo} = {foo: 1})
// TODO +=, *= etc
const assignment = if_ok( const assignment = if_ok(
comma_separated_1(decl_pair), comma_separated_1(simple_decl_pair),
({value, ...node}) => ({ ({value, ...node}) => ({
...node, ...node,
type: 'assignment', 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 // property to some nodes, and children no more equal to other properties
// of nodes by idenitity, which somehow breaks code (i dont remember how // of nodes by idenitity, which somehow breaks code (i dont remember how
// exactly). Refactor it? // 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) const problems = analyze(fixed_node)
if(problems.length != 0) { if(problems.length != 0) {
return {ok: false, problems} return {ok: false, problems}

View File

@@ -79,17 +79,20 @@ const do_run = function*(module_fns, cxt, io_trace){
toplevel: true, toplevel: true,
module, module,
id: ++cxt.call_counter, id: ++cxt.call_counter,
let_vars: {},
} }
try { try {
cxt.modules[module] = {} cxt.modules[module] = {}
const result = fn( const result = fn(
cxt, cxt,
calltree.let_vars,
calltree_node_by_loc.get(module), calltree_node_by_loc.get(module),
__trace, __trace,
__trace_call, __trace_call,
__do_await, __do_await,
__save_ct_node_for_path, __save_ct_node_for_path,
Multiversion,
) )
if(result instanceof cxt.window.Promise) { if(result instanceof cxt.window.Promise) {
yield cxt.window.Promise.race([replay_aborted_promise, result]) yield cxt.window.Promise.race([replay_aborted_promise, result])
@@ -102,6 +105,7 @@ const do_run = function*(module_fns, cxt, io_trace){
calltree.error = error calltree.error = error
} }
calltree.children = cxt.children calltree.children = cxt.children
calltree.next_id = cxt.call_counter + 1
if(!calltree.ok) { if(!calltree.ok) {
break break
} }
@@ -184,6 +188,19 @@ export const set_record_call = cxt => {
export const do_eval_expand_calltree_node = (cxt, node) => { export const do_eval_expand_calltree_node = (cxt, node) => {
cxt.is_recording_deferred_calls = false 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 cxt.children = null
try { try {
if(node.is_new) { 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' // 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 cxt.is_recording_deferred_calls = true
const children = cxt.children const children = cxt.children
cxt.children = null 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) => { const result = (...args) => {
if(result.__closure == null) { if(result.__closure == null) {
result.__closure = get_closure() 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 let ok, value, error
const is_toplevel_call_copy = cxt.is_toplevel_call const is_toplevel_call_copy = cxt.is_toplevel_call
@@ -296,6 +333,8 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure) => {
const call = { const call = {
id: call_id, id: call_id,
next_id: cxt.call_counter + 1,
let_vars,
ok, ok,
value, value,
error, error,
@@ -358,6 +397,8 @@ const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => {
cxt.children = null cxt.children = null
cxt.stack.push(false) cxt.stack.push(false)
const call_id = ++cxt.call_counter
// TODO: other console fns // TODO: other console fns
const is_log = fn == cxt.window.console.log || fn == cxt.window.console.error 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 cxt.prev_children = cxt.children
const call = { const call = {
id: ++cxt.call_counter, id: call_id,
next_id: cxt.call_counter + 1,
ok, ok,
value, value,
error, 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) { if(cxt.skip_save_ct_node_for_path) {
return return
} }
if(__calltree_node_by_loc.get(index) == null) { if(__calltree_node_by_loc.get(index) == null) {
__calltree_node_by_loc.set(index, __call_id) __calltree_node_by_loc.set(index, __call_id)
set_record_call(cxt) 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'}, {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) assert_equal(i.loading_external_imports_state != null, true)
const external_imports = await load_external_modules(i) const external_imports = await load_external_modules(i)
const loaded = COMMANDS.external_imports_loaded(i, i, external_imports) 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', () => { test('Simple expression', () => {
return assert_code_evals_to('1+1;', 2) return assert_code_evals_to('1+1;', 2)
}), }),
@@ -320,6 +328,7 @@ export const tests = [
) )
}), }),
/*
test('let variable', () => { test('let variable', () => {
const code = ` const code = `
let x, y = 2, unused, [z,q] = [3,4] 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')) const i = test_initial_state(code, code.indexOf('x'))
assert_equal(i.value_explorer.result.value, {y: 2, z: 3, q: 4}) assert_equal(i.value_explorer.result.value, {y: 2, z: 3, q: 4})
}), }),
*/
test('let variable not initialized bug', () => { test('let variable not initialized bug', () => {
const code = ` const code = `
@@ -352,11 +362,6 @@ export const tests = [
}; };
x x
` `
const parse_result = do_parse(code)
const assignment = find_leaf(
parse_result.node,
code.indexOf('x = 0')
)
assert_code_evals_to( assert_code_evals_to(
code, code,
1 1
@@ -1476,6 +1481,18 @@ export const tests = [
}), }),
test('multiple assignments', () => { 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( assert_code_evals_to(
` `
let x, y let x, y
@@ -1485,6 +1502,7 @@ export const tests = [
{x: 1, y: 2} {x: 1, y: 2}
) )
}), }),
*/
test('assigments value explorer', () => { test('assigments value explorer', () => {
const code = ` const code = `
@@ -1504,7 +1522,8 @@ export const tests = [
assert_equal(i.value_explorer.result.value, {x: 1, y: 2}) assert_equal(i.value_explorer.result.value, {x: 1, y: 2})
}), }),
test('assigments destructuring value explorer', () => { /* TODO
test('assignments destructuring value explorer', () => {
const code = ` const code = `
let x, y let x, y
x = 1, {y} = {y:2} x = 1, {y} = {y:2}
@@ -1512,6 +1531,7 @@ export const tests = [
const i = test_initial_state(code, code.indexOf('x = 1')) const i = test_initial_state(code, code.indexOf('x = 1'))
assert_equal(i.value_explorer.result.value, {x: 1, y: 2}) assert_equal(i.value_explorer.result.value, {x: 1, y: 2})
}), }),
*/
test('assigments error', () => { test('assigments error', () => {
const code = ` const code = `
@@ -1522,6 +1542,19 @@ export const tests = [
assert_equal(i.value_explorer.result.ok, false) 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', () => { test('block scoping', () => {
assert_code_evals_to( assert_code_evals_to(
` `
@@ -4079,4 +4112,570 @@ const y = x()`
'Map {foo: "bar", baz: "qux"}' '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) => { 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( return COMMANDS.open_app_window(
COMMANDS.get_initial_state( 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 => { export const test_initial_state_async = async (code, ...args) => {
const s = test_initial_state(code) const s = test_initial_state(code, ...args)
assert_equal(s.eval_modules_state != null, true) assert_equal(s.eval_modules_state != null, true)
const result = await s.eval_modules_state.promise const result = await s.eval_modules_state.promise
return COMMANDS.eval_modules_finished( return COMMANDS.eval_modules_finished(