mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 13:04:30 -08:00
Preserve redo log for mutable objects
Replay it during time travel debugging
This commit is contained in:
26
README.md
26
README.md
@@ -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:
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
98
src/cmd.js
98
src/cmd.js
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(', ')
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
6
src/effects.js
vendored
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
231
src/eval.js
231
src/eval.js
@@ -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
|
||||
|
||||
@@ -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
142
src/runtime/array.js
Normal 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
|
||||
}
|
||||
61
src/runtime/let_multiversion.js
Normal file
61
src/runtime/let_multiversion.js
Normal 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
45
src/runtime/map.js
Normal 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'],
|
||||
)
|
||||
|
||||
}
|
||||
@@ -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
68
src/runtime/object.js
Normal 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
|
||||
}
|
||||
@@ -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
44
src/runtime/set.js
Normal 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'],
|
||||
)
|
||||
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
584
test/test.js
584
test/test.js
@@ -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)
|
||||
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user