mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 21:14:28 -08:00
record io
This commit is contained in:
@@ -52,6 +52,7 @@ const apply_eval_result = (state, eval_result) => {
|
||||
log_position: null
|
||||
},
|
||||
modules: eval_result.modules,
|
||||
io_cache: eval_result.io_cache,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +198,7 @@ const external_imports_loaded = (
|
||||
external_imports,
|
||||
state.on_deferred_call,
|
||||
state.calltree_changed_token,
|
||||
state.io_cache,
|
||||
)
|
||||
toplevel = true
|
||||
} else {
|
||||
@@ -205,6 +207,7 @@ const external_imports_loaded = (
|
||||
external_imports,
|
||||
state.on_deferred_call,
|
||||
state.calltree_changed_token,
|
||||
state.io_cache,
|
||||
{index: node.index, module: state.current_module},
|
||||
)
|
||||
toplevel = false
|
||||
|
||||
41
src/eval.js
41
src/eval.js
@@ -287,6 +287,7 @@ export const eval_modules = (
|
||||
external_imports,
|
||||
on_deferred_call,
|
||||
calltree_changed_token,
|
||||
io_cache,
|
||||
location
|
||||
) => {
|
||||
// TODO gensym __cxt, __trace, __trace_call
|
||||
@@ -295,6 +296,23 @@ export const eval_modules = (
|
||||
|
||||
const is_async = has_toplevel_await(parse_result.modules)
|
||||
|
||||
const Function = is_async
|
||||
? globalThis.run_window.eval('(async function(){})').constructor
|
||||
: globalThis.run_window.Function
|
||||
|
||||
const module_fns = parse_result.sorted.map(module => (
|
||||
{
|
||||
module,
|
||||
fn: new Function(
|
||||
'__cxt',
|
||||
'__trace',
|
||||
'__trace_call',
|
||||
'__do_await',
|
||||
codegen(parse_result.modules[module], {module})
|
||||
)
|
||||
}
|
||||
))
|
||||
|
||||
const cxt = {
|
||||
modules: external_imports == null
|
||||
? {}
|
||||
@@ -325,24 +343,7 @@ export const eval_modules = (
|
||||
Promise: globalThis.run_window.Promise,
|
||||
}
|
||||
|
||||
const Function = is_async
|
||||
? globalThis.run_window.eval('(async function(){})').constructor
|
||||
: globalThis.run_window.Function
|
||||
|
||||
const module_fns = parse_result.sorted.map(module => (
|
||||
{
|
||||
module,
|
||||
fn: new Function(
|
||||
'__cxt',
|
||||
'__trace',
|
||||
'__trace_call',
|
||||
'__do_await',
|
||||
codegen(parse_result.modules[module], {module})
|
||||
)
|
||||
}
|
||||
))
|
||||
|
||||
const result = run(module_fns, cxt)
|
||||
const result = run(module_fns, cxt, io_cache)
|
||||
|
||||
const make_result = result => ({
|
||||
modules: result.modules,
|
||||
@@ -350,14 +351,14 @@ export const eval_modules = (
|
||||
eval_cxt: result.eval_cxt,
|
||||
calltree: assign_code(parse_result.modules, result.calltree),
|
||||
call: result.call && assign_code(parse_result.modules, result.call),
|
||||
io_cache: result.eval_cxt.io_cache,
|
||||
})
|
||||
|
||||
if(result.then != null) {
|
||||
if(is_async) {
|
||||
return result.then(make_result)
|
||||
} else {
|
||||
return make_result(result)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const eval_find_call = (cxt, parse_result, calltree, location) => {
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
export const globals = new Set(Object.getOwnPropertyNames(globalThis))
|
||||
|
||||
// Not available in node.js, but add to use in tests
|
||||
globals.add('fetch')
|
||||
|
||||
268
src/record_io.js
Normal file
268
src/record_io.js
Normal file
@@ -0,0 +1,268 @@
|
||||
import {set_record_call} from './runtime.js'
|
||||
|
||||
const io_patch = (cxt, obj, method, name, use_context = false) => {
|
||||
if(obj == null || obj[method] == null) {
|
||||
// Method is absent in current env, skip patching
|
||||
return
|
||||
}
|
||||
const original = obj[method]
|
||||
obj[method] = function(...args) {
|
||||
// TODO guard calls from prev run
|
||||
console.error('patched method', name, {
|
||||
io_cache_is_recording: cxt.io_cache_is_recording,
|
||||
io_cache_is_replay_aborted: cxt.io_cache_is_replay_aborted,
|
||||
io_cache_index: cxt.io_cache_is_recording
|
||||
? cxt.io_cache.length
|
||||
: cxt.io_cache_index
|
||||
})
|
||||
// TODO guard that in find_call io methods are not called?
|
||||
// if(searched_location != null) {
|
||||
// throw new Error('illegal state')
|
||||
// }
|
||||
if(cxt.io_cache_is_replay_aborted) {
|
||||
// Try to finish fast
|
||||
throw new Error('io recording aborted')
|
||||
} else if(cxt.io_cache_is_recording) {
|
||||
let ok, value, error
|
||||
const has_new_target = new.target != null
|
||||
try {
|
||||
// TODO. Do we need it here? Only need for IO calls view. And also
|
||||
// for expand_call and find_call, to not use cache on expand call
|
||||
// and find_call
|
||||
set_record_call(cxt)
|
||||
|
||||
const index = cxt.io_cache.length
|
||||
|
||||
if(name == 'setTimeout') {
|
||||
args = args.slice()
|
||||
// Patch callback
|
||||
const cb = args[0]
|
||||
args[0] = function() {
|
||||
// TODO guard calls from prev runs
|
||||
// TODO guard io_cache_is_replay_aborted
|
||||
cxt.io_cache.push({type: 'resolution', index})
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
value = has_new_target
|
||||
? new original(...args)
|
||||
: original.apply(this, args)
|
||||
|
||||
console.log('value', value)
|
||||
|
||||
if(value instanceof Promise) {
|
||||
// TODO use native .finally for promise, not patched then?
|
||||
value.finally(() => {
|
||||
// TODO guard calls from prev runs
|
||||
// TODO guard io_cache_is_replay_aborted
|
||||
cxt.io_cache.push({type: 'resolution', index})
|
||||
})
|
||||
}
|
||||
|
||||
ok = true
|
||||
return value
|
||||
} catch(e) {
|
||||
error = e
|
||||
ok = false
|
||||
throw e
|
||||
} finally {
|
||||
cxt.io_cache.push({
|
||||
type: 'call',
|
||||
name,
|
||||
ok,
|
||||
value,
|
||||
error,
|
||||
args,
|
||||
// To discern calls with and without 'new' keyword, primary for
|
||||
// Date that can be called with and without new
|
||||
has_new_target,
|
||||
use_context,
|
||||
context: use_context ? this : undefined,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const call = cxt.io_cache[cxt.io_cache_index]
|
||||
/*
|
||||
TODO remove
|
||||
console.log(
|
||||
call.type != 'call'
|
||||
, call == null
|
||||
, call.has_new_target != (new.target != null)
|
||||
, call.use_context && (call.context != this)
|
||||
, call.name != name
|
||||
, JSON.stringify(call.args) != JSON.stringify(args)
|
||||
)
|
||||
*/
|
||||
|
||||
// TODO if call.type != 'call', and there are no more calls, should
|
||||
// we abort, or just record one more call?
|
||||
|
||||
if(
|
||||
call == null
|
||||
|| call.type != 'call'
|
||||
|| call.has_new_target != (new.target != null)
|
||||
// TODO test
|
||||
|| call.use_context && (call.context != this)
|
||||
|| call.name != name
|
||||
|| (
|
||||
// TODO for setTimeout, compare last arg (timeout)
|
||||
name != 'setTimeout'
|
||||
&&
|
||||
JSON.stringify(call.args) != JSON.stringify(args)
|
||||
)
|
||||
){
|
||||
console.log('discard cache', call)
|
||||
cxt.io_cache_is_replay_aborted = true
|
||||
// Try to finish fast
|
||||
throw new Error('io replay aborted')
|
||||
} else {
|
||||
console.log('cached call found', call)
|
||||
const next_resolution = cxt.io_cache.find((e, i) =>
|
||||
e.type == 'resolution' && i > cxt.io_cache_index
|
||||
)
|
||||
|
||||
if(next_resolution != null && !cxt.io_cache_resolver_is_set) {
|
||||
console.error('set resolver')
|
||||
const original_setTimeout = globalThis.setTimeout.__original
|
||||
cxt.io_cache_resolver_is_set = true
|
||||
|
||||
original_setTimeout(() => {
|
||||
// TODO guard from previous run
|
||||
console.error('resolver', {
|
||||
io_cache_is_replay_aborted: cxt.io_cache_is_replay_aborted,
|
||||
io_cache_index: cxt.io_cache_index,
|
||||
})
|
||||
|
||||
cxt.io_cache_resolver_is_set = false
|
||||
|
||||
// TODO check if call from prev run
|
||||
|
||||
if(cxt.io_cache_is_replay_aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
if(cxt.io_cache_index >= cxt.io_cache.length) {
|
||||
// TODO Do nothing or what?
|
||||
// Should not gonna happen
|
||||
throw new Error('illegal state')
|
||||
} else {
|
||||
const next_event = cxt.io_cache[cxt.io_cache_index]
|
||||
if(next_event.type == 'call') {
|
||||
// TODO Call not happened, replay?
|
||||
cxt.io_cache_is_replay_aborted = true
|
||||
} else {
|
||||
while(
|
||||
cxt.io_cache_index < cxt.io_cache.length
|
||||
&&
|
||||
cxt.io_cache[cxt.io_cache_index].type == 'resolution'
|
||||
) {
|
||||
const resolution = cxt.io_cache[cxt.io_cache_index]
|
||||
const resolver = cxt.io_cache_resolvers.get(resolution.index)
|
||||
|
||||
cxt.io_cache_index++
|
||||
|
||||
if(cxt.io_cache[resolution.index].name == 'setTimeout') {
|
||||
resolver()
|
||||
} else {
|
||||
resolver(cxt.io_cache[resolution.index].value)
|
||||
}
|
||||
console.log('RESOLVE', cxt.io_cache_index, resolution.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, 0)
|
||||
}
|
||||
|
||||
cxt.io_cache_index++
|
||||
|
||||
if(call.ok) {
|
||||
// TODO resolve promises in the same order they were resolved on
|
||||
// initial execution
|
||||
|
||||
if(call.value instanceof Promise) {
|
||||
return new Promise(resolve => {
|
||||
cxt.io_cache_resolvers.set(cxt.io_cache_index - 1, resolve)
|
||||
})
|
||||
} else if(name == 'setTimeout') {
|
||||
const timeout_cb = args[0]
|
||||
cxt.io_cache_resolvers.set(cxt.io_cache_index - 1, timeout_cb)
|
||||
return call.value
|
||||
} else {
|
||||
return call.value
|
||||
}
|
||||
} else {
|
||||
throw call.error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(obj[method], 'name', {value: original.name})
|
||||
|
||||
obj[method].__original = original
|
||||
}
|
||||
|
||||
const io_patch_remove = (obj, method) => {
|
||||
if(obj == null || obj[method] == null) {
|
||||
// Method is absent in current env, skip patching
|
||||
return
|
||||
}
|
||||
obj[method] = obj[method].__original
|
||||
}
|
||||
|
||||
const Response_methods = [
|
||||
'arrayBuffer',
|
||||
'blob',
|
||||
'formData',
|
||||
'json',
|
||||
'text',
|
||||
]
|
||||
|
||||
export const apply_io_patches = cxt => {
|
||||
io_patch(cxt, Math, 'random', 'Math.random')
|
||||
|
||||
io_patch(cxt, globalThis, 'setTimeout', 'setTimeout')
|
||||
// TODO test
|
||||
io_patch(cxt, globalThis, 'clearTimeout', 'clearTimeout')
|
||||
|
||||
|
||||
// TODO test
|
||||
const Date = globalThis.Date
|
||||
io_patch(cxt, globalThis, 'Date', 'Date')
|
||||
globalThis.Date.parse = Date.parse
|
||||
globalThis.Date.now = Date.now
|
||||
globalThis.Date.UTC = Date.UTC
|
||||
io_patch(cxt, globalThis.Date, 'now', 'Date.now')
|
||||
|
||||
|
||||
io_patch(cxt, globalThis, 'fetch', 'fetch')
|
||||
// Check if Response is defined, for node.js
|
||||
if(globalThis.Response != null) {
|
||||
for(let key of Response_methods) {
|
||||
io_patch(cxt, Response.prototype, key, 'Response.prototype.' + key, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const remove_io_patches = cxt => {
|
||||
// TODO when to apply io_patches and promise_patches? Only once, when we
|
||||
// create window?
|
||||
|
||||
io_patch_remove(Math, 'random')
|
||||
|
||||
io_patch_remove(globalThis, 'setTimeout')
|
||||
// TODO test
|
||||
io_patch_remove(globalThis, 'clearTimeout')
|
||||
|
||||
io_patch_remove(globalThis, 'Date')
|
||||
io_patch_remove(globalThis, 'fetch')
|
||||
|
||||
// Check if Response is defined, for node.js
|
||||
if(globalThis.Response != null) {
|
||||
for(let key of Response_methods) {
|
||||
io_patch_remove(Response.prototype, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import {apply_io_patches, remove_io_patches} from './record_io.js'
|
||||
|
||||
/*
|
||||
Converts generator-returning function to promise-returning function. Allows to
|
||||
have the same code both for sync and async. If we have only sync modules (no
|
||||
@@ -27,10 +29,27 @@ const gen_to_promise = gen_fn => {
|
||||
}
|
||||
}
|
||||
|
||||
export const run = gen_to_promise(function*(module_fns, cxt){
|
||||
const do_run = function*(module_fns, cxt, io_cache){
|
||||
let calltree
|
||||
|
||||
cxt = io_cache == null
|
||||
// TODO move all io_cache properties to the object?
|
||||
? {...cxt,
|
||||
io_cache_is_recording: true,
|
||||
io_cache: [],
|
||||
}
|
||||
: {...cxt,
|
||||
io_cache_is_recording: false,
|
||||
io_cache,
|
||||
io_cache_resolver_is_set: false,
|
||||
// Map of (index in io_cache) -> resolve
|
||||
io_cache_resolvers: new Map(),
|
||||
io_cache_is_replay_aborted: false,
|
||||
io_cache_index: 0,
|
||||
}
|
||||
|
||||
apply_promise_patch(cxt)
|
||||
apply_io_patches(cxt)
|
||||
|
||||
for(let {module, fn} of module_fns) {
|
||||
cxt.found_call = null
|
||||
@@ -60,6 +79,7 @@ export const run = gen_to_promise(function*(module_fns, cxt){
|
||||
cxt.logs = []
|
||||
cxt.children = null
|
||||
|
||||
remove_io_patches(cxt)
|
||||
remove_promise_patch(cxt)
|
||||
|
||||
cxt.searched_location = null
|
||||
@@ -73,6 +93,20 @@ export const run = gen_to_promise(function*(module_fns, cxt){
|
||||
logs: _logs,
|
||||
eval_cxt: cxt,
|
||||
}
|
||||
}
|
||||
|
||||
export const run = gen_to_promise(function*(module_fns, cxt, io_cache) {
|
||||
const result = yield* do_run(module_fns, cxt, io_cache)
|
||||
|
||||
if(result.eval_cxt.io_cache_is_replay_aborted) {
|
||||
// TODO test next line
|
||||
result.eval_cxt.is_recording_deferred_calls = false
|
||||
|
||||
// run again without io cache
|
||||
return yield* do_run(module_fns, cxt, null)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
||||
const apply_promise_patch = cxt => {
|
||||
@@ -113,7 +147,7 @@ const remove_promise_patch = cxt => {
|
||||
cxt.Promise.prototype.then = cxt.promise_then
|
||||
}
|
||||
|
||||
const set_record_call = cxt => {
|
||||
export const set_record_call = cxt => {
|
||||
for(let i = 0; i < cxt.stack.length; i++) {
|
||||
cxt.stack[i] = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user