diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6823-node-island-nested-stack/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6823-node-island-nested-stack/main.ts.json new file mode 100644 index 000000000..aa900980e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6823-node-island-nested-stack/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6823-node-island-nested-stack/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 0f18756e1..3e3c7f265 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -28,6 +28,7 @@ #include "scr_runtime.h" #ifdef SCR_NODE_ISLANDS #include "scr_node_bridge.h" +#include "scr_node_island.h" #endif #include @@ -1659,6 +1660,10 @@ static void scr_switch(ScrCtx *from, ScrCtx *to, ScrFiber *to_fiber) { uintptr_t guard = scr_stack_guard; if (to_fiber) SCR_STACK_GUARD_SET(scr_stack_guard_for((uintptr_t)to_fiber->stack, SCR_FIBER_STACK)); #endif +#ifdef SCR_NODE_ISLANDS + /* Leaving Node's stack keeps this frame and those above it live. */ + scr_node_island_leave_stack((uintptr_t)__builtin_frame_address(0)); +#endif #ifdef _WIN32 /* SwitchToFiber snapshots the outgoing fiber's state inside its own * fiber object — `from` has nothing to record. */ diff --git a/packages/runtime/src/scr_node_island.c b/packages/runtime/src/scr_node_island.c index 3b21dffff..cad9d80e3 100644 --- a/packages/runtime/src/scr_node_island.c +++ b/packages/runtime/src/scr_node_island.c @@ -865,8 +865,10 @@ static bool ni_stack_has(uintptr_t sp) { return ni_stack != NULL && sp > (uintptr_t)ni_stack && sp <= (uintptr_t)ni_stack + NI_STACK; } -/* The lowest address of Node's stack its frames use while runtime code - * they called runs (a host callback below), 0 outside them. Runtime code +/* The lowest address of Node's stack in use while runtime code Node's + * frames called runs (a host callback below), 0 outside them: the + * callback's frame, or lower, where that code switched to a fiber's stack + * (scr_node_island_leave_stack) and left its own frames live. Runtime code * may resume an async fiber there, and the fiber may call into Node again: * that entry runs on Node's stack below this mark, so every frame of Node * stays on the one stack V8 scans (heap::base::Stack, bounded by @@ -883,6 +885,10 @@ static uintptr_t ni_mark = 0; } while (0) #define NI_MARK_END (ni_mark = ni_saved_mark) +void scr_node_island_leave_stack(uintptr_t sp) { + if (ni_mark != 0 && ni_stack_has(sp) && sp < ni_mark) ni_mark = sp; +} + /* A nested entry: job(arg) on Node's stack below the mark, from a fiber. */ static void (*ni_nested_job)(void *) = NULL; static void *ni_nested_arg = NULL; diff --git a/packages/runtime/src/scr_node_island.h b/packages/runtime/src/scr_node_island.h index 6c6dddf22..0374508e6 100644 --- a/packages/runtime/src/scr_node_island.h +++ b/packages/runtime/src/scr_node_island.h @@ -49,6 +49,11 @@ void scr_loop_hosted_io_state(struct ScrNodeIoState *state); void scr_loop_hosted_io(void); void scr_loop_hosted_microtasks_done(void); +/* Runtime code switches stacks (scr_switch) at `sp`: when that is Node's + * stack, the frames above stay live until it switches back, so a call + * into Node.js from the other stack runs below them. */ +void scr_node_island_leave_stack(uintptr_t sp); + /* Process events island code listens to (SCR_NODE_EVENT_* of the bridge * ABI). Their listeners sit in the runtime's registries (the * SCR_NODE_ISLANDS build of scr_events.c) at their registration position, diff --git a/tests/corpus/6823-node-island-nested-stack/echo.cjs b/tests/corpus/6823-node-island-nested-stack/echo.cjs new file mode 100644 index 000000000..f74a40e4e --- /dev/null +++ b/tests/corpus/6823-node-island-nested-stack/echo.cjs @@ -0,0 +1,8 @@ +// Returns its argument after using some of Node's stack. +function down(n, depth) { + return depth === 0 ? n : down(n, depth - 1); +} + +exports.echo = function (n) { + return down(n, 500); +}; diff --git a/tests/corpus/6823-node-island-nested-stack/main.ts b/tests/corpus/6823-node-island-nested-stack/main.ts new file mode 100644 index 000000000..f9539187f --- /dev/null +++ b/tests/corpus/6823-node-island-nested-stack/main.ts @@ -0,0 +1,26 @@ +// @requires-libnode +// A timer on Node's loop runs runtime code deep below the callback's own +// frame, and an async function it starts calls into Node.js before its +// first await: that call's frames go below the deepest live runtime frame +// on Node's stack, so the frames above it return intact. +import { callSync } from "@scriptc/node"; + +async function viaNode(n: number): Promise { + const echoed = callSync("./echo.cjs", "echo", n); + await null; + return echoed; +} + +const started: Promise[] = []; +function deep(depth: number, n: number): number { + const local = [depth, n, depth * n]; + if (depth > 0) return deep(depth - 1, n) + local[2]!; + started.push(viaNode(n)); + return local[0]! + local[1]!; +} + +console.log("loaded", callSync("./echo.cjs", "echo", 1)); +setTimeout(() => { + console.log("deep", deep(200, 3)); + void Promise.all(started).then((values) => console.log("echoed", values.join(","))); +}, 0); diff --git a/tests/harness/corpus-costs/6823-node-island-nested-stack/main.ts.json b/tests/harness/corpus-costs/6823-node-island-nested-stack/main.ts.json new file mode 100644 index 000000000..5a61731bd --- /dev/null +++ b/tests/harness/corpus-costs/6823-node-island-nested-stack/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 20000 }