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:
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user