Preserve redo log for mutable objects

Replay it during time travel debugging
This commit is contained in:
Dmitry Vasilev
2024-01-01 18:33:46 +08:00
parent acd24fe5b7
commit 2830a160af
23 changed files with 1575 additions and 280 deletions

View File

@@ -1,6 +1,6 @@
# Leporello.js
Leporello.js is an interactive functional programming environment for JavaScript.
Leporello.js is an interactive JavaScript environment with a time-travel debugger
[<img src="docs/images/video_cover.png" width="600px">](https://vimeo.com/845773267)
@@ -13,12 +13,9 @@ Support us on [Github Sponsors](https://github.com/sponsors/leporello-js) and be
## Features
### Interactive
### Going beyond the REPL
Your code is executed instantly as you type, with the results displayed next to it. No need to set breakpoints for debugging. Just move the cursor to any line and see what's happening.
### Designed for functional programming
Supercharge your functional code with unprecedented developer tooling.
### Next level debugging capabilities
Visualise and navigate a dynamic call graph of your program in a time-travel manner.
@@ -41,7 +38,7 @@ The `if` / `else` statements can only contain blocks of code and not single stat
Both traditional functions and arrow functions, with block bodies and concise bodies, are supported. Method definitions, however, are not currently supported.
Classes are not supported at the moment. There is a possibility of supporting some form of immutable classes in the future. The `this` keyword is not currently supported, but the `new` operator can be used for instantiating built-in classes.
Classes are not supported at the moment. The `this` keyword is not currently supported. The `new` operator can be used for instantiating built-in classes or classes imported from [third-party](#importing-third-party-libs) libs.
`switch` statements will be supported in future updates.
@@ -61,26 +58,26 @@ Some operators are not supported at the moment, including:
- `in`
- `void`
- Comma operator
Certain operators that are not supported are by design, as they are not purely functional. These include:
- Increment and decrement
- `delete`
## Importing third-party libs
Sometimes you want to import third party library that uses imperative language constructs. You may want to use it to perform side-effects or maybe it mutates data inside but still provides functional interface (does not mutate function arguments). Good example of such library is [bignumber.js](https://github.com/MikeMcl/bignumber.js/) - it makes a lot of mutating assignments inside, but `BigNumber` instances are immutable.
To use `bignumber.js` you add an `external pragma` before the import:
To enable its comprehensive functionalities, Leporello.js parses and instruments your source code. Should you wish to import a module as a black box, preventing the ability to step into its functions, you can utilize the `external` pragma. For instance:
```
/* external */
import BigNumber from './path/to/bignumber.mjs';
import {Foo} from './path/to/foo.js';
```
`external pragma` is just a comment that contains only the literal string `external` (both styles for comments and extra whitespaces are allowed). Now the module is imported as a black box - you cannot debug `BigNumber` methods.
`external` pragma is just a comment that contains only the literal string `external` (both styles for comments and extra whitespaces are allowed).
If a module path is a non-local path including a protocol and a host then it is always imported as an external module. Example:
![External import](docs/images/external_import.png)
Now the module is imported as a black box - you cannot debug `BigNumber` methods.
Currently every external is loaded once and cached until Leporello is restarted
(TODO change path to modules every time it changed on disk, since modules are
served from service workers).
@@ -221,6 +218,5 @@ run tests in leporello itself:
## Roadmap
* Use production level JS parser, probably TypeScript parser (so it will be
possible to program in pure functional subset of TypeScript)
* Use production level JS parser, probably TypeScript parser
* Implement VSCode plugin

View File

@@ -105,7 +105,10 @@ export const find_versioned_let_vars = (node, current_fn = node) => {
} 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 =>
const next_assigned_vars = node
.children
.filter(c => c.type == 'decl_pair')
.flatMap(decl_pair =>
collect_destructuring_identifiers(decl_pair).map(id => {
if(id.definition.index == null) {
throw new Error('illegal state')

View File

@@ -5,6 +5,10 @@ import {find_node, find_leaf, ancestry_inc} from './ast_utils.js'
import {color} from './color.js'
import {eval_frame, eval_expand_calltree_node, get_after_if_path} from './eval.js'
export const version_number_symbol = Symbol('version_number')
export const is_versioned_object = o => o?.[version_number_symbol] != null
export const get_version_number = o => o[version_number_symbol]
export const pp_calltree = tree => ({
id: tree.id,
ok: tree.ok,
@@ -148,7 +152,7 @@ export const add_frame = (
let frame
frame = state.frames?.[active_calltree_node.id]
if(frame == null) {
frame = update_children(eval_frame(active_calltree_node, state.modules))
frame = update_children(eval_frame(active_calltree_node, state.modules, state.rt_cxt))
const execution_paths = active_calltree_node.toplevel
? null
: get_execution_paths(frame)
@@ -304,37 +308,45 @@ const jump_calltree_node = (_state, _current_calltree_node) => {
)
let value_explorer
if(next.current_calltree_node.toplevel) {
value_explorer = null
} else {
const args = show_body
? active_frame(with_selected_calltree_node)
// function args node
.children[0]
.result
.value
: current_calltree_node.args
const _arguments = {
value:
show_body
? active_frame(with_selected_calltree_node)
// function args node
.children[0]
.result
.value
: current_calltree_node.args,
[version_number_symbol]: current_calltree_node.version_number,
}
value_explorer = {
index: loc.index,
result: {
ok: true,
is_calltree_node_explorer: true,
value: current_calltree_node.ok
? {
'*arguments*': args,
'*return*': current_calltree_node.value,
? {
'*arguments*': _arguments,
'*return*': {
value: current_calltree_node.value,
[version_number_symbol]: current_calltree_node.last_version_number,
}
}
: {
'*arguments*': args,
'*throws*': current_calltree_node.error,
'*arguments*': _arguments,
'*throws*': {
value: current_calltree_node.error,
[version_number_symbol]: current_calltree_node.last_version_number,
}
}
}
}
}
return {...with_selected_calltree_node,
return {...with_selected_calltree_node,
value_explorer,
selection_state: show_body
? null

View File

@@ -18,6 +18,9 @@ import {
is_native_fn,
} from './calltree.js'
// external
import {with_version_number} from './runtime/runtime.js'
const collect_logs = (logs, call) => {
const id_to_log = new Map(
collect_nodes_with_parents(call, n => n.is_log)
@@ -26,6 +29,7 @@ const collect_logs = (logs, call) => {
node.id,
{
id: node.id,
version_number: node.version_number,
toplevel: parent.toplevel,
module: parent.toplevel
? parent.module
@@ -40,6 +44,9 @@ const collect_logs = (logs, call) => {
return logs.map(l => id_to_log.get(l.id))
}
export const with_version_number_of_log = (state, log_item, action) =>
with_version_number(state.rt_cxt, log_item.version_number, action)
const apply_eval_result = (state, eval_result) => {
// TODO what if console.log called from native fn (like Array::map)?
// Currently it is not recorded. Maybe we should monkey patch `console`?
@@ -619,38 +626,53 @@ const get_stmt_value_explorer = (state, stmt) => {
if(stmt.type == 'return') {
result = stmt.children[0].result
} else if(['let', 'const', 'assignment'].includes(stmt.type)) {
const identifiers = stmt
.children
.flatMap(
collect_destructuring_identifiers
)
.filter(id => id.result != null)
.map(id => [id.value, id.result.value])
let value
if(
stmt.children.length == 1
&&
(
stmt.children[0].type == 'identifier'
||
stmt.children[0].type == 'decl_pair'
&&
stmt.children[0].name_node.type == 'identifier'
)
) {
// Just a single declaration
if(identifiers.length != 1) {
throw new Error('illegal state')
}
value = identifiers[0][1]
} else {
value = Object.fromEntries(identifiers)
}
return {
index: stmt.index,
length: stmt.length,
result: {ok: true, value},
if(stmt.children.find(c => c.type == 'assignment_pair') != null) {
if(stmt.children.length != 1) {
// Multiple assignments, not clear what value to show in value
// explorer, show nothing
return null
}
// get result of first assignment
result = stmt.children[0].result
} else {
const identifiers = stmt
.children
.flatMap(
collect_destructuring_identifiers
)
.filter(id => id.result != null)
.map(id => [id.value, id.result.value])
let value
if(
stmt.children.length == 1
&&
(
stmt.children[0].type == 'identifier'
||
stmt.children[0].type == 'decl_pair'
&&
stmt.children[0].name_node.type == 'identifier'
)
) {
// Just a single declaration
if(identifiers.length != 1) {
throw new Error('illegal state')
}
value = identifiers[0][1]
} else {
value = Object.fromEntries(identifiers)
}
// TODO different identifiers may have different version_number,
// because there may be function calls and assignments in between fix
// it
const version_number = stmt.children[0].result.version_number
return {
index: stmt.index,
length: stmt.length,
result: {ok: true, value, version_number},
}
}
} else if(stmt.type == 'if'){
return null
@@ -658,6 +680,9 @@ const get_stmt_value_explorer = (state, stmt) => {
result = {
ok: true,
value: state.modules[stmt.full_import_path],
// For imports, we show version for the moment of module toplevel
// starts execution
version_number: state.active_calltree_node.version_number,
}
} else if (stmt.type == 'export') {
return get_stmt_value_explorer(state, stmt.children[0])
@@ -766,7 +791,16 @@ const get_value_explorer = (state, index) => {
}
const do_move_cursor = (state, index) => {
return { ...state, value_explorer: get_value_explorer(state, index) }
const value_explorer = get_value_explorer(state, index)
if(
value_explorer != null
&& value_explorer.result.ok
&& value_explorer.result.version_number == null
) {
console.error('no version_number found', value_explorer)
throw new Error('illegal state')
}
return { ...state, value_explorer}
}
const move_cursor = (s, index) => {

View File

@@ -2,6 +2,7 @@ import {exec} from '../index.js'
import {el, stringify, fn_link, scrollIntoViewIfNeeded} from './domutils.js'
import {stringify_for_header} from '../value_explorer_utils.js'
import {find_node} from '../ast_utils.js'
import {with_version_number} from '../runtime/runtime.js'
import {is_expandable, root_calltree_node, get_deferred_calls, has_error}
from '../calltree.js'
@@ -104,15 +105,23 @@ export class CallTree {
n.fn.name,
'(' ,
...join(
n.args.map(
a => typeof(a) == 'function'
? fn_link(a)
: stringify_for_header(a)
// for arguments, use n.version_number - last version before call
with_version_number(this.state.rt_cxt, n.version_number, () =>
n.args.map(
a => typeof(a) == 'function'
? fn_link(a)
: stringify_for_header(a)
)
)
),
')' ,
// TODO: show error message only where it was thrown, not every frame?
': ', (n.ok ? stringify_for_header(n.value) : stringify_for_header(n.error))
': ',
// for return value, use n.last_version_number - last version that was
// created during call
with_version_number(this.state.rt_cxt, n.last_version_number, () =>
(n.ok ? stringify_for_header(n.value) : stringify_for_header(n.error))
)
),
),
(n.children == null || !is_expanded)

View File

@@ -2,6 +2,7 @@ import {exec, get_state} from '../index.js'
import {ValueExplorer} from './value_explorer.js'
import {stringify_for_header} from '../value_explorer_utils.js'
import {el, stringify, fn_link} from './domutils.js'
import {version_number_symbol} from '../calltree.js'
/*
normalize events 'change' and 'changeSelection':
@@ -224,7 +225,21 @@ export class Editor {
}
}
embed_value_explorer({node, index, length, result: {ok, value, error}}) {
embed_value_explorer(
state,
{
node,
index,
length,
result: {
ok,
value,
error,
version_number,
is_calltree_node_explorer
}
}
) {
this.unembed_value_explorer()
const session = this.ace_editor.getSession()
@@ -304,8 +319,22 @@ export class Editor {
}
},
})
if(is_calltree_node_explorer) {
exp.render(state, value, {
is_expanded: true,
children: {
'*arguments*': {is_expanded: true},
}
})
} else {
exp.render(
state,
{value, [version_number_symbol]: version_number},
{is_expanded: true},
)
}
exp.render(value)
}
} else {
is_dom_el = false

View File

@@ -1,6 +1,7 @@
import {el, scrollIntoViewIfNeeded} from './domutils.js'
import {exec} from '../index.js'
import {header} from '../value_explorer_utils.js'
import {with_version_number_of_log} from '../cmd.js'
export class Logs {
constructor(ui, el) {
@@ -36,12 +37,12 @@ export class Logs {
})
}
rerender_logs(logs) {
rerender_logs(state, logs) {
this.el.innerHTML = ''
this.render_logs(null, logs)
this.render_logs(state, null, logs)
}
render_logs(prev_logs, logs) {
render_logs(state, prev_logs, logs) {
for(
let i = prev_logs == null ? 0 : prev_logs.logs.length ;
i < logs.logs.length;
@@ -71,8 +72,10 @@ export class Logs {
+ ':'
),
' ',
// TODO fn_link, for function args, like in ./calltree.js
log.args.map(a => header(a)).join(', ')
with_version_number_of_log(state, log, () =>
// TODO fn_link, for function args, like in ./calltree.js
log.args.map(a => header(a)).join(', ')
)
)
)
}

View File

@@ -234,7 +234,7 @@ export class UI {
this.debugger_loaded.style = ''
this.calltree.render_calltree(state)
this.logs.render_logs(null, state.logs)
this.logs.render_logs(state, null, state.logs)
}
render_io_trace(state) {

View File

@@ -5,15 +5,30 @@
import {el, stringify, scrollIntoViewIfNeeded} from './domutils.js'
import {with_code_execution} from '../index.js'
import {header, is_expandable, displayed_entries} from '../value_explorer_utils.js'
// TODO remove is_expandble, join with displayed entries
import {header, short_header, is_expandable, displayed_entries} from '../value_explorer_utils.js'
import {with_version_number} from '../runtime/runtime.js'
import {is_versioned_object, get_version_number} from '../calltree.js'
const get_value_by_path = (o, path) => {
if(path.length == 0) {
return o
} else {
const node_props_by_path = (state, o, path) => {
if(is_versioned_object(o)) {
return with_version_number(
state.rt_cxt,
get_version_number(o),
() => node_props_by_path(state, o.value, path),
)
}
if(path.length != 0) {
const [start, ...rest] = path
return get_value_by_path(o[start], rest)
const value = displayed_entries(o).find(([k,v]) => k == start)[1]
return node_props_by_path(state, value, rest)
} else {
return {
displayed_entries: displayed_entries(o),
header: header(o),
short_header: short_header(o),
is_exp: is_expandable(o),
}
}
}
@@ -52,15 +67,15 @@ export class ValueExplorer {
return
}
const current_object = get_value_by_path(this.value, this.current_path)
const current_node = node_props_by_path(this.state, this.value, this.current_path)
if(e.key == 'ArrowDown' || e.key == 'j'){
// Do not scroll
e.preventDefault()
if(is_expandable(current_object) && this.is_expanded(this.current_path)) {
if(current_node.is_exp && this.is_expanded(this.current_path)) {
this.select_path(this.current_path.concat(
displayed_entries(current_object)[0][0]
current_node.displayed_entries[0][0]
))
} else {
const next = p => {
@@ -68,7 +83,8 @@ export class ValueExplorer {
return null
}
const parent = p.slice(0, p.length - 1)
const children = displayed_entries(get_value_by_path(this.value, parent))
const children = node_props_by_path(this.state, this.value, parent)
.displayed_entries
const child_index = children.findIndex(([k,v]) =>
k == p[p.length - 1]
)
@@ -96,7 +112,7 @@ export class ValueExplorer {
return
}
const parent = this.current_path.slice(0, this.current_path.length - 1)
const children = displayed_entries(get_value_by_path(this.value, parent))
const children = node_props_by_path(this.state, this.value, parent).displayed_entries
const child_index = children.findIndex(([k,v]) =>
k == this.current_path[this.current_path.length - 1]
)
@@ -105,10 +121,12 @@ export class ValueExplorer {
this.select_path(parent)
} else {
const last = p => {
if(!is_expandable(get_value_by_path(this.value, p)) || !this.is_expanded(p)) {
const node_props = node_props_by_path(this.state, this.value, p)
if(!node_props.is_exp || !this.is_expanded(p)) {
return p
} else {
const children = displayed_entries(get_value_by_path(this.value, p))
const children = node_props
.displayed_entries
.map(([k,v]) => k)
return last([...p, children[children.length - 1]])
@@ -123,7 +141,7 @@ export class ValueExplorer {
e.preventDefault()
const is_expanded = this.is_expanded(this.current_path)
if(!is_expandable(current_object) || !is_expanded) {
if(!current_node.is_exp || !is_expanded) {
if(this.current_path.length != 0) {
const parent = this.current_path.slice(0, this.current_path.length - 1)
this.select_path(parent)
@@ -139,12 +157,13 @@ export class ValueExplorer {
// Do not scroll
e.preventDefault()
if(is_expandable(current_object)) {
if(current_node.is_exp) {
const is_expanded = this.is_expanded(this.current_path)
if(!is_expanded) {
this.toggle_expanded()
} else {
const children = displayed_entries(get_value_by_path(this.value, this.current_path))
const children = node_props_by_path(this.state, this.value, this.current_path)
.displayed_entries
this.select_path(
[
...this.current_path,
@@ -175,11 +194,12 @@ export class ValueExplorer {
this.toggle_expanded()
}
render(value) {
this.node_data = {is_expanded: true}
render(state, value, node_data) {
this.state = state
this.value = value
this.node_data = node_data
const path = []
this.container.appendChild(this.render_value_explorer_node(null, value, path, this.node_data))
this.container.appendChild(this.render_value_explorer_node(path, this.node_data))
this.select_path(path)
}
@@ -217,11 +237,7 @@ export class ValueExplorer {
const data = this.get_node_data(this.current_path)
data.is_expanded = fn(data.is_expanded)
const prev_dom_node = data.el
const key = this.current_path.length == 0
? null
: this.current_path[this.current_path.length - 1]
const value = get_value_by_path(this.value, this.current_path)
const next = this.render_value_explorer_node(key, value, this.current_path, data)
const next = this.render_value_explorer_node(this.current_path, data)
prev_dom_node.parentNode.replaceChild(next, prev_dom_node)
}
@@ -230,18 +246,23 @@ export class ValueExplorer {
this.set_active(this.current_path, true)
}
render_value_explorer_node(...args) {
return with_code_execution(() => {
return this.do_render_value_explorer_node(...args)
})
render_value_explorer_node(path, node_data) {
return with_code_execution(() => (
this.do_render_value_explorer_node(path, node_data)
), this.state)
}
do_render_value_explorer_node(key, value, path, node_data) {
do_render_value_explorer_node(path, node_data) {
const key = path.length == 0
? null
: path[path.length - 1]
const {displayed_entries, header, short_header, is_exp}
= node_props_by_path(this.state, this.value, path)
const is_exp = is_expandable(value)
const is_expanded = is_exp && node_data.is_expanded
node_data.children = {}
node_data.children ??= {}
const result = el('div', 'value_explorer_node',
@@ -259,17 +280,17 @@ export class ValueExplorer {
key == null || !is_exp || !is_expanded
// Full header
? header(value)
? header
// Short header
: Array.isArray(value)
? 'Array(' + value.length + ')'
: ''
: key == '*arguments*'
? ''
: short_header
),
(is_exp && is_expanded)
? displayed_entries(value).map(([k,v]) => {
node_data.children[k] = {}
return this.render_value_explorer_node(k, v, [...path, k], node_data.children[k])
? displayed_entries.map(([k,v]) => {
node_data.children[k] ??= {}
return this.do_render_value_explorer_node([...path, k], node_data.children[k])
})
: []
)

6
src/effects.js vendored
View File

@@ -218,7 +218,7 @@ export const apply_side_effects = (prev, next, ui) => {
ui.render_debugger(next)
clear_coloring(ui)
render_coloring(ui, next)
ui.logs.rerender_logs(next.logs)
ui.logs.rerender_logs(next, next.logs)
if(
prev.io_trace != next.io_trace
@@ -253,7 +253,7 @@ export const apply_side_effects = (prev, next, ui) => {
render_coloring(ui, next)
}
ui.logs.render_logs(prev.logs, next.logs)
ui.logs.render_logs(next, prev.logs, next.logs)
}
}
@@ -280,7 +280,7 @@ export const apply_side_effects = (prev, next, ui) => {
if(next.value_explorer == null) {
ui.editor.unembed_value_explorer()
} else {
ui.editor.embed_value_explorer(next.value_explorer)
ui.editor.embed_value_explorer(next, next.value_explorer)
}
}
}

View File

@@ -15,10 +15,14 @@ import {
} from './ast_utils.js'
import {has_toplevel_await} from './find_definitions.js'
// external
import {with_version_number} from './runtime/runtime.js'
// import runtime as external because it has non-functional code
// external
import {run, do_eval_expand_calltree_node, LetMultiversion} from './runtime/runtime.js'
import {run, do_eval_expand_calltree_node} from './runtime/runtime.js'
// external
import {LetMultiversion} from './runtime/let_multiversion.js'
// TODO: fix error messages. For example, "__fn is not a function"
@@ -73,7 +77,7 @@ const codegen_function_expr = (node, node_cxt) => {
? `(${args}) => `
: `function(${args})`
// TODO gensym __obj, __fn, __call_id, __let_vars
// TODO gensym __obj, __fn, __call_id, __let_vars, __literals
const prolog =
'{const __call_id = __cxt.call_counter;'
+ (
@@ -81,6 +85,7 @@ const codegen_function_expr = (node, node_cxt) => {
? 'const __let_vars = __cxt.let_vars;'
: ''
)
+ 'const __literals = __cxt.literals;';
const call = (node.is_async ? 'async ' : '') + decl + (
(node.body.type == 'do')
@@ -206,7 +211,9 @@ const codegen = (node, node_cxt) => {
`__save_ct_node_for_path(__cxt, __calltree_node_by_loc, `
+ `${get_after_if_path(node)}, __call_id);`
} else if(node.type == 'array_literal'){
return '[' + node.elements.map(c => do_codegen(c)).join(', ') + ']'
return '__create_array(['
+ node.elements.map(c => do_codegen(c)).join(', ')
+ `], __cxt, ${node.index}, __literals)`
} else if(node.type == 'object_literal'){
const elements =
node.elements.map(el => {
@@ -223,7 +230,7 @@ const codegen = (node, node_cxt) => {
}
})
.join(',')
return '({' + elements + '})'
return `__create_object({${elements}}, __cxt, ${node.index}, __literals)`
} else if(node.type == 'function_call'){
return codegen_function_call(node, node_cxt)
} else if(node.type == 'function_expr'){
@@ -278,7 +285,7 @@ const codegen = (node, node_cxt) => {
} // Otherwise goes to the end of the func
}
if(node.type == 'assignment') {
if(node.type == 'assignment' && c.children[0].type != 'member_access') {
const [lefthand, righthand] = c.children
if(lefthand.type != 'identifier') {
// TODO
@@ -406,7 +413,7 @@ export const eval_modules = (
location
) => {
// TODO gensym __cxt, __trace, __trace_call, __calltree_node_by_loc,
// __do_await, __Multiversion
// __do_await, __Multiversion, __create_array, __create_object
// TODO bug if module imported twice, once as external and as regular
@@ -428,15 +435,19 @@ export const eval_modules = (
module,
// TODO refactor, instead of multiple args prefixed with '__', pass
// single arg called `runtime`
fn: new Function(
'__cxt',
'__let_vars',
'__literals',
'__calltree_node_by_loc',
'__trace',
'__trace_call',
'__do_await',
'__save_ct_node_for_path',
'__Multiversion',
'__create_array',
'__create_object',
/* Add dummy __call_id for toplevel. It does not make any sence
* (toplevel is executed only once unlike function), we only add it
@@ -695,7 +706,14 @@ const do_eval_frame_expr = (node, eval_cxt, frame_cxt) => {
'computed_property'
].includes(node.type)) {
return eval_children(node, eval_cxt, frame_cxt)
} else if(node.type == 'array_literal' || node.type == 'call_args'){
} else if(node.type == 'array_literal'){
const {ok, children, eval_cxt: next_eval_cxt} = eval_children(node, eval_cxt, frame_cxt)
if(!ok) {
return {ok, children, eval_cxt: next_eval_cxt}
}
const value = frame_cxt.calltree_node.literals.get(node.index)
return {ok, children, value, eval_cxt: next_eval_cxt}
} else if(node.type == 'call_args'){
const {ok, children, eval_cxt: next_eval_cxt} = eval_children(node, eval_cxt, frame_cxt)
if(!ok) {
return {ok, children, eval_cxt: next_eval_cxt}
@@ -716,31 +734,7 @@ const do_eval_frame_expr = (node, eval_cxt, frame_cxt) => {
if(!ok) {
return {ok, children, eval_cxt: next_eval_cxt}
}
const value = children.reduce(
(value, el) => {
if(el.type == 'object_spread'){
return {...value, ...el.children[0].result.value}
} else if(el.type == 'identifier') {
// TODO check that it works
return {...value, ...{[el.value]: el.result.value}}
} else if(el.type == 'key_value_pair') {
const [key, val] = el.children
let k
if(key.type == 'computed_property') {
k = key.children[0].result.value
} else {
k = key.result.value
}
return {
...value,
...{[k]: val.result.value},
}
} else {
throw new Error('unknown node type ' + el.type)
}
},
{}
)
const value = frame_cxt.calltree_node.literals.get(node.index)
return {ok, children, value, eval_cxt: next_eval_cxt}
} else if(node.type == 'function_call' || node.type == 'new'){
const {ok, children, eval_cxt: next_eval_cxt} = eval_children(node, eval_cxt, frame_cxt)
@@ -762,6 +756,10 @@ const do_eval_frame_expr = (node, eval_cxt, frame_cxt) => {
throw new Error('illegal state')
}
// TODO refactor. let vars in scope should be Multiversions, not values
// Unwrap them to values inside idenitifer handler
// So here should be no logic for extracting Multiversion values
const closure = frame_cxt.calltree_node.fn?.__closure
const closure_let_vars = closure == null
? null
@@ -791,6 +789,7 @@ const do_eval_frame_expr = (node, eval_cxt, frame_cxt) => {
...next_eval_cxt,
call_index: next_eval_cxt.call_index + 1,
scope: {...next_eval_cxt.scope, ...updated_let_scope},
version_number: call.last_version_number,
},
}
}
@@ -841,6 +840,8 @@ const do_eval_frame_expr = (node, eval_cxt, frame_cxt) => {
}
}
} else if(node.type == 'member_access'){
// TODO prop should only be evaluated if obj is not null, if optional
// chaining
const {ok, children, eval_cxt: next_eval_cxt} = eval_children(node, eval_cxt, frame_cxt)
if(!ok) {
return {ok: false, children, eval_cxt: next_eval_cxt}
@@ -848,18 +849,35 @@ const do_eval_frame_expr = (node, eval_cxt, frame_cxt) => {
const [obj, prop] = children
const codestring = node.is_optional_chaining ? 'obj?.[prop]' : 'obj[prop]'
// TODO do not use eval here
let value
if(obj.result.value == null) {
if(!node.is_optional_chaining) {
return {
ok: false,
error: new TypeError('Cannot read properties of '
+ obj.result.value
+ ` (reading '${prop.result.value}')`
),
is_error_origin: true,
children,
eval_cxt: next_eval_cxt,
}
} else {
value = undefined
}
} else {
value = with_version_number(frame_cxt.rt_cxt, obj.result.version_number, () =>
obj.result.value[prop.result.value]
)
}
return {
...eval_codestring(codestring, {
obj: obj.result.value,
prop: prop.result.value,
}),
ok: true,
value,
children,
eval_cxt: next_eval_cxt,
}
} else if(node.type == 'unary') {
const {ok, children, eval_cxt: next_eval_cxt} = eval_children(node, eval_cxt, frame_cxt)
if(!ok) {
@@ -968,21 +986,26 @@ const eval_children = (node, eval_cxt, frame_cxt) => {
const eval_frame_expr = (node, eval_cxt, frame_cxt) => {
const {ok, error, is_error_origin, value, call, children, eval_cxt: next_eval_cxt}
= do_eval_frame_expr(node, eval_cxt, frame_cxt)
return {
node: {
...node,
children,
// Add `call` for step_into
result: {ok, error, value, call, is_error_origin}
result: {
ok,
error,
value,
call,
is_error_origin,
version_number: next_eval_cxt.version_number,
}
},
eval_cxt: next_eval_cxt,
}
}
const eval_decl_pair = (s, eval_cxt, frame_cxt) => {
if(s.type != 'decl_pair') {
throw new Error('illegal state')
}
// TODO default values for destructuring can be function calls
const {node, eval_cxt: next_eval_cxt}
@@ -1026,12 +1049,14 @@ const eval_decl_pair = (s, eval_cxt, frame_cxt) => {
ok,
error: ok ? null : error,
value: !ok ? null : next_scope[symbol_for_identifier(node, frame_cxt)],
version_number: next_eval_cxt.version_number,
}
})
),
n =>
// TODO this should set result for default values in destructuring
// Currently not implemented
// TODO version_number
n.result == null
? {...n, result: {ok}}
: n
@@ -1061,6 +1086,62 @@ const eval_decl_pair = (s, eval_cxt, frame_cxt) => {
}
}
const eval_assignment_pair = (s, eval_cxt, context) => {
const [lefthand, righthand] = s.children
if(lefthand.type != 'member_access') {
throw new Error('illegal state')
}
// TODO it also evals value of member access which we dont need. We should
// eval obj, prop, and then righthand
// TODO prop and righthand only evaluated if obj is not null???
const {node: lefthand_evaled, eval_cxt: lefthand_eval_cxt} =
eval_frame_expr(lefthand, eval_cxt, context)
if(!lefthand_evaled.result.ok) {
return {
ok: false,
eval_cxt: lefthand_eval_cxt,
node: {...s, result: {ok: false}, children: [lefthand_evaled, righthand]},
}
}
const {node: righthand_evaled, eval_cxt: righthand_eval_cxt} =
eval_frame_expr(righthand, lefthand_eval_cxt, context)
if(!righthand_evaled.result.ok) {
return {
ok: false,
eval_cxt: righthand_eval_cxt,
node: {
...s,
result: {ok: false},
children: [lefthand_evaled, righthand_evaled],
},
}
}
const next_version_number = righthand_eval_cxt.version_number + 1
return {
ok: true,
eval_cxt: {
...righthand_eval_cxt,
version_number: next_version_number,
},
node: {...s,
result: righthand_evaled.result,
children: [
{
...lefthand_evaled,
result: {
...righthand_evaled.result,
},
},
righthand_evaled,
]
},
}
}
const eval_statement = (s, eval_cxt, frame_cxt) => {
if(s.type == 'do') {
@@ -1147,19 +1228,28 @@ const eval_statement = (s, eval_cxt, frame_cxt) => {
return {ok, eval_cxt, children: [...children, s]}
}
if(stmt.type == 'let' && s.type == 'identifier') {
const node = {...s, result: {ok: true}}
const scope = {...eval_cxt.scope, [symbol_for_identifier(s, frame_cxt)]: undefined}
const value = undefined
const node = {...s, result: {ok: true, value, version_number: eval_cxt.version_number}}
const scope = {...eval_cxt.scope, [symbol_for_identifier(s, frame_cxt)]: value}
return {
ok,
children: [...children, node],
eval_cxt: {...eval_cxt, scope},
}
}
let result
if(s.type == 'decl_pair') {
result = eval_decl_pair(s, eval_cxt, frame_cxt)
} else if(s.type == 'assignment_pair') {
result = eval_assignment_pair(s, eval_cxt, frame_cxt)
} else {
throw new Error('illegal state')
}
const {
ok: next_ok,
node,
eval_cxt: next_eval_cxt,
} = eval_decl_pair(s, eval_cxt, frame_cxt)
} = result
return {
ok: next_ok,
eval_cxt: next_eval_cxt,
@@ -1201,7 +1291,10 @@ const eval_statement = (s, eval_cxt, frame_cxt) => {
ok: true,
value: imp.definition.is_default
? module['default']
: module[imp.value]
: module[imp.value],
// For imports, we show version for the moment of module toplevel
// starts execution
version_number: frame_cxt.calltree_node.version_number,
}
}
))
@@ -1319,14 +1412,32 @@ const check_eval_result = result => {
if(!result.eval_cxt.__eval_cxt_marker) {
throw new Error('illegal state')
}
// Commented for performance
// Check that every node with result has version_number property
//function check_version_number(node) {
// if(node.result != null && node.result.ok && node.result.value != null) {
// if(node.result.version_number == null) {
// console.error(node)
// throw new Error('bad node')
// }
// }
// if(node.children != null) {
// node.children.forEach(c => check_version_number(c))
// }
//}
//check_version_number(result.node)
}
export const eval_frame = (calltree_node, modules) => {
export const eval_frame = (calltree_node, modules, rt_cxt) => {
if(calltree_node.has_more_children) {
throw new Error('illegal state')
}
const node = calltree_node.code
const frame_cxt = {calltree_node, modules}
const frame_cxt = {calltree_node, modules, rt_cxt}
if(node.type == 'do') {
// eval module toplevel
const eval_result = eval_statement(
@@ -1335,6 +1446,7 @@ export const eval_frame = (calltree_node, modules) => {
__eval_cxt_marker: true,
scope: {},
call_index: 0,
version_number: calltree_node.version_number,
},
frame_cxt,
)
@@ -1350,11 +1462,17 @@ export const eval_frame = (calltree_node, modules) => {
? value.get_version(calltree_node.version_number)
: value
})
const args_scope_result = get_args_scope(
node,
calltree_node.args,
closure
)
const version_number = calltree_node.version_number
const args_scope_result = {
...get_args_scope(
node,
calltree_node.args,
closure
),
version_number,
}
// TODO fine-grained destructuring error, only for identifiers that
// failed destructuring
@@ -1371,10 +1489,14 @@ export const eval_frame = (calltree_node, modules) => {
error: args_scope_result.ok ? null : args_scope_result.error,
is_error_origin: !args_scope_result.ok,
value: !args_scope_result.ok ? null : args_scope_result.value[a.value],
version_number,
}
})
),
n => n.result == null
// TODO this should set result for default values in destructuring
// Currently not implemented
// TODO version_number
? {...n, result: {ok: args_scope_result.ok}}
: n
)
@@ -1407,6 +1529,7 @@ export const eval_frame = (calltree_node, modules) => {
__eval_cxt_marker: true,
scope,
call_index: 0,
version_number: calltree_node.version_number,
}
let eval_result

View File

@@ -314,6 +314,20 @@ const if_ok = (parser, fn) => cxt => {
}
}
const check_if_valid = (parser, check) => cxt => {
const result = parser(cxt)
if(!result.ok) {
return result
} else {
const {ok, error} = check(result.value)
if(!ok) {
return {ok: false, error, cxt}
} else {
return result
}
}
}
const if_fail = (parser, error) => cxt => {
const result = parser(cxt)
if(result.ok) {
@@ -760,7 +774,7 @@ const function_call_or_member_access = nested =>
}
)
),
// Function call
if_ok(
list(
@@ -1077,6 +1091,7 @@ const expr =
unary('-'),
unary('typeof'),
unary('await'),
// TODO 'delete' operator
binary(['**']),
binary(['*','/','%']),
binary(['+','-']),
@@ -1168,9 +1183,40 @@ const const_statement = const_or_let(true)
const let_declaration = const_or_let(false)
// TODO object assignment required braces, like ({foo} = {foo: 1})
// require assignment cannot start with '{' by not_followed_by
// TODO chaining assignment, like 'foo = bar = baz'
// TODO +=, *= etc
// TODO make assignment an expression, not a statement. Add comma operator to
// allow multiple assignments
const assignment = if_ok(
comma_separated_1(simple_decl_pair),
comma_separated_1(
either(
simple_decl_pair,
check_if_valid(
if_ok(
seq([
expr,
literal('='),
expr,
]),
({value: [lefthand, _, righthand], ...node}) => {
return {...node,
type: 'assignment_pair',
children: [lefthand, righthand],
}
}
),
(node) => {
const [lefthand, righthand] = node.children
if(lefthand.type != 'member_access' || lefthand.is_optional_chaining) {
return {ok: false, error: 'Invalid left-hand side in assignment'}
} else {
return {ok: true}
}
}
)
)
),
({value, ...node}) => ({
...node,
type: 'assignment',
@@ -1178,11 +1224,13 @@ const assignment = if_ok(
})
)
const return_statement =
if_ok(
seq_select(1, [
// We forbid bare return statement
// TODO bare return statement
// TODO implement bare return statement compatible with ASI
// see https://riptutorial.com/javascript/example/15248/rules-of-automatic-semicolon-insertion
// see ASI_restrited unit test
@@ -1464,6 +1512,8 @@ const update_children_not_rec = (node, children = node.children) => {
return {...node, is_statement: true }
} else if(node.type == 'decl_pair') {
return {...node, expr: children[1], name_node: children[0]}
} else if(node.type == 'assignment_pair') {
return {...node, children}
} else if(node.type == 'assignment'){
return {...node, is_statement: true}
} else if(node.type == 'do'){

142
src/runtime/array.js Normal file
View File

@@ -0,0 +1,142 @@
import {Multiversion, rollback_if_needed, wrap_methods, mutate} from './multiversion.js'
function set(prop, value) {
this[prop] = value
}
export const defineMultiversionArray = window => {
// We declare class in such a weird name to have its displayed name to be
// exactly 'Array'
window.MultiversionArray = class Array extends window.Array {
constructor(initial, cxt) {
super()
this.multiversion = new Multiversion(cxt)
this.initial = [...initial]
this.redo_log = []
this.apply_initial()
}
apply_initial() {
super.length = this.initial.length
for(let i = 0; i < this.initial.length; i++) {
this[i] = this.initial[i]
}
}
static get [Symbol.species]() {
return globalThis.Array
}
}
wrap_methods(
window.MultiversionArray,
[
'at',
'concat',
'copyWithin',
'entries',
'every',
'fill',
'filter',
'find',
'findIndex',
'findLast',
'findLastIndex',
'flat',
'flatMap',
'forEach',
'includes',
'indexOf',
'join',
'keys',
'lastIndexOf',
'map',
'pop',
'push',
'reduce',
'reduceRight',
'reverse',
'shift',
'slice',
'some',
'sort',
'splice',
'toLocaleString',
'toReversed',
'toSorted',
'toSpliced',
'toString',
'unshift',
'values',
'with',
Symbol.iterator,
],
[
'copyWithin',
'fill',
'pop',
'push',
'reverse',
'shift',
'sort',
'splice',
'unshift',
]
)
}
const methods_that_return_self = new Set([
'copyWithin',
'fill',
'reverse',
'sort',
])
export function wrap_array(initial, cxt) {
const array = new cxt.window.MultiversionArray(initial, cxt)
const handler = {
get(target, prop, receiver) {
rollback_if_needed(target)
const result = target[prop]
if(
typeof(prop) == 'string'
&& isNaN(Number(prop))
&& typeof(result) == 'function'
) {
if(methods_that_return_self.has(prop)) {
// declare object with key prop for function to have a name
return {
[prop]() {
result.apply(target, arguments)
return receiver
}
}[prop]
} else {
return {
[prop]() {
return result.apply(target, arguments)
}
}[prop]
}
} else {
return result
}
},
set(obj, prop, val) {
mutate(obj, set, [prop, val])
return true
},
}
return new Proxy(array, handler)
}
export function create_array(initial, cxt, index, literals) {
const result = wrap_array(initial, cxt)
literals.set(index, result)
return result
}

View File

@@ -0,0 +1,61 @@
import {Multiversion} from './multiversion.js'
// 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;
}
export class LetMultiversion extends Multiversion {
constructor(cxt, initial) {
super(cxt)
this.latest = initial
this.versions = [{version_number: cxt.version_counter, value: initial}]
}
rollback_if_needed() {
if(this.needs_rollback()) {
this.latest = this.get_version(this.cxt.version_counter)
}
}
get() {
this.rollback_if_needed()
return this.latest
}
set(value) {
this.rollback_if_needed()
const version_number = ++this.cxt.version_counter
if(this.is_created_during_current_expansion()) {
this.versions.push({version_number, value})
}
this.latest = value
}
get_version(version_number) {
if(version_number == null) {
throw new Error('illegal state')
}
const idx = binarySearch(this.versions, version_number, (id, el) => id - el.version_number)
if(idx >= 0) {
return this.versions[idx].value
} else if(idx == -1) {
throw new Error('illegal state')
} else {
return this.versions[-idx - 2].value
}
}
}

45
src/runtime/map.js Normal file
View File

@@ -0,0 +1,45 @@
import {Multiversion, wrap_methods, rollback_if_needed} from './multiversion.js'
export const defineMultiversionMap = window => {
// We declare class in such a weird name to have its displayed name to be
// exactly 'Map'
window.MultiversionMap = class Map extends window.Map {
constructor(initial, cxt) {
super()
this.multiversion = new Multiversion(cxt)
this.initial = new globalThis.Map(initial)
this.redo_log = []
this.apply_initial()
}
apply_initial() {
super.clear()
for(let [k,v] of this.initial) {
super.set(k,v)
}
}
get size() {
rollback_if_needed(this)
return super.size
}
}
wrap_methods(
window.MultiversionMap,
// all methods
[
'clear', 'delete', 'entries', 'forEach', 'get', 'has', 'keys', 'set', 'values',
Symbol.iterator,
],
// mutation methods
['set', 'delete', 'clear'],
)
}

View File

@@ -1,76 +1,91 @@
// 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;
}
export class Multiversion {
constructor(cxt, initial) {
constructor(cxt) {
this.cxt = cxt
this.expand_calltree_node_number = cxt.expand_calltree_node_number
this.latest = initial
this.versions = [{version_number: cxt.version_counter, value: initial}]
this.ct_expansion_id = cxt.ct_expansion_id
}
is_created_during_current_expand() {
return this.expand_calltree_node_number == this.cxt.expand_calltree_node_number
is_created_during_current_expansion() {
return this.ct_expansion_id == this.cxt.ct_expansion_id
}
get() {
if(!this.cxt.is_expanding_calltree_node) {
return this.latest
} else {
if(this.is_created_during_current_expand()) {
return this.latest
} else {
const version_number = this.cxt.version_counter
return this.get_version(version_number)
}
}
}
get_version(version_number) {
if(version_number == null) {
throw new Error('illegal state')
}
const idx = binarySearch(this.versions, version_number, (id, el) => id - el.version_number)
if(idx >= 0) {
return this.versions[idx].value
} else if(idx == -1) {
throw new Error('illegal state')
} else {
return this.versions[-idx - 2].value
}
}
set(value) {
const version_number = ++this.cxt.version_counter
needs_rollback() {
if(this.cxt.is_expanding_calltree_node) {
if(this.is_created_during_current_expand()) {
this.latest = value
this.set_version(version_number, value)
if(this.is_created_during_current_expansion()) {
// do nothing, keep using current version
} else {
if(this.rollback_expansion_id == this.cxt.ct_expansion_id) {
// do nothing, keep using current version
// We are in the same expansion rollback was done, keep using current version
} else {
this.rollback_expansion_id = this.cxt.ct_expansion_id
return true
}
}
} else {
if(this.rollback_expansion_id != null) {
this.rollback_expansion_id = null
return true
} else {
// do nothing
}
} else {
this.latest = value
this.set_version(version_number, value)
}
}
}
set_version(version_number, value) {
this.versions.push({version_number, value})
export function rollback_if_needed(object) {
if(object.multiversion.needs_rollback()) {
// Rollback to initial value
object.apply_initial()
// Replay redo log
for(let i = 0; i < object.redo_log.length; i++) {
const log_item = object.redo_log[i]
if(log_item.version_number > object.multiversion.cxt.version_counter) {
break
}
log_item.method.apply(object, log_item.args)
}
}
}
function wrap_readonly_method(clazz, method) {
const original = clazz.__proto__.prototype[method]
clazz.prototype[method] = {
[method](){
rollback_if_needed(this)
return original.apply(this, arguments)
}
}[method]
}
export function mutate(object, method, args) {
rollback_if_needed(object)
const version_number = ++object.multiversion.cxt.version_counter
if(object.multiversion.is_created_during_current_expansion()) {
object.redo_log.push({
method,
args,
version_number,
})
}
return method.apply(object, args)
}
function wrap_mutating_method(clazz, method) {
const original = clazz.__proto__.prototype[method]
clazz.prototype[method] = {
[method]() {
return mutate(this, original, arguments)
}
}[method]
}
export function wrap_methods(clazz, all_methods, mutating_methods) {
for (let method of all_methods) {
if(mutating_methods.includes(method)) {
wrap_mutating_method(clazz, method)
} else {
wrap_readonly_method(clazz, method)
}
}
}

68
src/runtime/object.js Normal file
View File

@@ -0,0 +1,68 @@
import {Multiversion, rollback_if_needed, wrap_methods, mutate} from './multiversion.js'
export function create_object(initial, cxt, index, literals) {
const multiversion = new Multiversion(cxt)
let latest = {...initial}
const redo_log = []
function rollback_if_needed() {
if(multiversion.needs_rollback()) {
latest = {...initial}
for(let i = 0; i < redo_log.length; i++) {
const log_item = redo_log[i]
if(log_item.version_number > multiversion.cxt.version_counter) {
break
}
if(log_item.type == 'set') {
latest[log_item.prop] = log_item.value
} else if(log_item.type == 'delete') {
delete latest[log_item.prop]
} else {
throw new Error('illegal type')
}
}
}
}
const handler = {
get(target, prop, receiver) {
rollback_if_needed()
return latest[prop]
},
has(target, prop) {
rollback_if_needed()
return prop in latest
},
set(obj, prop, value) {
rollback_if_needed()
const version_number = ++multiversion.cxt.version_counter
if(multiversion.is_created_during_current_expansion()) {
redo_log.push({ type: 'set', prop, value, version_number })
}
latest[prop] = value
return true
},
ownKeys(target) {
rollback_if_needed()
return Object.keys(latest)
},
getOwnPropertyDescriptor(target, prop) {
rollback_if_needed()
return {
configurable: true,
enumerable: true,
value: latest[prop],
};
},
// TODO delete property handler
}
const result = new Proxy(initial, handler)
literals.set(index, result)
return result
}

View File

@@ -1,8 +1,9 @@
import {set_current_context} from './record_io.js'
import {Multiversion} from './multiversion.js'
// Create separate class to check value instanceof LetMultiversion
export class LetMultiversion extends Multiversion {}
import {LetMultiversion} from './let_multiversion.js'
import {defineMultiversionArray, create_array, wrap_array} from './array.js'
import {create_object} from './object.js'
import {defineMultiversionSet} from './set.js'
import {defineMultiversionMap} from './map.js'
/*
Converts generator-returning function to promise-returning function. Allows to
@@ -70,6 +71,7 @@ const do_run = function*(module_fns, cxt, io_trace){
io_trace_abort_replay,
}
defineMultiversion(cxt.window)
apply_promise_patch(cxt)
set_current_context(cxt)
@@ -85,6 +87,7 @@ const do_run = function*(module_fns, cxt, io_trace){
id: ++cxt.call_counter,
version_number: cxt.version_counter,
let_vars: {},
literals: new Map(),
}
try {
@@ -92,12 +95,15 @@ const do_run = function*(module_fns, cxt, io_trace){
const result = fn(
cxt,
calltree.let_vars,
calltree.literals,
calltree_node_by_loc.get(module),
__trace,
__trace_call,
__do_await,
__save_ct_node_for_path,
LetMultiversion,
create_array,
create_object,
)
if(result instanceof cxt.window.Promise) {
yield cxt.window.Promise.race([replay_aborted_promise, result])
@@ -193,10 +199,6 @@ 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.expand_calltree_node_number = cxt.expand_calltree_node_number == null
? 0
: cxt.expand_calltree_node_number + 1
// Save call counter and set it to the value it had when executed 'fn' for
// the first time
@@ -208,29 +210,24 @@ export const do_eval_expand_calltree_node = (cxt, node) => {
// as node.id
: node.id - 1
const version_counter = cxt.version_counter
// Save version_counter
cxt.version_counter = node.version_number
cxt.children = null
try {
if(node.is_new) {
new node.fn(...node.args)
} else {
node.fn.apply(node.context, node.args)
}
with_version_number(cxt, node.version_number, () => {
if(node.is_new) {
new node.fn(...node.args)
} else {
node.fn.apply(node.context, node.args)
}
})
} catch(e) {
// do nothing. Exception was caught and recorded inside '__trace'
}
// Restore call counter
cxt.call_counter = call_counter
// Restore version_counter
cxt.version_counter = version_counter
cxt.is_expanding_calltree_node = false
cxt.is_recording_deferred_calls = true
const children = cxt.children
cxt.children = null
@@ -312,6 +309,9 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure, has_versione
let_vars = cxt.let_vars = {}
}
// TODO only allocate map if has literals
const literals = cxt.literals = new Map()
let ok, value, error
const is_toplevel_call_copy = cxt.is_toplevel_call
@@ -343,6 +343,7 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure, has_versione
version_number,
last_version_number: cxt.version_counter,
let_vars,
literals,
ok,
value,
error,
@@ -386,6 +387,47 @@ const __trace = (cxt, fn, name, argscount, __location, get_closure, has_versione
return result
}
const defineMultiversion = window => {
if(window.defineMultiversionDone) {
return
}
window.defineMultiversionDone = true
defineMultiversionArray(window)
defineMultiversionSet(window)
defineMultiversionMap(window)
}
const wrap_multiversion_value = (value, cxt) => {
// TODO use a WeakMap value => wrapper ???
if(value instanceof cxt.window.Set) {
if(!(value instanceof cxt.window.MultiversionSet)) {
return new cxt.window.MultiversionSet(value, cxt)
} else {
return value
}
}
if(value instanceof cxt.window.Map) {
if(!(value instanceof cxt.window.MultiversionMap)) {
return new cxt.window.MultiversionMap(value, cxt)
} else {
return value
}
}
if(value instanceof cxt.window.Array) {
if(!(value instanceof cxt.window.MultiversionArray)) {
return wrap_array(value, cxt)
} else {
return value
}
}
return value
}
const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => {
if(fn != null && fn.__location != null && !is_new) {
// Call will be traced, because tracing code is already embedded inside
@@ -431,7 +473,11 @@ const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => {
if(value instanceof cxt.window.Promise) {
set_record_call(cxt)
}
value = wrap_multiversion_value(value, cxt)
return value
} catch(_error) {
ok = false
error = _error
@@ -488,3 +534,28 @@ const __save_ct_node_for_path = (cxt, __calltree_node_by_loc, index, __call_id)
set_record_call(cxt)
}
}
export const with_version_number = (rt_cxt, version_number, action) => {
if(rt_cxt.logs == null) {
// check that argument is rt_cxt
throw new Error('illegal state')
}
if(version_number == null) {
throw new Error('illegal state')
}
if(rt_cxt.is_expanding_calltree_node) {
throw new Error('illegal state')
}
rt_cxt.is_expanding_calltree_node = true
const version_counter_copy = rt_cxt.version_counter
rt_cxt.version_counter = version_number
rt_cxt.ct_expansion_id = rt_cxt.ct_expansion_id == null
? 0
: rt_cxt.ct_expansion_id + 1
try {
return action()
} finally {
rt_cxt.is_expanding_calltree_node = false
rt_cxt.version_counter = version_counter_copy
}
}

44
src/runtime/set.js Normal file
View File

@@ -0,0 +1,44 @@
import {Multiversion, wrap_methods, rollback_if_needed} from './multiversion.js'
export const defineMultiversionSet = window => {
// We declare class in such a weird name to have its displayed name to be
// exactly 'Set'
window.MultiversionSet = class Set extends window.Set {
constructor(initial, cxt) {
super()
this.multiversion = new Multiversion(cxt)
this.initial = new globalThis.Set(initial)
this.redo_log = []
this.apply_initial()
}
apply_initial() {
super.clear()
for (const item of this.initial) {
super.add(item)
}
}
get size() {
rollback_if_needed(this)
return super.size
}
}
wrap_methods(
window.MultiversionSet,
// all methods
[
'has', 'add', 'delete', 'clear', 'entries', 'forEach', 'values', 'keys',
Symbol.iterator,
],
// mutation methods
['add', 'delete', 'clear'],
)
}

View File

@@ -134,6 +134,11 @@ export const stringify_for_header = (v, no_toJSON = false) => {
}
}
export const short_header = value =>
Array.isArray(value)
? 'Array(' + value.length + ')'
: ''
const header_object = object => {
const prefix =
(object.constructor?.name == null || object.constructor?.name == 'Object')

View File

@@ -90,6 +90,9 @@ const state = COMMANDS.eval_modules_finished(loaded, loaded, result, s.node, s.t
const root = root_calltree_node(state)
const run = root.children[0]
if(!root_calltree_node(state).ok) {
console.error(root_calltree_node(state).error)
}
assert_equal(root_calltree_node(state).ok, true)
// Assert that run children are tests

View File

@@ -1,7 +1,7 @@
import {find_leaf, ancestry, find_node} from '../src/ast_utils.js'
import {print_debug_node} from '../src/parse_js.js'
import {eval_frame, eval_modules} from '../src/eval.js'
import {COMMANDS} from '../src/cmd.js'
import {COMMANDS, with_version_number_of_log} from '../src/cmd.js'
import {header} from '../src/value_explorer_utils.js'
import {
root_calltree_node,
@@ -11,6 +11,7 @@ import {
current_cursor_position,
get_execution_paths,
} from '../src/calltree.js'
import {color_file} from '../src/color.js'
import {
test,
@@ -20,6 +21,7 @@ import {
do_parse,
assert_code_evals_to, assert_code_evals_to_async,
assert_code_error, assert_code_error_async,
assert_versioned_value, assert_value_explorer, assert_selection,
parse_modules,
test_initial_state, test_initial_state_async,
test_deferred_calls_state,
@@ -427,6 +429,24 @@ export const tests = [
)
}),
test('parse assignment error', () => {
const code = `
const x = [0]
x[0] = 1, x?.[0] = 2
`
const parse_result = do_parse(code)
assert_equal(parse_result.ok, false)
}),
test('parse assignment ok', () => {
const code = `
const x = [0]
x[0] = 1
`
const parse_result = do_parse(code)
assert_equal(parse_result.ok, true)
}),
test('ASI_1', () => {
const parse_result = do_parse(`
1
@@ -717,6 +737,16 @@ export const tests = [
assert_equal(active_frame(i).children[0].result.value, 'bar')
}),
test('eval_frame member_access null', () => {
const frame = active_frame(test_initial_state('null["foo"]'))
const result = frame.children[0].result
assert_equal(result.ok, false)
assert_equal(
result.error,
new TypeError("Cannot read properties of null (reading 'foo')")
)
}),
test('eval_frame new', () => {
const i = test_initial_state('new Error("foobar")')
assert_equal(active_frame(i).children[0].result.value.message, 'foobar')
@@ -736,7 +766,9 @@ export const tests = [
x(2);
`
const i = test_initial_state(code, code.indexOf('y;'))
assert_equal(active_frame(i).children[1].result, {ok: true, value: 2})
const result = active_frame(i).children[1].result
assert_equal(result.ok, true)
assert_equal(result.value, 2)
}),
test('eval_frame function_body_do', () => {
@@ -765,7 +797,8 @@ export const tests = [
`)
const frame = active_frame(i)
const _if = frame.children[0]
assert_equal(_if.children[0].result, {ok: true, value: 1})
assert_equal(_if.children[0].result.ok, true)
assert_equal(_if.children[0].result.value, 1)
assert_equal(_if.children[1].result, {ok: true})
assert_equal(_if.children[2].result, null)
}),
@@ -779,7 +812,8 @@ export const tests = [
const frame = active_frame(i)
const _if = frame.children[0]
assert_equal(_if.children.length, 2)
assert_equal(_if.children[0].result, {ok: true, value: 1})
assert_equal(_if.children[0].result.ok, true)
assert_equal(_if.children[0].result.value, 1)
assert_equal(_if.children[1].result, {ok: true})
}),
@@ -884,6 +918,17 @@ export const tests = [
//)
}),
test('eval_frame const lefthand', () => {
const code = `
const x = 1
`
const initial = test_initial_state(code)
const frame = active_frame(initial)
const x = find_node(frame, n => n.string == 'x')
assert_equal(x.result.value, 1)
assert_equal(x.result.version_number, 0)
}),
test('array spread not iterable', () => {
assert_code_error(
`[...null]`,
@@ -2139,7 +2184,7 @@ const y = x()`
`)
const expanded = COMMANDS.calltree.click(i, root_calltree_node(i).children[0].id)
const args = expanded.value_explorer.result.value['*arguments*']
assert_equal(args, {x: 1, y: 2})
assert_equal(args, {value: {x: 1, y: 2}})
}),
test('click native calltree node', () => {
@@ -2154,11 +2199,16 @@ const y = x()`
index,
result: {
"ok": true,
"is_calltree_node_explorer": true,
"value": {
"*arguments*": [
[]
],
"*return*": {}
"*arguments*": {
value: [
[]
],
},
"*return*": {
value: {},
}
}
}
}
@@ -2548,7 +2598,8 @@ const y = x()`
// focus call
const s2 = COMMANDS.calltree.arrow_right(s1)
const s3 = COMMANDS.calltree.select_arguments(s2)
assert_equal(s3.state.value_explorer.result, {ok: true, value: [1]})
assert_equal(s3.state.value_explorer.result.ok, true)
assert_equal(s3.state.value_explorer.result.value, [1])
assert_equal(current_cursor_position(s3.state), code.indexOf('(1)'))
assert_equal(s3.effects, {type: 'set_focus'})
}),
@@ -2564,7 +2615,14 @@ const y = x()`
// expand call
const s2 = COMMANDS.calltree.arrow_right(s2_0)
const s3 = COMMANDS.calltree.select_arguments(s2)
assert_equal(s3.state.value_explorer.result, {ok: true, value: {a: 1}})
assert_equal(
s3.state.value_explorer.result,
{
ok: true,
value: {a: 1},
version_number: 0,
}
)
assert_equal(current_cursor_position(s3.state), code.indexOf('(a)'))
assert_equal(s3.effects, {type: 'set_focus'})
}),
@@ -2574,7 +2632,8 @@ const y = x()`
const s1 = test_initial_state(code)
const s2 = COMMANDS.calltree.arrow_right(s1)
const s3 = COMMANDS.calltree.select_arguments(s2).state
assert_equal(s3.value_explorer.result, {ok: true, value: ["1"]})
assert_equal(s3.value_explorer.result.ok, true)
assert_equal(s3.value_explorer.result.value, ["1"])
}),
test('select_error', () => {
@@ -2627,7 +2686,7 @@ const y = x()`
assert_equal(s4.value_explorer, {
index: code.indexOf(selected),
length: selected.length,
result: {ok: true, value: {a: 1, b: 2}},
result: {ok: true, value: {a: 1, b: 2}, version_number: 0},
})
}),
@@ -2638,11 +2697,10 @@ const y = x()`
`
const s1 = test_initial_state(code)
const s2 = COMMANDS.move_cursor(s1, code.indexOf('2'))
assert_equal(s2.value_explorer, {
index: code.indexOf('y*2'),
length: 3,
result: {ok: true, value: 4},
})
assert_equal(s2.value_explorer.index, code.indexOf('y*2'))
assert_equal(s2.value_explorer.length, 3)
assert_equal(s2.value_explorer.result.ok, true)
assert_equal(s2.value_explorer.result.value, 4)
}),
test('move_cursor let', () => {
@@ -2655,7 +2713,7 @@ const y = x()`
assert_equal(s2.value_explorer, {
index: code.indexOf(lettext),
length: lettext.length,
result: {ok: true, value: 1},
result: {ok: true, value: 1, version_number: 0},
})
}),
@@ -2742,7 +2800,7 @@ const y = x()`
const m = COMMANDS.move_cursor(i, code.indexOf('x(null'))
assert_equal(
m.value_explorer.result.error,
new Error("Cannot read properties of null (reading 'foo')")
new TypeError("Cannot read properties of null (reading 'foo')")
)
}),
@@ -4284,7 +4342,8 @@ const y = x()`
`
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})
assert_equal(i.value_explorer.result.ok, true)
assert_equal(i.value_explorer.result.value, undefined)
}),
test('let_versions save version bug', () => {
@@ -4656,7 +4715,7 @@ const y = x()`
assert_equal(second_map_call_exp.children[0].id == second_map_call_exp.id + 1, true)
}),
test('let_versions expand twice', () => {
test('let_versions expand_calltree_node twice', () => {
const code = `
function test() {
let x = 0
@@ -4724,6 +4783,7 @@ const y = x()`
assert_equal(moved.value_explorer.result.value, 2)
}),
test('let_versions deferred calls get value', () => {
const code = `
let x = 0
@@ -4751,4 +4811,484 @@ const y = x()`
const exp = COMMANDS.calltree.click(i, second_set_call.id)
assert_equal(exp.modules[''].get(), 3)
}),
test('let_versions multiple assignments', () => {
const code = `
let x
function foo () {
x /*x foo*/
}
x = 1
foo()
x = 2
foo() /*foo 2*/
x = 3
x /*x*/
`
const i = test_initial_state(code, code.indexOf('x /*x*/'))
assert_value_explorer(i, 3)
const stepped = COMMANDS.step_into(i, code.indexOf('foo() /*foo 2*/'))
const moved = COMMANDS.move_cursor(stepped, code.indexOf('x /*x foo*/'))
assert_value_explorer(moved, 2)
}),
test('mutability array', () => {
const code = `
const arr = [2,1]
arr.at(1)
arr.push(3)
arr /*after push*/
arr.sort()
arr /*after sort*/
arr[0] = 4
arr /*after set*/
`
const i = test_initial_state(code, code.indexOf('arr.at'))
assert_value_explorer(i, 1)
const s1 = COMMANDS.move_cursor(i, code.indexOf('arr /*after push*/'))
assert_value_explorer(s1, [2,1,3])
const s2 = COMMANDS.move_cursor(i, code.indexOf('arr /*after sort*/'))
assert_value_explorer(s2, [1,2,3])
const s3 = COMMANDS.move_cursor(i, code.indexOf('arr /*after set*/'))
assert_value_explorer(s3, [4,2,3])
}),
test('mutability array set length', () => {
const code = `
const x = [1,2,3]
x.length = 2
x /*x*/
x.length = 1
`
const i = test_initial_state(code, code.indexOf('x /*x*/'))
assert_value_explorer(i, [1,2])
}),
test('mutability array method name', () => {
assert_code_evals_to(`[].sort.name`, 'sort')
assert_code_evals_to(`[].forEach.name`, 'forEach')
}),
test('mutability array method returns itself', () => {
const code = `
const x = [3,2,1]
const y = x.sort()
if(x != y) {
throw new Error('not eq')
}
x.push(4)
`
const i = test_initial_state(code, code.indexOf('const y'))
assert_equal(root_calltree_node(i).ok, true)
assert_value_explorer(i, [1,2,3])
}),
test('mutability set', () => {
const code = `
const s = new Set([1,2])
s.delete(2)
if(s.size != 1) {
throw new Error('size not eq')
}
s.add(3)
s /*s*/
`
const i = test_initial_state(code, code.indexOf('const s'))
assert_value_explorer(i, new Set([1,2]))
const moved = COMMANDS.move_cursor(i, code.indexOf('s /*s*/'))
assert_value_explorer(moved, new Set([1,3]))
}),
test('mutability set method name', () => {
assert_code_evals_to(`new Set().delete.name`, 'delete')
}),
// This test is for browser environment where runtime is loaded from the main
// (IDE) window, and user code is loaded from app window
test('mutability instanceof', () => {
assert_code_evals_to(`{} instanceof Object`, true)
assert_code_evals_to(`new Object() instanceof Object`, true)
assert_code_evals_to(`[] instanceof Array`, true)
assert_code_evals_to(`new Array() instanceof Array`, true)
assert_code_evals_to(`new Set() instanceof Set`, true)
assert_code_evals_to(`new Map() instanceof Map`, true)
}),
test('mutability map', () => {
const code = `
const s = new Map([['foo', 1], ['bar', 2]])
s.delete('foo')
s.set('baz', 3)
s /*s*/
`
const i = test_initial_state(code, code.indexOf('const s'))
assert_value_explorer(i, {foo: 1, bar: 2})
const moved = COMMANDS.move_cursor(i, code.indexOf('s /*s*/'))
assert_value_explorer(moved, {bar: 2, baz: 3})
}),
test('mutability object', () => {
const code = `
const s = {foo: 1, bar: 2}
s.foo = 2
s.baz = 3
s /*s*/
`
const i = test_initial_state(code, code.indexOf('const s'))
assert_value_explorer(i, {foo: 1, bar: 2})
const moved = COMMANDS.move_cursor(i, code.indexOf('s /*s*/'))
assert_value_explorer(moved, {foo: 2, bar: 2, baz: 3})
}),
test('mutability', () => {
const code = `
const make_array = () => [3,2,1]
const x = make_array()
x.sort()
`
const i = test_initial_state(code)
const index = code.indexOf('x.sort()')
const selected_x = COMMANDS.eval_selection(i, index, true).state
assert_equal(selected_x.selection_state.node.length, 'x'.length)
assert_selection(selected_x, [3, 2, 1])
const selected_sort = COMMANDS.eval_selection(
COMMANDS.eval_selection(selected_x, index, true).state, index, true
).state
assert_equal(selected_sort.selection_state.node.length, 'x.sort()'.length)
assert_selection(selected_sort, [1,2,3])
}),
test('mutability value_explorer bug', () => {
const code = `
const x = [3,2,1]
x.sort()
x /*x*/
`
const i = test_initial_state(code, code.indexOf('x /*x*/'))
assert_value_explorer(
i,
[1,2,3]
)
}),
test('mutability with_version_number', () => {
const code = `
const make_array = () => [3,2,1]
const x = make_array()
x.sort()
`
const i = test_initial_state(code, code.indexOf('const x'))
assert_value_explorer(i, [3,2,1])
}),
test('mutability member access version', () => {
const code = `
const x = [0]
x[0] /*x[0]*/
x[0] = 1
`
const i = test_initial_state(code, code.indexOf('x[0] /*x[0]*/'))
assert_equal(i.value_explorer.result.value, 0)
}),
test('mutability assignment', () => {
const code = `
const x = [0]
x[0] = 1
`
const i = test_initial_state(code)
const index = code.indexOf('x[0]')
const evaled = COMMANDS.eval_selection(
COMMANDS.eval_selection(i, index).state,
index,
).state
assert_equal(evaled.selection_state.node.length, 'x[0]'.length)
assert_selection(evaled, 1)
}),
test('mutability assignment value explorer', () => {
const code = `
const x = [0]
x[0] = 1
`
const i = test_initial_state(code, code.indexOf('x[0]'))
assert_value_explorer(i, 1)
}),
test('mutability multiple assignment value explorer', () => {
const code = `
const x = [0]
x[0] = 1, x[0] = 2
x /*x*/
`
const i = test_initial_state(code, code.indexOf('x[0]'))
assert_equal(i.value_explorer, null)
const moved = COMMANDS.move_cursor(i, code.indexOf('x /*x*/'))
assert_value_explorer(moved, [2])
}),
test('mutability assignment value explorer new value', () => {
const code = `
const x = [0]
x[0] = 1
x[0] /*x*/
`
const i = test_initial_state(code, code.indexOf('x[0] /*x*/'))
assert_value_explorer(i, [1])
}),
test('mutability eval_selection lefthand', () => {
const code = `
const x = [0]
x[0] = 1
`
const i = test_initial_state(code)
const evaled = COMMANDS.eval_selection(i, code.indexOf('x[0]')).state
assert_selection(evaled, [0])
// expand eval to x[0]
const evaled2 = COMMANDS.eval_selection(evaled, code.indexOf('x[0]')).state
assert_selection(evaled2, 1)
}),
test('mutability multiple assignments', () => {
const code = `
const x = [0]
x[0] = 1
x /*x*/
x[0] = 2
`
const i = test_initial_state(code, code.indexOf('x /*x*/'))
assert_value_explorer(i, [1])
}),
test('mutability value explorer', () => {
const code = `
const x = [0]
x[0] = 1
`
const i = test_initial_state(code, code.indexOf('x[0] = 1'))
assert_value_explorer(i, 1)
}),
test('mutability calltree value explorer', () => {
const i = test_initial_state(`
const array = [3,2,1]
function sort(array) {
return array.sort()
}
sort(array)
`)
const selected = COMMANDS.calltree.click(i, root_calltree_node(i).children[0].id)
const args = selected.value_explorer.result.value['*arguments*']
assert_versioned_value(i, args, {array: [3,2,1]})
const returned = selected.value_explorer.result.value['*return*']
assert_versioned_value(i, returned, [1,2,3])
}),
test('mutability import mutable value', () => {
const code = {
'': `
import {array} from 'x.js'
import {change_array} from 'x.js'
change_array()
array /*result*/
`,
'x.js': `
export const array = ['initial']
export const change_array = () => {
array[0] = 'changed'
}
`
}
const main = code['']
const i = test_initial_state(code, main.indexOf('import'))
assert_value_explorer(i, {array: ['initial']})
const sel = COMMANDS.eval_selection(i, main.indexOf('array')).state
assert_selection(sel, ['initial'])
const moved = COMMANDS.move_cursor(sel, main.indexOf('array /*result*/'))
assert_value_explorer(moved, ['changed'])
}),
test('mutability Object.assign', () => {
const i = test_initial_state(`Object.assign({}, {foo: 1})`)
assert_value_explorer(i, {foo: 1})
}),
test('mutability wrap external arrays', () => {
const code = `
const x = "foo bar".split(' ')
x.push('baz')
x /*x*/
`
const i = test_initial_state(code, code.indexOf('const x'))
assert_value_explorer(i, ['foo', 'bar'])
}),
test('mutability logs', () => {
const i = test_initial_state(`
const x = [1]
console.log(x)
x.push(2)
console.log(x)
`)
const log1 = i.logs.logs[0]
with_version_number_of_log(i, log1, () =>
assert_equal(
[[1]],
log1.args,
)
)
const log2 = i.logs.logs[1]
with_version_number_of_log(i, log2, () =>
assert_equal(
[[1,2]],
log2.args,
)
)
}),
// copypasted from the same test for let_versions
test('mutability expand_calltree_node', () => {
const code = `
const y = []
function foo(x) {
y /*y*/
bar(y)
}
function bar(arg) {
}
foo(0)
y[0] = 11
foo(0)
y[0] = 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')
const moved = COMMANDS.move_cursor(expanded, code.indexOf('y /*y*/'))
assert_value_explorer(moved, [11])
}),
// copypasted from the same test for let_versions
test('mutability expand_calltree_node twice', () => {
const code = `
function test() {
let x = {value: 0}
function test2() {
function foo() {
x /*x*/
}
x.value = x.value + 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, {value: 1 })
}),
test('mutability quicksort', () => {
const code = `
const loop = new Function('action', \`
while(true) {
if(action()) {
return
}
}
\`)
function partition(arr, begin, end) {
const pivot = arr[begin]
let i = begin - 1, j = end + 1
loop(() => {
i = i + 1
loop(() => {
if(arr[i] < pivot) {
i = i + 1
} else {
return true /* stop */
}
})
j = j - 1
loop(() => {
if(arr[j] > pivot) {
j = j - 1
} else {
return true // stop iteration
}
})
if(i >= j) {
return true // stop iteration
}
const temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
})
return j
}
function qsort(arr, begin = 0, end = arr.length - 1) {
if(begin >= 0 && end >= 0 && begin < end) {
const p = partition(arr, begin, end)
qsort(arr, begin, p)
qsort(arr, p + 1, end)
}
}
const arr = [ 2, 15, 13, 12, 3, 9, 14, 3, 18, 0 ]
qsort(arr)
arr /*result*/
`
const i = test_initial_state(code, code.indexOf('arr /*result*/'))
const expected = [ 0, 2, 3, 3, 9, 12, 13, 14, 15, 18 ]
assert_value_explorer(i, expected)
}),
]

View File

@@ -1,9 +1,12 @@
import {find_error_origin_node} from '../src/ast_utils.js'
import {parse, print_debug_node, load_modules} from '../src/parse_js.js'
import {eval_modules} from '../src/eval.js'
import {active_frame, pp_calltree} from '../src/calltree.js'
import {active_frame, pp_calltree, version_number_symbol} from '../src/calltree.js'
import {COMMANDS} from '../src/cmd.js'
// external
import {with_version_number} from '../src/runtime/runtime.js'
Object.assign(globalThis,
{
// for convenince, to type just `log` instead of `console.log`
@@ -163,6 +166,8 @@ export const stringify = val =>
JSON.stringify(val, (key, value) => {
if(value instanceof Set){
return [...value]
} else if (value instanceof Map) {
return Object.fromEntries([...value.entries()])
} else if(value instanceof Error) {
return {message: value.message}
} else {
@@ -198,6 +203,22 @@ export const print_debug_ct_node = node => {
return stringify(do_print(node))
}
export const assert_versioned_value = (state, versioned, expected) => {
const version_number = versioned[version_number_symbol] ?? versioned.version_number
if(version_number == null) {
throw new Error('illegal state')
}
return with_version_number(state.rt_cxt, version_number, () =>
assert_equal(versioned.value, expected)
)
}
export const assert_value_explorer = (state, expected) =>
assert_versioned_value(state, state.value_explorer.result, expected)
export const assert_selection = (state, expected) =>
assert_versioned_value(state, state.selection_state.node.result, expected)
export const test = (message, test, only = false) => {
return {
message,