mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 13:04:30 -08:00
mutable closures (let variables)
This commit is contained in:
12
README.md
12
README.md
@@ -33,17 +33,7 @@ Leporello.js source code is developed within Leporello.js itself
|
||||
|
||||
## Supported javascript subset
|
||||
|
||||
Variables are declared using the `const` declaration. The use of `var` is not supported. `let` variables can be declared without an initial assignment, which is useful in cases where the value depends on a condition. Here's an example:
|
||||
```
|
||||
let result
|
||||
if (n == 0 || n == 1) {
|
||||
result = n
|
||||
} else {
|
||||
result = fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
```
|
||||
|
||||
Currently, only a single declaration for a single `const` statement is supported (TODO).
|
||||
Variables are declared using the `const` or 'let' declaration. The use of `var` is not supported.
|
||||
|
||||
Loops of any kind are not supported. Instead, consider using recursion or array functions as alternatives.
|
||||
|
||||
|
||||
150
src/analyze_versioned_let_vars.js
Normal file
150
src/analyze_versioned_let_vars.js
Normal 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}
|
||||
}
|
||||
}
|
||||
@@ -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 &&
|
||||
|
||||
418
src/eval.js
418
src/eval.js
@@ -7,7 +7,8 @@ import {
|
||||
|
||||
import {
|
||||
find_fn_by_location,
|
||||
find_node,
|
||||
find_declaration,
|
||||
find_leaf,
|
||||
collect_destructuring_identifiers,
|
||||
map_destructuring_identifiers,
|
||||
map_tree,
|
||||
@@ -17,7 +18,7 @@ import {has_toplevel_await} from './find_definitions.js'
|
||||
|
||||
// import runtime as external because it has non-functional code
|
||||
// external
|
||||
import {run, do_eval_expand_calltree_node} from './runtime.js'
|
||||
import {run, do_eval_expand_calltree_node, Multiversion} from './runtime.js'
|
||||
|
||||
// TODO: fix error messages. For example, "__fn is not a function"
|
||||
|
||||
@@ -72,8 +73,14 @@ const codegen_function_expr = (node, node_cxt) => {
|
||||
? `(${args}) => `
|
||||
: `function(${args})`
|
||||
|
||||
// TODO gensym __obj, __fn, __call_id
|
||||
const prolog = '{const __call_id = __cxt.call_counter;'
|
||||
// TODO gensym __obj, __fn, __call_id, __let_vars
|
||||
const prolog =
|
||||
'{const __call_id = __cxt.call_counter;'
|
||||
+ (
|
||||
node.has_versioned_let_vars
|
||||
? 'const __let_vars = __cxt.let_vars;'
|
||||
: ''
|
||||
)
|
||||
|
||||
const call = (node.is_async ? 'async ' : '') + decl + (
|
||||
(node.body.type == 'do')
|
||||
@@ -96,7 +103,7 @@ const codegen_function_expr = (node, node_cxt) => {
|
||||
const get_closure = `() => ({${[...node.closed].join(',')}})`
|
||||
|
||||
return `__trace(__cxt, ${call}, "${node.name}", ${argscount}, ${location}, \
|
||||
${get_closure})`
|
||||
${get_closure}, ${node.has_versioned_let_vars})`
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -142,12 +149,28 @@ ${JSON.stringify(errormessage)})`
|
||||
// marker
|
||||
export const get_after_if_path = node => node.index + 1
|
||||
|
||||
const codegen = (node, node_cxt, parent) => {
|
||||
const codegen = (node, node_cxt) => {
|
||||
|
||||
const do_codegen = (n, parent) => codegen(n, node_cxt, parent)
|
||||
const do_codegen = n => codegen(n, node_cxt)
|
||||
|
||||
if([
|
||||
'identifier',
|
||||
if(node.type == 'identifier') {
|
||||
|
||||
if(node.definition == 'self' || node.definition == 'global') {
|
||||
return node.value
|
||||
}
|
||||
|
||||
if(node.definition.index == null) {
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
if(
|
||||
!node_cxt.literal_identifiers &&
|
||||
find_leaf(node_cxt.toplevel, node.definition.index).is_versioned_let_var
|
||||
) {
|
||||
return node.value + '.get()'
|
||||
}
|
||||
|
||||
return node.value
|
||||
} else if([
|
||||
'number',
|
||||
'string_literal',
|
||||
'builtin_identifier',
|
||||
@@ -190,7 +213,8 @@ const codegen = (node, node_cxt, parent) => {
|
||||
if(el.type == 'object_spread'){
|
||||
return do_codegen(el)
|
||||
} else if(el.type == 'identifier') {
|
||||
return el.value
|
||||
const value = do_codegen(el)
|
||||
return el.value + ': ' + value
|
||||
} else if(el.type == 'key_value_pair') {
|
||||
return '[' + do_codegen(el.key.type == 'computed_property' ? el.key.expr : el.key) + ']'
|
||||
+ ': (' + do_codegen(el.value, el) + ')'
|
||||
@@ -223,10 +247,53 @@ const codegen = (node, node_cxt, parent) => {
|
||||
: node.type + ' '
|
||||
return prefix + node
|
||||
.children
|
||||
.map(c => c.type == 'identifier'
|
||||
? c.value
|
||||
: do_codegen(c.children[0]) + ' = ' + do_codegen(c.children[1])
|
||||
)
|
||||
.map(c => {
|
||||
if(node.type == 'let') {
|
||||
let lefthand, righthand
|
||||
if(c.type == 'identifier') {
|
||||
// let decl without assignment, like 'let x'
|
||||
lefthand = c
|
||||
if(!lefthand.is_versioned_let_var) {
|
||||
return lefthand.value
|
||||
}
|
||||
} else if(c.type == 'decl_pair') {
|
||||
lefthand = c.children[0], righthand = c.children[1]
|
||||
if(lefthand.type != 'identifier') {
|
||||
// TODO
|
||||
// See comment for 'simple_decl_pair' in 'src/parse_js.js'
|
||||
// Currently it is unreachable
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
} else {
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
if(lefthand.is_versioned_let_var) {
|
||||
const name = lefthand.value
|
||||
const symbol = symbol_for_let_var(lefthand)
|
||||
return name + (
|
||||
righthand == null
|
||||
? ` = __let_vars['${symbol}'] = new __Multiversion(__cxt)`
|
||||
: ` = __let_vars['${symbol}'] = new __Multiversion(__cxt, ${do_codegen(righthand)})`
|
||||
)
|
||||
} // Otherwise goes to the end of the func
|
||||
}
|
||||
|
||||
if(node.type == 'assignment') {
|
||||
const [lefthand, righthand] = c.children
|
||||
if(lefthand.type != 'identifier') {
|
||||
// TODO
|
||||
// See comment for 'simple_decl_pair' in 'src/parse_js.js'
|
||||
// Currently it is unreachable
|
||||
throw new Error('TODO: illegal state')
|
||||
}
|
||||
const let_var = find_leaf(node_cxt.toplevel, lefthand.definition.index)
|
||||
if(let_var.is_versioned_let_var){
|
||||
return lefthand.value + '.set(' + do_codegen(righthand, node) + ')'
|
||||
}
|
||||
}
|
||||
|
||||
return do_codegen(c.children[0]) + ' = ' + do_codegen(c.children[1])
|
||||
})
|
||||
.join(',')
|
||||
+ ';'
|
||||
+ node.children.map(decl => {
|
||||
@@ -338,7 +405,8 @@ export const eval_modules = (
|
||||
io_trace,
|
||||
location
|
||||
) => {
|
||||
// TODO gensym __cxt, __trace, __trace_call, __calltree_node_by_loc, __do_await
|
||||
// TODO gensym __cxt, __trace, __trace_call, __calltree_node_by_loc,
|
||||
// __do_await, __Multiversion
|
||||
|
||||
// TODO bug if module imported twice, once as external and as regular
|
||||
|
||||
@@ -348,28 +416,37 @@ export const eval_modules = (
|
||||
? globalThis.app_window.eval('(async function(){})').constructor
|
||||
: globalThis.app_window.Function
|
||||
|
||||
const module_fns = parse_result.sorted.map(module => (
|
||||
{
|
||||
const module_fns = parse_result.sorted.map(module => {
|
||||
const code = codegen(
|
||||
parse_result.modules[module],
|
||||
{
|
||||
module,
|
||||
toplevel: parse_result.modules[module],
|
||||
}
|
||||
)
|
||||
return {
|
||||
module,
|
||||
// TODO refactor, instead of multiple args prefixed with '__', pass
|
||||
// single arg called `runtime`
|
||||
fn: new Function(
|
||||
'__cxt',
|
||||
'__let_vars',
|
||||
'__calltree_node_by_loc',
|
||||
'__trace',
|
||||
'__trace_call',
|
||||
'__do_await',
|
||||
'__save_ct_node_for_path',
|
||||
'__Multiversion',
|
||||
|
||||
/* Add dummy __call_id for toplevel. It does not make any sence
|
||||
* (toplevel is executed only once unlike function), we only add it
|
||||
* because we dont want to codegen differently for if statements in
|
||||
* toplevel and if statements within functions*/
|
||||
'const __call_id = "SOMETHING_WRONG_HAPPENED";' +
|
||||
codegen(parse_result.modules[module], {module})
|
||||
'const __call_id = __cxt.call_counter;' +
|
||||
code
|
||||
)
|
||||
}
|
||||
))
|
||||
})
|
||||
|
||||
const cxt = {
|
||||
modules: external_imports == null
|
||||
@@ -482,7 +559,10 @@ const get_args_scope = (fn_node, args, closure) => {
|
||||
collect_destructuring_identifiers(fn_node.function_args)
|
||||
.map(i => i.value)
|
||||
|
||||
const destructuring = fn_node.function_args.children.map(n => codegen(n)).join(',')
|
||||
const destructuring = fn_node
|
||||
.function_args
|
||||
.children.map(n => codegen(n, {literal_identifiers: true}))
|
||||
.join(',')
|
||||
|
||||
/*
|
||||
// TODO gensym __args. Or
|
||||
@@ -516,22 +596,72 @@ const get_args_scope = (fn_node, args, closure) => {
|
||||
}
|
||||
|
||||
const eval_binary_expr = (node, scope, callsleft, context) => {
|
||||
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
||||
const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context)
|
||||
if(!ok) {
|
||||
return {ok, children, calls}
|
||||
return {ok, children, calls, scope: nextscope}
|
||||
}
|
||||
|
||||
const op = node.operator
|
||||
const a = children[0].result.value
|
||||
const b = children[1].result.value
|
||||
const value = (new Function('a', 'b', ' return a ' + op + ' b'))(a, b)
|
||||
return {ok, children, calls, value}
|
||||
return {ok, children, calls, value, scope: nextscope}
|
||||
}
|
||||
|
||||
const is_symbol_for_let_var = symbol =>
|
||||
symbol.startsWith('!')
|
||||
|
||||
const symbol_for_let_var = let_var_node =>
|
||||
'!' + let_var_node.value + '_' + let_var_node.index
|
||||
|
||||
const symbol_for_closed_let_var = name =>
|
||||
'!' + name + '_' + 'closed'
|
||||
|
||||
/*
|
||||
For versioned let vars, any function call within expr can mutate let vars, so
|
||||
it can change current scope and outer scopes. Since current scope and outer
|
||||
scope can have let var with the same name, we need to address them with
|
||||
different names. Prepend '!' symbol because it is not a valid js identifier,
|
||||
so it will not be mixed with other variables
|
||||
*/
|
||||
const symbol_for_identifier = (node, context) => {
|
||||
if(node.definition == 'global') {
|
||||
return node.value
|
||||
}
|
||||
|
||||
const index = node.definition == 'self' ? node.index : node.definition.index
|
||||
const declaration = find_declaration(context.calltree_node.code, index)
|
||||
|
||||
const variable = node.definition == 'self'
|
||||
? node
|
||||
: find_leaf(context.calltree_node.code, node.definition.index)
|
||||
if(declaration == null) {
|
||||
/*
|
||||
Variable was declared in outer scope. Since there can be only one variable
|
||||
with given name in outer scope, generate a name for it
|
||||
*/
|
||||
return symbol_for_closed_let_var(node.value)
|
||||
}
|
||||
if(declaration.type == 'let'){
|
||||
return symbol_for_let_var(variable)
|
||||
} else {
|
||||
return node.value
|
||||
}
|
||||
}
|
||||
|
||||
const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
if([
|
||||
'identifier',
|
||||
if(node.type == 'identifier') {
|
||||
if(node.definition == 'global') {
|
||||
return {...eval_codestring(node.value, scope), calls: callsleft, scope}
|
||||
} else {
|
||||
return {
|
||||
ok: true,
|
||||
value: scope[symbol_for_identifier(node, context)],
|
||||
calls: callsleft,
|
||||
scope
|
||||
}
|
||||
}
|
||||
} else if([
|
||||
'builtin_identifier',
|
||||
'number',
|
||||
'string_literal',
|
||||
@@ -539,7 +669,7 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
].includes(node.type)){
|
||||
// TODO exprs inside backtick string
|
||||
// Pass scope for backtick string
|
||||
return {...eval_codestring(node.value, scope), calls: callsleft}
|
||||
return {...eval_codestring(node.value, scope), calls: callsleft, scope}
|
||||
} else if(node.type == 'array_spread') {
|
||||
const result = eval_children(node, scope, callsleft, context)
|
||||
if(!result.ok) {
|
||||
@@ -553,6 +683,7 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
ok: false,
|
||||
children: result.children,
|
||||
calls: result.calls,
|
||||
scope: result.scope,
|
||||
error: new TypeError(child.string + ' is not iterable'),
|
||||
is_error_origin: true,
|
||||
}
|
||||
@@ -564,9 +695,9 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
].includes(node.type)) {
|
||||
return eval_children(node, scope, callsleft, context)
|
||||
} else if(node.type == 'array_literal' || node.type == 'call_args'){
|
||||
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
||||
const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context)
|
||||
if(!ok) {
|
||||
return {ok, children, calls}
|
||||
return {ok, children, calls, scope: nextscope}
|
||||
}
|
||||
const value = children.reduce(
|
||||
(arr, el) => {
|
||||
@@ -578,11 +709,11 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
},
|
||||
[],
|
||||
)
|
||||
return {ok, children, calls, value}
|
||||
return {ok, children, calls, value, scope: nextscope}
|
||||
} else if(node.type == 'object_literal'){
|
||||
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
||||
const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context)
|
||||
if(!ok) {
|
||||
return {ok, children, calls}
|
||||
return {ok, children, calls, scope: nextscope}
|
||||
}
|
||||
const value = children.reduce(
|
||||
(value, el) => {
|
||||
@@ -609,11 +740,11 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
},
|
||||
{}
|
||||
)
|
||||
return {ok, children, value, calls}
|
||||
return {ok, children, value, calls, scope: nextscope}
|
||||
} else if(node.type == 'function_call' || node.type == 'new'){
|
||||
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
||||
const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context)
|
||||
if(!ok) {
|
||||
return {ok: false, children, calls}
|
||||
return {ok: false, children, calls, scope: nextscope}
|
||||
} else {
|
||||
if(typeof(children[0].result.value) != 'function') {
|
||||
return {
|
||||
@@ -622,12 +753,43 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
is_error_origin: true,
|
||||
children,
|
||||
calls,
|
||||
scope: nextscope,
|
||||
}
|
||||
}
|
||||
const c = calls[0]
|
||||
const [c, ...next_calls] = calls
|
||||
if(c == null) {
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
|
||||
const closure = context.calltree_node.fn?.__closure
|
||||
const closure_let_vars = closure == null
|
||||
? null
|
||||
: Object.fromEntries(
|
||||
Object.entries(closure)
|
||||
.filter(([k,value]) => value instanceof Multiversion)
|
||||
.map(([k,value]) => [symbol_for_closed_let_var(k), value])
|
||||
)
|
||||
|
||||
const let_vars = {
|
||||
...context.calltree_node.let_vars,
|
||||
...closure_let_vars,
|
||||
}
|
||||
const changed_vars = filter_object(let_vars, (name, v) =>
|
||||
v.last_version_number() >= c.id
|
||||
)
|
||||
|
||||
const next_id = next_calls.length == 0
|
||||
? context.calltree_node.next_id
|
||||
: next_calls[0].id
|
||||
|
||||
const updated_let_scope = map_object(changed_vars, (name, v) =>
|
||||
/*
|
||||
We can't just use c.next_id here because it will break in async
|
||||
context
|
||||
*/
|
||||
v.get_version(next_id)
|
||||
)
|
||||
|
||||
return {
|
||||
ok: c.ok,
|
||||
call: c,
|
||||
@@ -635,7 +797,8 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
error: c.error,
|
||||
is_error_origin: !c.ok,
|
||||
children,
|
||||
calls: calls.slice(1)
|
||||
calls: next_calls,
|
||||
scope: {...nextscope, ...updated_let_scope},
|
||||
}
|
||||
}
|
||||
} else if(node.type == 'function_expr'){
|
||||
@@ -650,10 +813,11 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
ok: true,
|
||||
value: fn_placeholder,
|
||||
calls: callsleft,
|
||||
scope,
|
||||
children: node.children,
|
||||
}
|
||||
} else if(node.type == 'ternary') {
|
||||
const {node: cond_evaled, calls: calls_after_cond} = eval_frame_expr(
|
||||
const {node: cond_evaled, calls: calls_after_cond, scope: scope_after_cond} = eval_frame_expr(
|
||||
node.cond,
|
||||
scope,
|
||||
callsleft,
|
||||
@@ -666,11 +830,13 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
ok: false,
|
||||
children: [cond_evaled, branches[0], branches[1]],
|
||||
calls: calls_after_cond,
|
||||
scope: scope_after_cond,
|
||||
}
|
||||
} else {
|
||||
const {node: branch_evaled, calls: calls_after_branch} = eval_frame_expr(
|
||||
const {node: branch_evaled, calls: calls_after_branch, scope: scope_after_branch}
|
||||
= eval_frame_expr(
|
||||
branches[value ? 0 : 1],
|
||||
scope,
|
||||
scope_after_cond,
|
||||
calls_after_cond,
|
||||
context
|
||||
)
|
||||
@@ -679,15 +845,16 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
: [cond_evaled, branches[0], branch_evaled]
|
||||
const ok = branch_evaled.result.ok
|
||||
if(ok) {
|
||||
return {ok, children, calls: calls_after_branch, value: branch_evaled.result.value}
|
||||
return {ok, children, calls: calls_after_branch, scope: scope_after_branch,
|
||||
value: branch_evaled.result.value}
|
||||
} else {
|
||||
return {ok, children, calls: calls_after_branch}
|
||||
return {ok, children, calls: calls_after_branch, scope: scope_after_branch}
|
||||
}
|
||||
}
|
||||
} else if(node.type == 'member_access'){
|
||||
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
||||
const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context)
|
||||
if(!ok) {
|
||||
return {ok: false, children, calls}
|
||||
return {ok: false, children, calls, scope: nextscope}
|
||||
}
|
||||
|
||||
const [obj, prop] = children
|
||||
@@ -702,12 +869,13 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
}),
|
||||
children,
|
||||
calls,
|
||||
scope: nextscope
|
||||
}
|
||||
|
||||
} else if(node.type == 'unary') {
|
||||
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
||||
const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context)
|
||||
if(!ok) {
|
||||
return {ok: false, children, calls}
|
||||
return {ok: false, children, calls, scope: nextscope}
|
||||
} else {
|
||||
const expr = children[0]
|
||||
let ok, value, error, is_error_origin
|
||||
@@ -739,14 +907,14 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
} else {
|
||||
throw new Error('unknown op')
|
||||
}
|
||||
return {ok, children, calls, value, error, is_error_origin}
|
||||
return {ok, children, calls, value, error, is_error_origin, scope: nextscope}
|
||||
}
|
||||
} else if(node.type == 'binary' && !['&&', '||', '??'].includes(node.operator)){
|
||||
|
||||
return eval_binary_expr(node, scope, callsleft, context)
|
||||
|
||||
} else if(node.type == 'binary' && ['&&', '||', '??'].includes(node.operator)){
|
||||
const {node: left_evaled, calls} = eval_frame_expr(
|
||||
const {node: left_evaled, calls, scope: nextscope} = eval_frame_expr(
|
||||
node.children[0],
|
||||
scope,
|
||||
callsleft,
|
||||
@@ -768,17 +936,18 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
value,
|
||||
children: [left_evaled, node.children[1]],
|
||||
calls,
|
||||
scope: nextscope,
|
||||
}
|
||||
} else {
|
||||
return eval_binary_expr(node, scope, callsleft, context)
|
||||
}
|
||||
|
||||
} else if(node.type == 'grouping'){
|
||||
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
||||
const {ok, children, calls, scope: nextscope} = eval_children(node, scope, callsleft, context)
|
||||
if(!ok) {
|
||||
return {ok, children, calls}
|
||||
return {ok, children, calls, scope: nextscope}
|
||||
} else {
|
||||
return {ok: true, children, calls, value: children[0].result.value}
|
||||
return {ok: true, children, calls, scope: nextscope, value: children[0].result.value}
|
||||
}
|
||||
} else {
|
||||
console.error(node)
|
||||
@@ -788,26 +957,33 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
|
||||
const eval_children = (node, scope, calls, context) => {
|
||||
return node.children.reduce(
|
||||
({ok, children, calls}, child) => {
|
||||
let next_child, next_ok, next_calls
|
||||
({ok, children, calls, scope}, child) => {
|
||||
let next_child, next_ok, next_calls, next_scope
|
||||
if(!ok) {
|
||||
next_child = child
|
||||
next_ok = false
|
||||
next_calls = calls
|
||||
next_scope = scope
|
||||
} else {
|
||||
const result = eval_frame_expr(child, scope, calls, context)
|
||||
next_child = result.node
|
||||
next_calls = result.calls
|
||||
next_ok = next_child.result.ok
|
||||
next_scope = result.scope
|
||||
}
|
||||
return {
|
||||
ok: next_ok,
|
||||
children: [...children, next_child],
|
||||
calls: next_calls,
|
||||
scope: next_scope,
|
||||
}
|
||||
return {ok: next_ok, children: [...children, next_child], calls: next_calls}
|
||||
},
|
||||
{ok: true, children: [], calls}
|
||||
{ok: true, children: [], calls, scope}
|
||||
)
|
||||
}
|
||||
|
||||
const eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
const {ok, error, is_error_origin, value, call, children, calls}
|
||||
const {ok, error, is_error_origin, value, call, children, calls, scope: nextscope}
|
||||
= do_eval_frame_expr(node, scope, callsleft, context)
|
||||
if(callsleft != null && calls == null) {
|
||||
// TODO remove it, just for debug
|
||||
@@ -821,35 +997,18 @@ const eval_frame_expr = (node, scope, callsleft, context) => {
|
||||
// Add `call` for step_into
|
||||
result: {ok, error, value, call, is_error_origin}
|
||||
},
|
||||
scope: nextscope,
|
||||
calls,
|
||||
}
|
||||
}
|
||||
|
||||
const apply_assignments = (node, assignments) => {
|
||||
const let_ids = node
|
||||
.children
|
||||
.filter(c => c.type == 'let')
|
||||
.map(l => l.children)
|
||||
.flat()
|
||||
.map(c => c.index)
|
||||
|
||||
const parent_assignments = Object.entries(assignments).filter(([index, v]) =>
|
||||
let_ids.find(i => i.toString() == index) == null
|
||||
)
|
||||
// Scope we return to parent block
|
||||
const scope = Object.fromEntries(
|
||||
parent_assignments.map(([k, {name, value}]) => [name, value])
|
||||
)
|
||||
return {node, scope, assignments: Object.fromEntries(parent_assignments)}
|
||||
}
|
||||
|
||||
const eval_decl_pair = (s, scope, calls, context, is_assignment) => {
|
||||
const eval_decl_pair = (s, scope, calls, context) => {
|
||||
if(s.type != 'decl_pair') {
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
// TODO default values for destructuring can be function calls
|
||||
|
||||
const {node, calls: next_calls}
|
||||
const {node, calls: next_calls, scope: scope_after_expr}
|
||||
= eval_frame_expr(s.expr, scope, calls, context)
|
||||
const s_expr_evaled = {...s, children: [s.name_node, node]}
|
||||
if(!node.result.ok) {
|
||||
@@ -858,23 +1017,28 @@ const eval_decl_pair = (s, scope, calls, context, is_assignment) => {
|
||||
node: {...s_expr_evaled, result: {ok: false}},
|
||||
scope,
|
||||
calls: next_calls,
|
||||
scope: scope_after_expr,
|
||||
}
|
||||
}
|
||||
|
||||
const name_nodes = collect_destructuring_identifiers(s.name_node)
|
||||
const names = name_nodes.map(n => n.value)
|
||||
const destructuring = codegen(s.name_node)
|
||||
const destructuring = codegen(s.name_node, {literal_identifiers: true})
|
||||
|
||||
// TODO unique name for __value (gensym)
|
||||
const codestring = `
|
||||
const ${destructuring} = __value;
|
||||
({${names.join(',')}});
|
||||
`
|
||||
const {ok, value: next_scope, error} = eval_codestring(
|
||||
const {ok, value: values, error} = eval_codestring(
|
||||
codestring,
|
||||
{...scope, __value: node.result.value}
|
||||
{...scope_after_expr, __value: node.result.value}
|
||||
)
|
||||
|
||||
const next_scope = Object.fromEntries(name_nodes.map(n =>
|
||||
[symbol_for_identifier(n, context), values[n.value]]
|
||||
))
|
||||
|
||||
// TODO fine-grained destructuring error, only for identifiers that failed
|
||||
// destructuring
|
||||
const name_node_with_result = map_tree(
|
||||
@@ -884,7 +1048,7 @@ const eval_decl_pair = (s, scope, calls, context, is_assignment) => {
|
||||
result: {
|
||||
ok,
|
||||
error: ok ? null : error,
|
||||
value: !ok ? null : next_scope[node.value],
|
||||
value: !ok ? null : next_scope[symbol_for_identifier(node, context)],
|
||||
}
|
||||
})
|
||||
),
|
||||
@@ -903,7 +1067,7 @@ const eval_decl_pair = (s, scope, calls, context, is_assignment) => {
|
||||
ok: false,
|
||||
// TODO assign error to node where destructuring failed, not to every node
|
||||
node: {...s_evaled, result: {ok, error, is_error_origin: true}},
|
||||
scope,
|
||||
scope: scope_after_expr,
|
||||
calls,
|
||||
}
|
||||
}
|
||||
@@ -911,19 +1075,8 @@ const eval_decl_pair = (s, scope, calls, context, is_assignment) => {
|
||||
return {
|
||||
ok: true,
|
||||
node: {...s_evaled, result: {ok: true}},
|
||||
scope: {...scope, ...next_scope},
|
||||
scope: {...scope_after_expr, ...next_scope},
|
||||
calls: next_calls,
|
||||
assignments: is_assignment
|
||||
? Object.fromEntries(
|
||||
name_nodes.map(n => [
|
||||
n.definition.index,
|
||||
{
|
||||
value: next_scope[n.value],
|
||||
name: n.value,
|
||||
}
|
||||
])
|
||||
)
|
||||
: null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -954,17 +1107,16 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
|
||||
const initial_scope = {...scope, ...hoisted_functions_scope}
|
||||
|
||||
const {ok, assignments, returned, children, calls: next_calls} = s.children.reduce(
|
||||
({ok, returned, children, scope, calls, assignments}, s) => {
|
||||
const {ok, returned, children, calls: next_calls, scope: next_scope} =
|
||||
s.children.reduce( ({ok, returned, children, scope, calls}, s) => {
|
||||
if(returned || !ok) {
|
||||
return {ok, returned, scope, calls, children: [...children, s], assignments}
|
||||
return {ok, returned, scope, calls, children: [...children, s]}
|
||||
} else if(s.type == 'function_decl') {
|
||||
const node = function_decls.find(decl => decl.index == s.index)
|
||||
return {
|
||||
ok: true,
|
||||
returned: false,
|
||||
node,
|
||||
assignments,
|
||||
scope,
|
||||
calls,
|
||||
children: [...children, node],
|
||||
@@ -974,39 +1126,37 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
ok,
|
||||
returned,
|
||||
node,
|
||||
assignments: next_assignments,
|
||||
scope: nextscope,
|
||||
calls: next_calls,
|
||||
} = eval_statement(s, scope, calls, context)
|
||||
return {
|
||||
ok,
|
||||
returned,
|
||||
assignments: {...assignments, ...next_assignments},
|
||||
scope: nextscope,
|
||||
calls: next_calls,
|
||||
children: [...children, node],
|
||||
}
|
||||
}
|
||||
},
|
||||
{ok: true, returned: false, children: [], scope: initial_scope, calls, assignments: {}}
|
||||
{ok: true, returned: false, children: [], scope: initial_scope, calls}
|
||||
)
|
||||
const let_vars_scope = filter_object(next_scope, (k, v) =>
|
||||
is_symbol_for_let_var(k)
|
||||
)
|
||||
const {node: next_node, scope: next_scope, assignments: parent_assignments} =
|
||||
apply_assignments({...s, children: children, result: {ok}}, assignments)
|
||||
return {
|
||||
ok,
|
||||
node: next_node,
|
||||
scope: {...scope, ...next_scope},
|
||||
node: {...s, children: children, result: {ok}},
|
||||
scope: {...scope, ...let_vars_scope},
|
||||
returned,
|
||||
parent_assignments,
|
||||
calls: next_calls,
|
||||
}
|
||||
} else if(['let', 'const', 'assignment'].includes(s.type)) {
|
||||
const stmt = s
|
||||
|
||||
const initial = {ok: true, children: [], scope, calls, assignments: null}
|
||||
const initial = {ok: true, children: [], scope, calls}
|
||||
|
||||
const {ok, children, calls: next_calls, scope: next_scope, assignments} = s.children.reduce(
|
||||
({ok, children, scope, calls, assignments}, s) => {
|
||||
const {ok, children, calls: next_calls, scope: next_scope} = s.children.reduce(
|
||||
({ok, children, scope, calls}, s) => {
|
||||
if(!ok) {
|
||||
return {ok, scope, calls, children: [...children, s]}
|
||||
}
|
||||
@@ -1015,7 +1165,7 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
return {
|
||||
ok,
|
||||
children: [...children, node],
|
||||
scope: {...scope, [s.value]: undefined},
|
||||
scope: {...scope, [symbol_for_identifier(s, context)]: undefined},
|
||||
calls
|
||||
}
|
||||
}
|
||||
@@ -1024,16 +1174,12 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
node,
|
||||
scope: nextscope,
|
||||
calls: next_calls,
|
||||
assignments: next_assignments,
|
||||
} = eval_decl_pair(s, scope, calls, context, stmt.type == 'assignment')
|
||||
} = eval_decl_pair(s, scope, calls, context)
|
||||
return {
|
||||
ok: next_ok,
|
||||
scope: nextscope,
|
||||
calls: next_calls,
|
||||
children: [...children, node],
|
||||
assignments: stmt.type == 'assignment'
|
||||
? {...assignments, ...next_assignments}
|
||||
: null
|
||||
}
|
||||
},
|
||||
initial
|
||||
@@ -1043,18 +1189,17 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
node: {...s, children, result: {ok}},
|
||||
scope: {...scope, ...next_scope},
|
||||
calls: next_calls,
|
||||
assignments,
|
||||
}
|
||||
} else if(s.type == 'return') {
|
||||
|
||||
const {node, calls: next_calls} =
|
||||
const {node, calls: next_calls, scope: nextscope} =
|
||||
eval_frame_expr(s.expr, scope, calls, context)
|
||||
|
||||
return {
|
||||
ok: node.result.ok,
|
||||
returned: node.result.ok,
|
||||
node: {...s, children: [node], result: {ok: node.result.ok}},
|
||||
scope,
|
||||
scope: nextscope,
|
||||
calls: next_calls,
|
||||
}
|
||||
|
||||
@@ -1090,7 +1235,8 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
}
|
||||
} else if(s.type == 'if') {
|
||||
|
||||
const {node, calls: next_calls} = eval_frame_expr(s.cond, scope, calls, context)
|
||||
const {node, calls: next_calls, scope: scope_after_cond} =
|
||||
eval_frame_expr(s.cond, scope, calls, context)
|
||||
|
||||
if(!node.result.ok) {
|
||||
return {
|
||||
@@ -1098,6 +1244,7 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
node: {...s, children: [node, ...s.branches], result: {ok: false}},
|
||||
scope,
|
||||
calls: next_calls,
|
||||
scope: scope_after_cond,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1108,19 +1255,17 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
const {
|
||||
node: evaled_branch,
|
||||
returned,
|
||||
assignments,
|
||||
scope: next_scope,
|
||||
calls: next_calls2,
|
||||
} = eval_statement(
|
||||
s.branches[0],
|
||||
scope,
|
||||
scope_after_cond,
|
||||
next_calls,
|
||||
context
|
||||
)
|
||||
return {
|
||||
ok: evaled_branch.result.ok,
|
||||
returned,
|
||||
assignments,
|
||||
node: {...s,
|
||||
children: [node, evaled_branch],
|
||||
result: {ok: evaled_branch.result.ok}
|
||||
@@ -1144,12 +1289,11 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
const {
|
||||
node: evaled_branch,
|
||||
returned,
|
||||
assignments,
|
||||
scope: next_scope,
|
||||
calls: next_calls2
|
||||
} = eval_statement(
|
||||
active_branch,
|
||||
scope,
|
||||
scope_after_cond,
|
||||
next_calls,
|
||||
context,
|
||||
)
|
||||
@@ -1161,7 +1305,6 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
return {
|
||||
ok: evaled_branch.result.ok,
|
||||
returned,
|
||||
assignments,
|
||||
node: {...s, children, result: {ok: evaled_branch.result.ok}},
|
||||
scope: next_scope,
|
||||
calls: next_calls2,
|
||||
@@ -1170,7 +1313,8 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
|
||||
} else if(s.type == 'throw') {
|
||||
|
||||
const {node, calls: next_calls} = eval_frame_expr(s.expr, scope, calls, context)
|
||||
const {node, calls: next_calls, scope: next_scope} =
|
||||
eval_frame_expr(s.expr, scope, calls, context)
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
@@ -1182,17 +1326,17 @@ const eval_statement = (s, scope, calls, context) => {
|
||||
error: node.result.ok ? node.result.value : null,
|
||||
}
|
||||
},
|
||||
scope,
|
||||
scope: next_scope,
|
||||
calls: next_calls,
|
||||
}
|
||||
|
||||
} else {
|
||||
// stmt type is expression
|
||||
const {node, calls: next_calls} = eval_frame_expr(s, scope, calls, context)
|
||||
const {node, calls: next_calls, scope: next_scope} = eval_frame_expr(s, scope, calls, context)
|
||||
return {
|
||||
ok: node.result.ok,
|
||||
node,
|
||||
scope,
|
||||
scope: next_scope,
|
||||
calls: next_calls,
|
||||
}
|
||||
}
|
||||
@@ -1205,6 +1349,7 @@ export const eval_frame = (calltree_node, modules) => {
|
||||
const node = calltree_node.code
|
||||
const context = {calltree_node, modules}
|
||||
if(node.type == 'do') {
|
||||
// eval module toplevel
|
||||
return eval_statement(
|
||||
node,
|
||||
{},
|
||||
@@ -1214,10 +1359,15 @@ export const eval_frame = (calltree_node, modules) => {
|
||||
} else {
|
||||
// TODO default values for destructuring can be function calls
|
||||
|
||||
const closure = map_object(calltree_node.fn.__closure, (_key, value) => {
|
||||
return value instanceof Multiversion
|
||||
? value.get_version(calltree_node.id)
|
||||
: value
|
||||
})
|
||||
const args_scope_result = get_args_scope(
|
||||
node,
|
||||
calltree_node.args,
|
||||
calltree_node.fn.__closure
|
||||
closure
|
||||
)
|
||||
|
||||
// TODO fine-grained destructuring error, only for identifiers that
|
||||
@@ -1254,8 +1404,16 @@ export const eval_frame = (calltree_node, modules) => {
|
||||
}
|
||||
}
|
||||
|
||||
const scope = {...calltree_node.fn.__closure, ...args_scope_result.value}
|
||||
const closure_scope = Object.fromEntries(
|
||||
Object.entries(closure).map(([key, value]) => {
|
||||
return [
|
||||
symbol_for_closed_let_var(key),
|
||||
value,
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
const scope = {...closure_scope, ...args_scope_result.value}
|
||||
|
||||
let nextbody
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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}
|
||||
|
||||
146
src/runtime.js
146
src/runtime.js
@@ -79,17 +79,20 @@ const do_run = function*(module_fns, cxt, io_trace){
|
||||
toplevel: true,
|
||||
module,
|
||||
id: ++cxt.call_counter,
|
||||
let_vars: {},
|
||||
}
|
||||
|
||||
try {
|
||||
cxt.modules[module] = {}
|
||||
const result = fn(
|
||||
cxt,
|
||||
calltree.let_vars,
|
||||
calltree_node_by_loc.get(module),
|
||||
__trace,
|
||||
__trace_call,
|
||||
__do_await,
|
||||
__save_ct_node_for_path,
|
||||
Multiversion,
|
||||
)
|
||||
if(result instanceof cxt.window.Promise) {
|
||||
yield cxt.window.Promise.race([replay_aborted_promise, result])
|
||||
@@ -102,6 +105,7 @@ const do_run = function*(module_fns, cxt, io_trace){
|
||||
calltree.error = error
|
||||
}
|
||||
calltree.children = cxt.children
|
||||
calltree.next_id = cxt.call_counter + 1
|
||||
if(!calltree.ok) {
|
||||
break
|
||||
}
|
||||
@@ -184,6 +188,19 @@ export const set_record_call = cxt => {
|
||||
|
||||
export const do_eval_expand_calltree_node = (cxt, node) => {
|
||||
cxt.is_recording_deferred_calls = false
|
||||
cxt.is_expanding_calltree_node = true
|
||||
cxt.touched_multiversions = new Set()
|
||||
|
||||
// Save call counter and set it to the value it had when executed 'fn' for
|
||||
// the first time
|
||||
const call_counter = cxt.call_counter
|
||||
cxt.call_counter = node.fn.__location == null
|
||||
// Function is native, set call_counter to node.id
|
||||
? node.id
|
||||
// call_counter will be incremented inside __trace and produce the same id
|
||||
// as node.id
|
||||
: node.id - 1
|
||||
|
||||
cxt.children = null
|
||||
try {
|
||||
if(node.is_new) {
|
||||
@@ -195,6 +212,21 @@ export const do_eval_expand_calltree_node = (cxt, node) => {
|
||||
// do nothing. Exception was caught and recorded inside '__trace'
|
||||
}
|
||||
|
||||
// Restore call counter
|
||||
cxt.call_counter = call_counter
|
||||
|
||||
// Recover multiversions affected by expand_calltree_node
|
||||
for(let m of cxt.touched_multiversions) {
|
||||
if(m.is_expanding_calltree_node) {
|
||||
delete m.is_expanding_calltree_node
|
||||
}
|
||||
if(m.latest_copy != null) {
|
||||
m.latest = m.latest_copy.value
|
||||
}
|
||||
}
|
||||
delete cxt.touched_multiversions
|
||||
|
||||
cxt.is_expanding_calltree_node = false
|
||||
cxt.is_recording_deferred_calls = true
|
||||
const children = cxt.children
|
||||
cxt.children = null
|
||||
@@ -243,7 +275,7 @@ const __do_await = async (cxt, value) => {
|
||||
}
|
||||
}
|
||||
|
||||
const __trace = (cxt, fn, name, argscount, __location, get_closure) => {
|
||||
const __trace = (cxt, fn, name, argscount, __location, get_closure, has_versioned_let_vars) => {
|
||||
const result = (...args) => {
|
||||
if(result.__closure == null) {
|
||||
result.__closure = get_closure()
|
||||
@@ -268,6 +300,11 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure) => {
|
||||
}
|
||||
}
|
||||
|
||||
let let_vars
|
||||
if(has_versioned_let_vars) {
|
||||
let_vars = cxt.let_vars = {}
|
||||
}
|
||||
|
||||
let ok, value, error
|
||||
|
||||
const is_toplevel_call_copy = cxt.is_toplevel_call
|
||||
@@ -296,6 +333,8 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure) => {
|
||||
|
||||
const call = {
|
||||
id: call_id,
|
||||
next_id: cxt.call_counter + 1,
|
||||
let_vars,
|
||||
ok,
|
||||
value,
|
||||
error,
|
||||
@@ -358,6 +397,8 @@ const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => {
|
||||
cxt.children = null
|
||||
cxt.stack.push(false)
|
||||
|
||||
const call_id = ++cxt.call_counter
|
||||
|
||||
// TODO: other console fns
|
||||
const is_log = fn == cxt.window.console.log || fn == cxt.window.console.error
|
||||
|
||||
@@ -392,7 +433,8 @@ const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => {
|
||||
cxt.prev_children = cxt.children
|
||||
|
||||
const call = {
|
||||
id: ++cxt.call_counter,
|
||||
id: call_id,
|
||||
next_id: cxt.call_counter + 1,
|
||||
ok,
|
||||
value,
|
||||
error,
|
||||
@@ -431,9 +473,107 @@ const __save_ct_node_for_path = (cxt, __calltree_node_by_loc, index, __call_id)
|
||||
if(cxt.skip_save_ct_node_for_path) {
|
||||
return
|
||||
}
|
||||
|
||||
if(__calltree_node_by_loc.get(index) == null) {
|
||||
__calltree_node_by_loc.set(index, __call_id)
|
||||
set_record_call(cxt)
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/29018745
|
||||
function binarySearch(arr, el, compare_fn) {
|
||||
let m = 0;
|
||||
let n = arr.length - 1;
|
||||
while (m <= n) {
|
||||
let k = (n + m) >> 1;
|
||||
let cmp = compare_fn(el, arr[k]);
|
||||
if (cmp > 0) {
|
||||
m = k + 1;
|
||||
} else if(cmp < 0) {
|
||||
n = k - 1;
|
||||
} else {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
return ~m;
|
||||
}
|
||||
|
||||
// 'let' variable recording the history of its values
|
||||
export class Multiversion {
|
||||
constructor(cxt, initial) {
|
||||
this.cxt = cxt
|
||||
this.is_expanding_calltree_node = cxt.is_expanding_calltree_node
|
||||
this.latest = initial
|
||||
this.versions = [{call_id: cxt.call_counter, value: initial}]
|
||||
}
|
||||
|
||||
get() {
|
||||
const call_id = this.cxt.call_counter
|
||||
|
||||
if(!this.cxt.is_expanding_calltree_node) {
|
||||
return this.latest
|
||||
} else {
|
||||
if(this.is_expanding_calltree_node) {
|
||||
// var was created during current expansion, use its latest value
|
||||
return this.latest
|
||||
} else {
|
||||
if(this.latest_copy != null) {
|
||||
// value was set during expand_calltree_node, use this value
|
||||
return this.latest
|
||||
}
|
||||
// TODO on first read, set latest and latest_copy?
|
||||
return this.get_version(call_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get_version(call_id) {
|
||||
const idx = binarySearch(this.versions, call_id, (id, el) => id - el.call_id)
|
||||
if(idx == 0) {
|
||||
// This branch is unreachable. get_version will be never called for a
|
||||
// call_id where let variable was declared.
|
||||
throw new Error('illegal state')
|
||||
} else if(idx > 0) {
|
||||
return this.versions[idx - 1].value
|
||||
} else if(idx == -1) {
|
||||
throw new Error('illegal state')
|
||||
} else {
|
||||
return this.versions[-idx - 2].value
|
||||
}
|
||||
}
|
||||
|
||||
set(value) {
|
||||
const call_id = this.cxt.call_counter
|
||||
if(this.cxt.is_expanding_calltree_node) {
|
||||
if(this.is_expanding_calltree_node) {
|
||||
this.latest = value
|
||||
this.set_version(call_id, value)
|
||||
this.cxt.touched_multiversions.add(this)
|
||||
} else {
|
||||
if(this.latest_copy == null) {
|
||||
this.latest_copy = {value: this.latest}
|
||||
}
|
||||
this.cxt.touched_multiversions.add(this)
|
||||
this.latest = value
|
||||
}
|
||||
} else {
|
||||
this.latest = value
|
||||
this.set_version(call_id, value)
|
||||
}
|
||||
}
|
||||
|
||||
last_version_number() {
|
||||
return this.versions.at(-1).call_id
|
||||
}
|
||||
|
||||
set_version(call_id, value) {
|
||||
const last_version = this.versions.at(-1)
|
||||
if(last_version.call_id > call_id) {
|
||||
throw new Error('illegal state')
|
||||
}
|
||||
if(last_version.call_id == call_id) {
|
||||
last_version.value = value
|
||||
return
|
||||
}
|
||||
this.versions.push({call_id, value})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
611
test/test.js
611
test/test.js
@@ -105,6 +105,14 @@ export const tests = [
|
||||
)
|
||||
}),
|
||||
|
||||
// TODO
|
||||
// test('backtick_string let vars', () => {
|
||||
// assert_code_evals_to(
|
||||
// 'let x = `b`; `a${x}a`',
|
||||
// 'aba',
|
||||
// )
|
||||
// }),
|
||||
|
||||
test('Simple expression', () => {
|
||||
return assert_code_evals_to('1+1;', 2)
|
||||
}),
|
||||
@@ -320,6 +328,7 @@ export const tests = [
|
||||
)
|
||||
}),
|
||||
|
||||
/*
|
||||
test('let variable', () => {
|
||||
const code = `
|
||||
let x, y = 2, unused, [z,q] = [3,4]
|
||||
@@ -328,6 +337,7 @@ export const tests = [
|
||||
const i = test_initial_state(code, code.indexOf('x'))
|
||||
assert_equal(i.value_explorer.result.value, {y: 2, z: 3, q: 4})
|
||||
}),
|
||||
*/
|
||||
|
||||
test('let variable not initialized bug', () => {
|
||||
const code = `
|
||||
@@ -352,11 +362,6 @@ export const tests = [
|
||||
};
|
||||
x
|
||||
`
|
||||
const parse_result = do_parse(code)
|
||||
const assignment = find_leaf(
|
||||
parse_result.node,
|
||||
code.indexOf('x = 0')
|
||||
)
|
||||
assert_code_evals_to(
|
||||
code,
|
||||
1
|
||||
@@ -1476,6 +1481,18 @@ export const tests = [
|
||||
}),
|
||||
|
||||
test('multiple assignments', () => {
|
||||
assert_code_evals_to(
|
||||
`
|
||||
let x, y
|
||||
x = 1, y = 2
|
||||
{x,y}
|
||||
`,
|
||||
{x: 1, y: 2}
|
||||
)
|
||||
}),
|
||||
|
||||
/* TODO assignments destructuring
|
||||
test('multiple assignments destructuring', () => {
|
||||
assert_code_evals_to(
|
||||
`
|
||||
let x, y
|
||||
@@ -1485,6 +1502,7 @@ export const tests = [
|
||||
{x: 1, y: 2}
|
||||
)
|
||||
}),
|
||||
*/
|
||||
|
||||
test('assigments value explorer', () => {
|
||||
const code = `
|
||||
@@ -1504,7 +1522,8 @@ export const tests = [
|
||||
assert_equal(i.value_explorer.result.value, {x: 1, y: 2})
|
||||
}),
|
||||
|
||||
test('assigments destructuring value explorer', () => {
|
||||
/* TODO
|
||||
test('assignments destructuring value explorer', () => {
|
||||
const code = `
|
||||
let x, y
|
||||
x = 1, {y} = {y:2}
|
||||
@@ -1512,6 +1531,7 @@ export const tests = [
|
||||
const i = test_initial_state(code, code.indexOf('x = 1'))
|
||||
assert_equal(i.value_explorer.result.value, {x: 1, y: 2})
|
||||
}),
|
||||
*/
|
||||
|
||||
test('assigments error', () => {
|
||||
const code = `
|
||||
@@ -1522,6 +1542,19 @@ export const tests = [
|
||||
assert_equal(i.value_explorer.result.ok, false)
|
||||
}),
|
||||
|
||||
test('block scoping const', () => {
|
||||
assert_code_evals_to(
|
||||
`
|
||||
const x = 0
|
||||
if(true) {
|
||||
const x = 1
|
||||
}
|
||||
x
|
||||
`,
|
||||
0
|
||||
)
|
||||
}),
|
||||
|
||||
test('block scoping', () => {
|
||||
assert_code_evals_to(
|
||||
`
|
||||
@@ -4079,4 +4112,570 @@ const y = x()`
|
||||
'Map {foo: "bar", baz: "qux"}'
|
||||
)
|
||||
}),
|
||||
|
||||
test('let_versions find_versioned_lets toplevel', () => {
|
||||
const result = do_parse(`
|
||||
let x
|
||||
x = 1
|
||||
function foo() {
|
||||
x
|
||||
}
|
||||
`)
|
||||
assert_equal(result.node.has_versioned_let_vars, true)
|
||||
}),
|
||||
|
||||
test('let_versions find_versioned_lets', () => {
|
||||
function assert_is_versioned_let(code, is_versioned) {
|
||||
const result = do_parse(code)
|
||||
const root = find_node(result.node,
|
||||
n => n.name == 'root' && n.type == 'function_expr'
|
||||
)
|
||||
assert_equal(root.has_versioned_let_vars, is_versioned)
|
||||
const node = find_node(result.node, n => n.index == code.indexOf('x'))
|
||||
assert_equal(!(!node.is_versioned_let_var), is_versioned)
|
||||
}
|
||||
|
||||
assert_is_versioned_let(
|
||||
`
|
||||
function root() {
|
||||
let x
|
||||
x = 1
|
||||
function foo() {
|
||||
x
|
||||
}
|
||||
}
|
||||
`,
|
||||
true
|
||||
)
|
||||
|
||||
// closed but constant
|
||||
assert_is_versioned_let(
|
||||
`
|
||||
function root() {
|
||||
let x
|
||||
function foo() {
|
||||
x
|
||||
}
|
||||
}
|
||||
`,
|
||||
false
|
||||
)
|
||||
|
||||
// assigned but not closed
|
||||
assert_is_versioned_let(
|
||||
`
|
||||
function root() {
|
||||
let x
|
||||
x = 1
|
||||
}
|
||||
`,
|
||||
false
|
||||
)
|
||||
|
||||
// not closed, var has the same name
|
||||
assert_is_versioned_let(
|
||||
`
|
||||
function root() {
|
||||
let x
|
||||
x = 1
|
||||
function foo() {
|
||||
let x
|
||||
x
|
||||
}
|
||||
}
|
||||
`,
|
||||
false
|
||||
)
|
||||
|
||||
// not closed, var has the same name
|
||||
assert_is_versioned_let(
|
||||
`
|
||||
function root() {
|
||||
let x
|
||||
x = 1
|
||||
if(true) {
|
||||
let x
|
||||
function foo() {
|
||||
x
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
false
|
||||
)
|
||||
}),
|
||||
|
||||
test('let_versions', () => {
|
||||
const code = `
|
||||
let x
|
||||
[1,2].forEach(y => {
|
||||
x /*x*/
|
||||
x = y
|
||||
})
|
||||
`
|
||||
const x_pos = code.indexOf('x /*x*/')
|
||||
const i = test_initial_state(code, x_pos)
|
||||
const second_iter = COMMANDS.calltree.arrow_down(i)
|
||||
const select_x = COMMANDS.move_cursor(second_iter, x_pos)
|
||||
assert_equal(select_x.value_explorer.result.value, 1)
|
||||
}),
|
||||
|
||||
test('let_versions close let var bug', () => {
|
||||
const code = `
|
||||
let x
|
||||
x = 1
|
||||
function y() {
|
||||
return {x}
|
||||
}
|
||||
y() /*y()*/
|
||||
`
|
||||
const i = test_initial_state(code, code.indexOf('y() /*y()*/'))
|
||||
assert_equal(i.value_explorer.result.value, {x: 1})
|
||||
}),
|
||||
|
||||
test('let_versions initial let value', () => {
|
||||
const code = `
|
||||
let x
|
||||
function y() {
|
||||
x /*x*/
|
||||
}
|
||||
y()
|
||||
`
|
||||
const x_pos = code.indexOf('x /*x*/')
|
||||
const i = test_initial_state(code, x_pos)
|
||||
assert_equal(i.value_explorer.result, {ok: true, value: undefined})
|
||||
}),
|
||||
|
||||
test('let_versions save version bug', () => {
|
||||
const code = `
|
||||
let x = 0
|
||||
|
||||
function set_x(value) {
|
||||
x = value
|
||||
}
|
||||
|
||||
function get_x() {
|
||||
x /* result */
|
||||
}
|
||||
|
||||
get_x()
|
||||
|
||||
set_x(10)
|
||||
x = 10
|
||||
set_x(10)
|
||||
x = 10
|
||||
`
|
||||
const i = test_initial_state(code, code.indexOf('x /* result */'))
|
||||
assert_equal(i.value_explorer.result.value, 0)
|
||||
}),
|
||||
|
||||
test('let_versions expand_calltree_node', () => {
|
||||
const code = `
|
||||
let y
|
||||
|
||||
function foo(x) {
|
||||
y /*y*/
|
||||
bar(y)
|
||||
}
|
||||
|
||||
function bar(arg) {
|
||||
}
|
||||
|
||||
foo(0)
|
||||
y = 11
|
||||
foo(0)
|
||||
y = 12
|
||||
`
|
||||
const i = test_initial_state(code)
|
||||
const second_foo_call = root_calltree_node(i).children[1]
|
||||
assert_equal(second_foo_call.has_more_children, true)
|
||||
const expanded = COMMANDS.calltree.click(i, second_foo_call.id)
|
||||
const bar_call = root_calltree_node(expanded).children[1].children[0]
|
||||
assert_equal(bar_call.fn.name, 'bar')
|
||||
assert_equal(bar_call.args, [11])
|
||||
const moved = COMMANDS.move_cursor(expanded, code.indexOf('y /*y*/'))
|
||||
assert_equal(moved.value_explorer.result.value, 11)
|
||||
}),
|
||||
|
||||
test('let_versions expand_calltree_node 2', () => {
|
||||
const code = `
|
||||
let y
|
||||
|
||||
function deep(x) {
|
||||
if(x < 10) {
|
||||
y /*y*/
|
||||
y = x
|
||||
deep(x + 1)
|
||||
}
|
||||
}
|
||||
|
||||
deep(0)
|
||||
y = 11
|
||||
deep(0)
|
||||
y = 12
|
||||
`
|
||||
const i = test_initial_state(code)
|
||||
const second_deep_call = root_calltree_node(i).children[1]
|
||||
assert_equal(second_deep_call.has_more_children, true)
|
||||
const expanded = COMMANDS.calltree.click(i, second_deep_call.id)
|
||||
const moved = COMMANDS.move_cursor(expanded, code.indexOf('y /*y*/'))
|
||||
assert_equal(moved.value_explorer.result.value, 11)
|
||||
}),
|
||||
|
||||
test('let_versions create multiversion within expand_calltree_node', () => {
|
||||
const code = `
|
||||
function x() {
|
||||
let y
|
||||
function set(value) {
|
||||
y = value
|
||||
}
|
||||
set(1)
|
||||
y /*result*/
|
||||
set(2)
|
||||
}
|
||||
|
||||
x()
|
||||
x()
|
||||
|
||||
`
|
||||
const i = test_initial_state(code)
|
||||
const second_x_call = root_calltree_node(i).children[1]
|
||||
assert_equal(second_x_call.has_more_children, true)
|
||||
const expanded = COMMANDS.calltree.click(i, second_x_call.id)
|
||||
const moved = COMMANDS.move_cursor(expanded, code.indexOf('y /*result*/'))
|
||||
assert_equal(moved.value_explorer.result.value, 1)
|
||||
}),
|
||||
|
||||
test('let_versions mutable closure', () => {
|
||||
const code = `
|
||||
const holder = (function() {
|
||||
let value
|
||||
return {
|
||||
get: () => value,
|
||||
set: (v) => {
|
||||
value /*value*/
|
||||
value = v
|
||||
}
|
||||
}
|
||||
})()
|
||||
Array.from({length: 10}).map((_, i) => {
|
||||
holder.set(i)
|
||||
})
|
||||
holder.get()
|
||||
`
|
||||
const i = test_initial_state(code, code.indexOf('holder.get'))
|
||||
assert_equal(i.value_explorer.result.value, 9)
|
||||
|
||||
const map_expanded = COMMANDS.calltree.click(
|
||||
i,
|
||||
root_calltree_node(i).children[2].id
|
||||
)
|
||||
const expanded = COMMANDS.calltree.click(
|
||||
map_expanded,
|
||||
root_calltree_node(map_expanded).children[2].children[5].id
|
||||
)
|
||||
const set_call = COMMANDS.calltree.arrow_right(
|
||||
COMMANDS.calltree.arrow_right(
|
||||
expanded
|
||||
)
|
||||
)
|
||||
assert_equal(
|
||||
set_call.active_calltree_node.code.index,
|
||||
code.indexOf('(v) =>')
|
||||
)
|
||||
const moved = COMMANDS.move_cursor(set_call, code.indexOf('value /*value*/'))
|
||||
assert_equal(moved.value_explorer.result.value, 4)
|
||||
}),
|
||||
|
||||
test('let_versions forEach', () => {
|
||||
const code = `
|
||||
let sum = 0
|
||||
[1,2,3].forEach(v => {
|
||||
sum = sum + v
|
||||
})
|
||||
sum /*first*/
|
||||
[1,2,3].forEach(v => {
|
||||
sum = sum + v
|
||||
})
|
||||
sum /*second*/
|
||||
`
|
||||
const i = test_initial_state(code, code.indexOf('sum /*first*/'))
|
||||
assert_equal(i.value_explorer.result.value, 6)
|
||||
const second = COMMANDS.move_cursor(i, code.indexOf('sum /*second*/'))
|
||||
assert_equal(second.value_explorer.result.value, 12)
|
||||
}),
|
||||
|
||||
test('let_versions scope', () => {
|
||||
assert_code_evals_to(`
|
||||
let x = 1
|
||||
let y = 1
|
||||
function change_x() {
|
||||
x = 2
|
||||
}
|
||||
function change_y() {
|
||||
y = 2
|
||||
}
|
||||
function unused() {
|
||||
return {}
|
||||
}
|
||||
if(false) {
|
||||
} else {
|
||||
if((change_y() || true) ? true : null) {
|
||||
const a = [...[{...{
|
||||
y: unused()[!(1 + (true ? {y: [change_x()]} : null))]
|
||||
}}]]
|
||||
}
|
||||
}
|
||||
{x,y} /*result*/
|
||||
`,
|
||||
{x: 2, y: 2}
|
||||
)
|
||||
}),
|
||||
|
||||
test('let_versions expr', () => {
|
||||
assert_code_evals_to(`
|
||||
let x = 0
|
||||
function inc() {
|
||||
x = x + 1
|
||||
return 0
|
||||
}
|
||||
x + inc() + x + inc() + x
|
||||
`,
|
||||
3
|
||||
)
|
||||
}),
|
||||
|
||||
test('let_versions update in assignment', () => {
|
||||
assert_code_evals_to(`
|
||||
let x
|
||||
function set(value) {
|
||||
x = 1
|
||||
return 0
|
||||
}
|
||||
x = set()
|
||||
x
|
||||
`,
|
||||
0
|
||||
)
|
||||
}),
|
||||
|
||||
test('let_versions update in assignment closed', () => {
|
||||
const code = `
|
||||
function test() {
|
||||
let x
|
||||
function set(value) {
|
||||
x = 1
|
||||
return 0
|
||||
}
|
||||
x = set()
|
||||
return x
|
||||
}
|
||||
test()
|
||||
`
|
||||
const i = test_initial_state(code, code.indexOf('return x'))
|
||||
assert_equal(i.value_explorer.result.value, 0)
|
||||
}),
|
||||
|
||||
test('let_versions multiple vars with same name', () => {
|
||||
const code = `
|
||||
let x
|
||||
function x_1() {
|
||||
x = 1
|
||||
}
|
||||
if(true) {
|
||||
let x = 0
|
||||
function x_2() {
|
||||
x = 2
|
||||
}
|
||||
x /* result 0 */
|
||||
x_1()
|
||||
x /* result 1 */
|
||||
x_2()
|
||||
x /* result 2 */
|
||||
}
|
||||
`
|
||||
const i = test_initial_state(code, code.indexOf('x /* result 0 */'))
|
||||
const frame = active_frame(i)
|
||||
const result_0 = find_node(frame, n => n.index == code.indexOf('x /* result 0 */')).result
|
||||
assert_equal(result_0.value, 0)
|
||||
const result_1 = find_node(frame, n => n.index == code.indexOf('x /* result 1 */')).result
|
||||
assert_equal(result_1.value, 0)
|
||||
const result_2 = find_node(frame, n => n.index == code.indexOf('x /* result 2 */')).result
|
||||
assert_equal(result_2.value, 2)
|
||||
}),
|
||||
|
||||
test('let_versions closed let vars bug', () => {
|
||||
const code = `
|
||||
let x = 0
|
||||
function inc() {
|
||||
x = x + 1
|
||||
}
|
||||
function test() {
|
||||
inc()
|
||||
x /*x*/
|
||||
}
|
||||
test()
|
||||
`
|
||||
const i = test_initial_state(code, code.indexOf('x /*x*/'))
|
||||
assert_equal(i.value_explorer.result.value, 1)
|
||||
}),
|
||||
|
||||
test('let_versions assign and read variable multiple times within call', () => {
|
||||
const code = `
|
||||
let x;
|
||||
(() => {
|
||||
x = 1
|
||||
console.log(x)
|
||||
x = 2
|
||||
console.log(x)
|
||||
})()
|
||||
`
|
||||
}),
|
||||
|
||||
test('let_versions let assigned undefined bug', () => {
|
||||
const code = `
|
||||
let x = 1
|
||||
function set(value) {
|
||||
x = value
|
||||
}
|
||||
set(2)
|
||||
set(undefined)
|
||||
x /*x*/
|
||||
`
|
||||
const i = test_initial_state(code, code.indexOf('x /*x*/'))
|
||||
assert_equal(i.value_explorer.result.value, undefined)
|
||||
}),
|
||||
|
||||
// TODO function args should have multiple versions same as let vars
|
||||
|
||||
// test('let_versions function args closure', () => {
|
||||
// const code = `
|
||||
// (function(x) {
|
||||
// function y() {
|
||||
// x /*x*/
|
||||
// }
|
||||
// y()
|
||||
// x = 1
|
||||
// y()
|
||||
// })(0)
|
||||
// `
|
||||
// const i = test_initial_state(code)
|
||||
// const second_y_call = root_calltree_node(i).children[0].children[1]
|
||||
// const selected = COMMANDS.calltree.click(i, second_y_call.id)
|
||||
// const moved = COMMANDS.move_cursor(selected, code.indexOf('x /*x*/'))
|
||||
// assert_equal(moved.value_explorer.result.value, 1)
|
||||
// }),
|
||||
|
||||
test('let_versions async/await', async () => {
|
||||
const code = `
|
||||
let x
|
||||
function set(value) {
|
||||
x = value
|
||||
}
|
||||
await set(1)
|
||||
x /*x*/
|
||||
`
|
||||
const i = await test_initial_state_async(code, code.indexOf('x /*x*/'))
|
||||
assert_equal(i.value_explorer.result.value, 1)
|
||||
}),
|
||||
|
||||
test('let_versions async/await 2', async () => {
|
||||
const code = `
|
||||
let x
|
||||
function set(value) {
|
||||
x = value
|
||||
Promise.resolve().then(() => {
|
||||
x = 10
|
||||
})
|
||||
}
|
||||
await set(1)
|
||||
x /*x*/
|
||||
`
|
||||
const i = await test_initial_state_async(code, code.indexOf('x /*x*/'))
|
||||
assert_equal(i.value_explorer.result.value, 10)
|
||||
}),
|
||||
|
||||
// Test that expand_calltree_node produces correct id for expanded nodes
|
||||
test('let_versions native call', () => {
|
||||
const code = `
|
||||
function x() {}
|
||||
[1,2].map(x)
|
||||
[1,2].map(x)
|
||||
`
|
||||
const i = test_initial_state(code)
|
||||
const second_map_call = i.calltree.children[0].children[1]
|
||||
assert_equal(second_map_call.has_more_children, true)
|
||||
const expanded = COMMANDS.calltree.click(i, second_map_call.id)
|
||||
const second_map_call_exp = expanded.calltree.children[0].children[1]
|
||||
assert_equal(second_map_call.id == second_map_call_exp.id, true)
|
||||
assert_equal(second_map_call_exp.children[0].id == second_map_call_exp.id + 1, true)
|
||||
}),
|
||||
|
||||
test('let_versions expand twice', () => {
|
||||
const code = `
|
||||
function test() {
|
||||
let x = 0
|
||||
function test2() {
|
||||
function foo() {
|
||||
x /*x*/
|
||||
}
|
||||
x = x + 1
|
||||
foo()
|
||||
}
|
||||
test2()
|
||||
}
|
||||
test()
|
||||
test()
|
||||
`
|
||||
const i = test_initial_state(code)
|
||||
const test_call = root_calltree_node(i).children[1]
|
||||
assert_equal(test_call.has_more_children , true)
|
||||
|
||||
const expanded = COMMANDS.calltree.click(i, test_call.id)
|
||||
const test2_call = root_calltree_node(expanded).children[1].children[0]
|
||||
assert_equal(test2_call.has_more_children, true)
|
||||
|
||||
const expanded2 = COMMANDS.calltree.click(expanded, test2_call.id)
|
||||
const foo_call = root_calltree_node(expanded2).children[1].children[0].children[0]
|
||||
|
||||
const expanded3 = COMMANDS.calltree.click(expanded2, foo_call.id)
|
||||
|
||||
const moved = COMMANDS.move_cursor(expanded3, code.indexOf('x /*x*/'))
|
||||
assert_equal(moved.value_explorer.result.value, 1)
|
||||
}),
|
||||
|
||||
test('let_versions deferred calls', () => {
|
||||
const code = `
|
||||
let x = 0
|
||||
export const inc = () => {
|
||||
return do_inc()
|
||||
}
|
||||
const do_inc = () => {
|
||||
x = x + 1
|
||||
return x
|
||||
}
|
||||
inc()
|
||||
`
|
||||
|
||||
const {state: i, on_deferred_call} = test_deferred_calls_state(code)
|
||||
|
||||
// Make deferred call
|
||||
i.modules[''].inc()
|
||||
|
||||
const state = on_deferred_call(i)
|
||||
const call = get_deferred_calls(state)[0]
|
||||
assert_equal(call.has_more_children, true)
|
||||
assert_equal(call.value, 2)
|
||||
|
||||
// Expand call
|
||||
// first arrow rights selects do_inc call, second steps into it
|
||||
const expanded = COMMANDS.calltree.arrow_right(
|
||||
COMMANDS.calltree.arrow_right(
|
||||
COMMANDS.calltree.click(state, call.id)
|
||||
)
|
||||
)
|
||||
// Move cursor
|
||||
const moved = COMMANDS.move_cursor(expanded, code.indexOf('return x'))
|
||||
assert_equal(moved.value_explorer.result.value, 2)
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user