diff --git a/src/cmd.js b/src/cmd.js index 8ab9cdf..4553c06 100644 --- a/src/cmd.js +++ b/src/cmd.js @@ -185,7 +185,8 @@ const do_external_imports_loaded = ( const result = eval_modules( state.parse_result.modules, state.parse_result.sorted, - external_imports + external_imports, + state.on_async_call, ) const next = apply_eval_result(state, result) @@ -208,6 +209,7 @@ const do_external_imports_loaded = ( state.parse_result.modules, state.parse_result.sorted, external_imports, + state.on_async_call, {index: node.index, module: state.current_module}, ) @@ -737,6 +739,10 @@ const move_cursor = (s, index) => { return do_move_cursor(state, index) } +const on_async_call = (state, ...args) => { + console.log('on_async_call', state, args) +} + const load_dir = (state, dir) => { const collect_files = dir => dir.kind == 'file' ? [dir] @@ -792,5 +798,6 @@ export const COMMANDS = { move_cursor, eval_selection, external_imports_loaded, + on_async_call, calltree: calltree_commands, } diff --git a/src/eval.js b/src/eval.js index 29c37b0..cb4395a 100644 --- a/src/eval.js +++ b/src/eval.js @@ -56,6 +56,16 @@ type Call = { type Node = ToplevelCall | Call */ +// TODO just export const Iframe_Function? +const make_function = globalThis.process != null + // Tests are run in Node.js, no iframe + ? (...args) => new Function(...args) + // Browser context, run code in iframe + : (...args) => { + const fn_constructor = globalThis.run_code.contentWindow.Function + return new fn_constructor(...args) + } + const codegen_function_expr = (node, cxt, name) => { const do_codegen = n => codegen(n, cxt) @@ -235,7 +245,13 @@ const codegen = (node, cxt, parent) => { } } -export const eval_modules = (modules, sorted, external_imports, location) => { +export const eval_modules = ( + modules, + sorted, + external_imports, + on_async_call, + location +) => { // TODO gensym __modules, __exports // TODO bug if module imported twice, once as external and as regular @@ -243,7 +259,7 @@ export const eval_modules = (modules, sorted, external_imports, location) => { const codestring = ` let children, prev_children - // TODO use native array for stack? + // TODO use native array for stack for perf? const stack = new Array() let call_counter = 0 @@ -252,6 +268,9 @@ export const eval_modules = (modules, sorted, external_imports, location) => { let searched_location let found_call + let is_recording_async_calls + let is_toplevel_call = true + const set_record_call = () => { for(let i = 0; i < stack.length; i++) { stack[i] = true @@ -259,12 +278,14 @@ export const eval_modules = (modules, sorted, external_imports, location) => { } const expand_calltree_node = (node) => { + is_recording_async_calls = false children = null try { node.fn.apply(node.context, node.args) } catch(e) { // do nothing. Exception was caught and recorded inside 'trace' } + is_recording_async_calls = true if(node.fn.__location != null) { // fn is hosted, it created call, this time with children const result = children[0] @@ -319,6 +340,9 @@ export const eval_modules = (modules, sorted, external_imports, location) => { let ok, value, error + const is_toplevel_call_copy = is_toplevel_call + is_toplevel_call = false + try { value = fn(...args) ok = true @@ -361,6 +385,12 @@ export const eval_modules = (modules, sorted, external_imports, location) => { children = [] } children.push(call) + + is_toplevel_call = is_toplevel_call_copy + + if(is_recording_async_calls && is_toplevel_call) { + on_async_call(children) + } } } @@ -437,7 +467,14 @@ export const eval_modules = (modules, sorted, external_imports, location) => { } const run = entrypoint => { - const __modules = {...external_imports} + + is_recording_async_calls = false + + const __modules = { + /* external_imports passed as an argument to function generated with + * 'new Function' constructor */ + ...external_imports + } let current_call ` @@ -468,6 +505,8 @@ export const eval_modules = (modules, sorted, external_imports, location) => { })() current_call.children = children if(!__modules['${m}'].calls.ok) { + is_recording_async_calls = true + children = null return __modules } ` @@ -475,6 +514,8 @@ export const eval_modules = (modules, sorted, external_imports, location) => { .join('') + ` + is_recording_async_calls = true + children = null return __modules } @@ -485,12 +526,27 @@ export const eval_modules = (modules, sorted, external_imports, location) => { } ` - const actions = new Function('external_imports', codestring)( + const actions = make_function( + 'external_imports', + 'on_async_call', + codestring + )( + /* external_imports */ external_imports == null - ? null - : map_object(external_imports, (name, {module}) => - ({exports: module, is_external: true}) + ? null + : map_object(external_imports, (name, {module}) => + ({exports: module, is_external: true}) + ) + , + + /* on_async_call */ + calls => { + on_async_call( + calls.map(c => + assign_code(modules, c) + ) ) + }, ) const calltree_actions = { @@ -581,6 +637,9 @@ account // Workaround with statement forbidden in strict mode (imposed by ES6 modules) // Also currently try/catch is not implemented TODO + + +// TODO also create in Iframe Context? const eval_codestring = new Function('codestring', 'scope', // Make a copy of `scope` to not mutate it with assignments ` diff --git a/src/index.js b/src/index.js index 621d8fc..de5a971 100644 --- a/src/index.js +++ b/src/index.js @@ -46,7 +46,10 @@ export const init = (container) => { }) read_modules().then(initial_state => { - state = get_initial_state(initial_state) + state = get_initial_state({ + ...initial_state, + on_async_call: (...args) => exec('on_async_call', ...args) + }) // Expose state for debugging globalThis.__state = state ui = new UI(container, state) diff --git a/test/test.js b/test/test.js index c8bea5a..9766d5d 100644 --- a/test/test.js +++ b/test/test.js @@ -2227,4 +2227,18 @@ const y = x()` ) }), + test_only('async calls', () => { + const code = ` + const fn = () => { + } + // Use Function constructor to exec impure code for testing + new Function('fn', 'globalThis.__run_async_call = fn')(fn) + ` + const i = test_initial_state(code, { + on_async_call: (calls) => {console.log('test on async call', calls)} + }) + globalThis.__run_async_call() + delete globalThis.__run_async_call + }), + ] diff --git a/test/utils.js b/test/utils.js index d1089d6..1378dd0 100644 --- a/test/utils.js +++ b/test/utils.js @@ -27,12 +27,15 @@ export const assert_code_error = (codestring, error) => { assert_equal(result.error, error) } -export const test_initial_state = code => { - return get_initial_state({ +export const test_initial_state = (code, state) => { + return get_initial_state( + { + ...state, files: typeof(code) == 'object' ? code : { '' : code}, entrypoint: '', current_module: '', - }) + }, + ) } export const stringify = val =>