Preserve redo log for mutable objects

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

View File

@@ -105,7 +105,10 @@ export const find_versioned_let_vars = (node, current_fn = node) => {
} else if(node.type == 'assignment') {
const {node: next_node, closed_let_vars, assigned_vars}
= do_find_versioned_let_vars(node, current_fn)
const next_assigned_vars = node.children.flatMap(decl_pair =>
const next_assigned_vars = node
.children
.filter(c => c.type == 'decl_pair')
.flatMap(decl_pair =>
collect_destructuring_identifiers(decl_pair).map(id => {
if(id.definition.index == null) {
throw new Error('illegal state')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

6
src/effects.js vendored
View File

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

View File

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

View File

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

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

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

View File

@@ -0,0 +1,61 @@
import {Multiversion} from './multiversion.js'
// https://stackoverflow.com/a/29018745
function binarySearch(arr, el, compare_fn) {
let m = 0;
let n = arr.length - 1;
while (m <= n) {
let k = (n + m) >> 1;
let cmp = compare_fn(el, arr[k]);
if (cmp > 0) {
m = k + 1;
} else if(cmp < 0) {
n = k - 1;
} else {
return k;
}
}
return ~m;
}
export class LetMultiversion extends Multiversion {
constructor(cxt, initial) {
super(cxt)
this.latest = initial
this.versions = [{version_number: cxt.version_counter, value: initial}]
}
rollback_if_needed() {
if(this.needs_rollback()) {
this.latest = this.get_version(this.cxt.version_counter)
}
}
get() {
this.rollback_if_needed()
return this.latest
}
set(value) {
this.rollback_if_needed()
const version_number = ++this.cxt.version_counter
if(this.is_created_during_current_expansion()) {
this.versions.push({version_number, value})
}
this.latest = value
}
get_version(version_number) {
if(version_number == null) {
throw new Error('illegal state')
}
const idx = binarySearch(this.versions, version_number, (id, el) => id - el.version_number)
if(idx >= 0) {
return this.versions[idx].value
} else if(idx == -1) {
throw new Error('illegal state')
} else {
return this.versions[-idx - 2].value
}
}
}

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

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

View File

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

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

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

View File

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

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

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

View File

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