From eaefbdf44ff5c4a1102a1bd0582bdc8e5c608513 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 21 Oct 2026 10:58:50 -0700 Subject: [PATCH] Host the runtime's event loop on Node's libuv once Node loads (NI-3) Scope-B milestone NI-3. When a program loads Node in-process, its pending ticks, promise jobs, timers and immediates move onto Node's queues in order (timers keep their deadlines relative to the moment Node began loading; the work of a phase the switch interrupted finishes first) and every later one is scheduled there, so the jobs of both sides interleave as in Node. The runtime's I/O hooks are watched from Node's loop, and its liveness is re-read before each poll phase, after each check phase and after every microtask checkpoint. Node's loop runs on Node's stack, and V8's limit follows whichever stack an entry comes from. Island signal and exit listeners join the runtime's registries at their position, so the runtime keeps owning every signal disposition; Node's own 'exit' emission is left to the runtime's exit sequence, and the exit code island code leaves in process.exitCode becomes the program's. Bridge ABI 3. Ordering programs (tests/corpus-node-islands-order, harness node-island-order.test.ts), corpus programs 6804-6811 and the node-island-process harness pin the behavior against Node. Co-Authored-By: Claude Opus 5.5 --- .../src/backend/executable-features.ts | 4 +- packages/compiler/src/node-islands/libnode.ts | 2 +- .../6804-node-island-signals/main.mjs.json | 6 + .../6805-node-island-exit-code/main.ts.json | 6 + .../main.ts.json | 6 + .../main.ts.json | 6 + .../main.ts.json | 6 + .../6809-node-island-stdio/main.ts.json | 6 + .../6810-node-island-children/main.ts.json | 6 + .../main.mjs.json | 6 + .../libnode-common/bridge/scr_node_bridge.cc | 406 +++++++- packages/libnode-common/test/bridge-smoke.c | 37 +- packages/libnode-common/test/bridge.test.ts | 2 +- .../runtime-pack-matrix.mjs | 11 + packages/runtime/src/scr_async.c | 931 ++++++++++++++++-- packages/runtime/src/scr_events.c | 132 ++- packages/runtime/src/scr_node_bridge.h | 107 +- packages/runtime/src/scr_node_island.c | 166 +++- packages/runtime/src/scr_node_island.h | 31 +- packages/runtime/src/scr_node_island_host.cjs | 179 +++- packages/runtime/src/scr_node_island_host.h | 179 +++- .../e01-island-timer-keeps-alive/main.ts | 20 + .../e02-island-io-keeps-alive/main.ts | 20 + .../e03-unref-server-exits/main.ts | 20 + .../e04-throw-in-io-step/main.ts | 22 + .../g01-fs-chain/main.ts | 23 + .../corpus-node-islands-order/g02-net/main.ts | 23 + .../g03-child/main.ts | 24 + .../corpus-node-islands-order/g04-dns/main.ts | 24 + .../g05-zlib/main.ts | 23 + .../g06-worker/main.ts | 23 + .../g07-internal-timer/main.ts | 23 + tests/corpus-node-islands-order/island.cjs | 183 ++++ .../p01-timers-interleave/main.ts | 22 + .../p02-tick-micro-immediate/main.ts | 23 + .../p03-busy-vs-island-timer/main.ts | 21 + .../p05-io-and-promises/main.ts | 22 + .../p07-microtask-fifo/main.ts | 23 + .../p08-timer-vs-immediate/main.ts | 22 + .../p09-deadline-order-under-busy/main.ts | 22 + .../p10-island-chain/main.ts | 23 + .../p11-island-real-io/main.ts | 24 + .../p12-island-io-vs-busy/main.ts | 22 + .../q01-promise-job-fifo/main.ts | 23 + .../q02-promise-batch/main.ts | 23 + .../q03-io-callback-queues/main.ts | 21 + .../q04-fs-promises/main.ts | 23 + .../q06-async-context/main.ts | 19 + .../q07-timer-from-io-vs-host-timer/main.ts | 22 + .../q09-native-promise-jobs/main.ts | 25 + .../q10-async-await-vs-island-jobs/main.ts | 29 + .../q11-ticks-vs-island-ticks/main.ts | 25 + .../s01-pending-in-main-body/main.ts | 38 + .../s02-load-in-timer-callback/main.ts | 35 + .../s03-load-in-immediate/main.ts | 28 + .../s04-load-in-promise-job/main.ts | 35 + .../s05-load-with-open-socket/main.ts | 41 + .../main.ts | 29 + .../corpus/6804-node-island-signals/main.mjs | 54 + .../6804-node-island-signals/process.cjs | 23 + .../corpus/6805-node-island-exit-code/main.ts | 15 + .../6805-node-island-exit-code/process.cjs | 7 + .../6806-node-island-exit-in-call/main.ts | 22 + .../6806-node-island-exit-in-call/process.cjs | 8 + .../main.ts | 17 + .../process.cjs | 7 + .../main.ts | 15 + .../process.cjs | 10 + tests/corpus/6809-node-island-stdio/main.ts | 21 + tests/corpus/6809-node-island-stdio/stdio.cjs | 21 + .../6810-node-island-children/children.cjs | 9 + .../corpus/6810-node-island-children/main.ts | 27 + .../island.cjs | 4 + .../main.mjs | 20 + .../node-island-process/island-rejection.ts | 6 + .../node-island-process/island-uncaught.ts | 7 + tests/fixtures/node-island-process/island.cjs | 20 + .../node-island-process/native-rejection.ts | 10 + .../node-island-process/native-uncaught.ts | 10 + tests/harness/node-island-order.test.ts | 115 +++ tests/harness/node-island-process.test.ts | 72 ++ tests/harness/node-island.test.ts | 2 +- 82 files changed, 3661 insertions(+), 164 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6804-node-island-signals/main.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6805-node-island-exit-code/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6806-node-island-exit-in-call/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6807-node-island-exit-in-island-timer/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6808-node-island-exit-from-program/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6809-node-island-stdio/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6810-node-island-children/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6811-node-island-uncaught-listener/main.mjs.json create mode 100644 tests/corpus-node-islands-order/e01-island-timer-keeps-alive/main.ts create mode 100644 tests/corpus-node-islands-order/e02-island-io-keeps-alive/main.ts create mode 100644 tests/corpus-node-islands-order/e03-unref-server-exits/main.ts create mode 100644 tests/corpus-node-islands-order/e04-throw-in-io-step/main.ts create mode 100644 tests/corpus-node-islands-order/g01-fs-chain/main.ts create mode 100644 tests/corpus-node-islands-order/g02-net/main.ts create mode 100644 tests/corpus-node-islands-order/g03-child/main.ts create mode 100644 tests/corpus-node-islands-order/g04-dns/main.ts create mode 100644 tests/corpus-node-islands-order/g05-zlib/main.ts create mode 100644 tests/corpus-node-islands-order/g06-worker/main.ts create mode 100644 tests/corpus-node-islands-order/g07-internal-timer/main.ts create mode 100644 tests/corpus-node-islands-order/island.cjs create mode 100644 tests/corpus-node-islands-order/p01-timers-interleave/main.ts create mode 100644 tests/corpus-node-islands-order/p02-tick-micro-immediate/main.ts create mode 100644 tests/corpus-node-islands-order/p03-busy-vs-island-timer/main.ts create mode 100644 tests/corpus-node-islands-order/p05-io-and-promises/main.ts create mode 100644 tests/corpus-node-islands-order/p07-microtask-fifo/main.ts create mode 100644 tests/corpus-node-islands-order/p08-timer-vs-immediate/main.ts create mode 100644 tests/corpus-node-islands-order/p09-deadline-order-under-busy/main.ts create mode 100644 tests/corpus-node-islands-order/p10-island-chain/main.ts create mode 100644 tests/corpus-node-islands-order/p11-island-real-io/main.ts create mode 100644 tests/corpus-node-islands-order/p12-island-io-vs-busy/main.ts create mode 100644 tests/corpus-node-islands-order/q01-promise-job-fifo/main.ts create mode 100644 tests/corpus-node-islands-order/q02-promise-batch/main.ts create mode 100644 tests/corpus-node-islands-order/q03-io-callback-queues/main.ts create mode 100644 tests/corpus-node-islands-order/q04-fs-promises/main.ts create mode 100644 tests/corpus-node-islands-order/q06-async-context/main.ts create mode 100644 tests/corpus-node-islands-order/q07-timer-from-io-vs-host-timer/main.ts create mode 100644 tests/corpus-node-islands-order/q09-native-promise-jobs/main.ts create mode 100644 tests/corpus-node-islands-order/q10-async-await-vs-island-jobs/main.ts create mode 100644 tests/corpus-node-islands-order/q11-ticks-vs-island-ticks/main.ts create mode 100644 tests/corpus-node-islands-order/s01-pending-in-main-body/main.ts create mode 100644 tests/corpus-node-islands-order/s02-load-in-timer-callback/main.ts create mode 100644 tests/corpus-node-islands-order/s03-load-in-immediate/main.ts create mode 100644 tests/corpus-node-islands-order/s04-load-in-promise-job/main.ts create mode 100644 tests/corpus-node-islands-order/s05-load-with-open-socket/main.ts create mode 100644 tests/corpus-node-islands-order/s06-load-with-pending-fs-and-unref/main.ts create mode 100644 tests/corpus/6804-node-island-signals/main.mjs create mode 100644 tests/corpus/6804-node-island-signals/process.cjs create mode 100644 tests/corpus/6805-node-island-exit-code/main.ts create mode 100644 tests/corpus/6805-node-island-exit-code/process.cjs create mode 100644 tests/corpus/6806-node-island-exit-in-call/main.ts create mode 100644 tests/corpus/6806-node-island-exit-in-call/process.cjs create mode 100644 tests/corpus/6807-node-island-exit-in-island-timer/main.ts create mode 100644 tests/corpus/6807-node-island-exit-in-island-timer/process.cjs create mode 100644 tests/corpus/6808-node-island-exit-from-program/main.ts create mode 100644 tests/corpus/6808-node-island-exit-from-program/process.cjs create mode 100644 tests/corpus/6809-node-island-stdio/main.ts create mode 100644 tests/corpus/6809-node-island-stdio/stdio.cjs create mode 100644 tests/corpus/6810-node-island-children/children.cjs create mode 100644 tests/corpus/6810-node-island-children/main.ts create mode 100644 tests/corpus/6811-node-island-uncaught-listener/island.cjs create mode 100644 tests/corpus/6811-node-island-uncaught-listener/main.mjs create mode 100644 tests/fixtures/node-island-process/island-rejection.ts create mode 100644 tests/fixtures/node-island-process/island-uncaught.ts create mode 100644 tests/fixtures/node-island-process/island.cjs create mode 100644 tests/fixtures/node-island-process/native-rejection.ts create mode 100644 tests/fixtures/node-island-process/native-uncaught.ts create mode 100644 tests/harness/node-island-order.test.ts create mode 100644 tests/harness/node-island-process.test.ts diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 0aff57b86..86fbb73b3 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -33,7 +33,9 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), ...(features.dynBuffer ? { dynBuffer: true } : {}), ...(features.builtinValues ? { builtinValues: true } : {}), - events: features.processEvents, + // Island code's signal and exit listeners join the events unit's + // registries (scope-B NI-3). + events: features.processEvents || features.nodeIsland === true, emitter: features.emitter, symbol: features.symbol, bigint: features.bigint, diff --git a/packages/compiler/src/node-islands/libnode.ts b/packages/compiler/src/node-islands/libnode.ts index 2ba056023..bf6db36c2 100644 --- a/packages/compiler/src/node-islands/libnode.ts +++ b/packages/compiler/src/node-islands/libnode.ts @@ -22,7 +22,7 @@ import { basename, isAbsolute, join, resolve } from "node:path"; export const NODE_ISLAND_MIN_NODE = "24.13.1"; /** The bridge ABI this compiler's runtime speaks (scr_node_bridge.h). */ -export const NODE_BRIDGE_ABI = 2; +export const NODE_BRIDGE_ABI = 3; /** The prefix of a recorded path relative to the executable's directory. */ export const ORIGIN = "$ORIGIN/"; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6804-node-island-signals/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6804-node-island-signals/main.mjs.json new file mode 100644 index 000000000..ba3904bc3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6804-node-island-signals/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6804-node-island-signals/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6805-node-island-exit-code/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6805-node-island-exit-code/main.ts.json new file mode 100644 index 000000000..d00f9ae2d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6805-node-island-exit-code/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6805-node-island-exit-code/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6806-node-island-exit-in-call/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6806-node-island-exit-in-call/main.ts.json new file mode 100644 index 000000000..fbf105213 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6806-node-island-exit-in-call/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6806-node-island-exit-in-call/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6807-node-island-exit-in-island-timer/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6807-node-island-exit-in-island-timer/main.ts.json new file mode 100644 index 000000000..94e8c0207 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6807-node-island-exit-in-island-timer/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6807-node-island-exit-in-island-timer/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6808-node-island-exit-from-program/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6808-node-island-exit-from-program/main.ts.json new file mode 100644 index 000000000..c7c22efaf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6808-node-island-exit-from-program/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6808-node-island-exit-from-program/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6809-node-island-stdio/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6809-node-island-stdio/main.ts.json new file mode 100644 index 000000000..a33259b1d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6809-node-island-stdio/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6809-node-island-stdio/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6810-node-island-children/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6810-node-island-children/main.ts.json new file mode 100644 index 000000000..220ebcd2f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6810-node-island-children/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6810-node-island-children/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6811-node-island-uncaught-listener/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6811-node-island-uncaught-listener/main.mjs.json new file mode 100644 index 000000000..f2385e3b2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6811-node-island-uncaught-listener/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6811-node-island-uncaught-listener/main.mjs" + ], + "diags": [] +} diff --git a/packages/libnode-common/bridge/scr_node_bridge.cc b/packages/libnode-common/bridge/scr_node_bridge.cc index 1b578a0f7..7902c2d79 100644 --- a/packages/libnode-common/bridge/scr_node_bridge.cc +++ b/packages/libnode-common/bridge/scr_node_bridge.cc @@ -9,8 +9,17 @@ // no default signal handling, no debug-signal handler or inspector, no // stdio reinitialization, one platform worker thread. Node's process exit // (process.exit, an uncaught exception) goes to the runtime's exit -// sequence. Calls run in a CallbackScope with a nested TryCatch, so ticks -// and microtasks drain when the scope closes, as after any libuv callback. +// sequence. +// +// One loop (scope-B NI-3). Once the runtime calls host_loop, its queues +// live on Node's: its microtasks are V8 microtasks, its ticks, timers and +// immediates are Node's (scheduled through the bootstrap's helpers), and +// its I/O is watched from Node's libuv loop (uv_poll handles over the fds +// the runtime reports, one prepare handle whose ref is the runtime's +// liveness, an idle handle while runtime work is ready without any fd). +// Calls into JavaScript therefore do not drain queues themselves: the +// runtime's checkpoints do, through `checkpoint`, and Node's own after +// every callback it dispatches. #include "scr_node_bridge.h" #include @@ -20,10 +29,12 @@ #include #include +#include #include #include #include #include +#include #include namespace { @@ -49,6 +60,10 @@ node::EmbedderSnapshotData::Pointer g_snapshot; std::unique_ptr g_setup; Locker *g_locker = nullptr; Global g_call; +// The bootstrap's hosted-loop helpers (scr_node_island_host.cjs). +Global g_set_timer, g_clear_timer, g_ref_timer, g_refresh_timer; +Global g_set_immediate, g_clear_immediate, g_ref_immediate; +Global g_next_tick, g_emit; char *Copy(const std::string &text, size_t *len) { char *out = static_cast(std::malloc(text.size() + 1)); @@ -59,6 +74,29 @@ char *Copy(const std::string &text, size_t *len) { return out; } +// V8's stack limit follows the stack the runtime calls in from: its main +// thread's, or an async fiber's (a few hundred KiB, mapped elsewhere), so +// JavaScript entered from a fiber neither trips a false stack overflow nor +// overruns the fiber. Restored when the entry returns. +uintptr_t g_stack_limit = 0; + +struct StackLimit { + uintptr_t previous; + explicit StackLimit(Isolate *isolate) : previous(g_stack_limit) { + uintptr_t want = g_host->stack_limit(); + if (want != 0 && want != g_stack_limit) { + isolate->SetStackLimit(want); + g_stack_limit = want; + } + } + ~StackLimit() { + if (previous != 0 && previous != g_stack_limit) { + g_setup->isolate()->SetStackLimit(previous); + g_stack_limit = previous; + } + } +}; + Local Utf8(Isolate *isolate, const char *text, size_t len) { return String::NewFromUtf8(isolate, text, NewStringType::kNormal, static_cast(len)) .ToLocalChecked(); @@ -96,6 +134,39 @@ void Settle(const FunctionCallbackInfo &info) { g_host->settle(id, outcome.data(), outcome.size()); } +uint32_t Uint32Arg(const FunctionCallbackInfo &info, int i) { + return info[i]->Uint32Value(info.GetIsolate()->GetCurrentContext()).FromMaybe(0); +} + +// run(kind, token): a runtime job Node's queues dispatched. +void Run(const FunctionCallbackInfo &info) { + g_host->run(Uint32Arg(info, 0), Uint32Arg(info, 1)); +} + +// listen(event, name, listener, add, once): island process listeners. +void Listen(const FunctionCallbackInfo &info) { + std::string name = ToStdString(info.GetIsolate(), info[1]); + g_host->listen(Uint32Arg(info, 0), name.c_str(), Uint32Arg(info, 2), Uint32Arg(info, 3) != 0, + Uint32Arg(info, 4) != 0); +} + +void SetMethod(Local context, Local target, const char *name, + v8::FunctionCallback callback) { + target + ->Set(context, Utf8(context->GetIsolate(), name), + Function::New(context, callback).ToLocalChecked()) + .Check(); +} + +bool TakeFunction(Isolate *isolate, Local context, Local from, const char *name, + Global *to) { + Local value; + if (!from->Get(context, Utf8(isolate, name)).ToLocal(&value) || !value->IsFunction()) + return false; + to->Reset(isolate, value.As()); + return true; +} + char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *argv0, uintptr_t stack_limit) { if (g_setup) return nullptr; @@ -140,8 +211,10 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar g_locker = new Locker(isolate); isolate->Enter(); // The runtime calls into Node on a dedicated stack of its own - // (scr_node_island.c), not the thread's: V8's limit follows it. + // (scr_node_island.c), not the thread's: V8's limit follows it, and + // follows any other stack an entry comes from (StackLimit). if (stack_limit != 0) isolate->SetStackLimit(stack_limit); + g_stack_limit = stack_limit; node::SetProcessExitHandler(g_setup->env(), [](node::Environment *, int code) { g_host->exit(code); }); HandleScope handles(isolate); @@ -149,9 +222,9 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar Context::Scope context_scope(context); Local global = context->Global(); Local native = Object::New(isolate); - native - ->Set(context, Utf8(isolate, "settle"), Function::New(context, Settle).ToLocalChecked()) - .Check(); + SetMethod(context, native, "settle", Settle); + SetMethod(context, native, "run", Run); + SetMethod(context, native, "listen", Listen); global->Set(context, Utf8(isolate, "__scriptcNodeIslandHost"), native).Check(); TryCatch try_catch(isolate); if (node::LoadEnvironment(g_setup->env(), bootstrap).IsEmpty()) { @@ -170,13 +243,21 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar return Copy(message, nullptr); } Local dispatcher; - Local call; if (!global->Get(context, Utf8(isolate, "__scriptcNodeIsland")).ToLocal(&dispatcher) || - !dispatcher->IsObject() || - !dispatcher.As()->Get(context, Utf8(isolate, "call")).ToLocal(&call) || - !call->IsFunction()) + !dispatcher->IsObject()) return Copy("the Node island bootstrap left no dispatcher", nullptr); - g_call.Reset(isolate, call.As()); + Local d = dispatcher.As(); + if (!TakeFunction(isolate, context, d, "call", &g_call) || + !TakeFunction(isolate, context, d, "setTimer", &g_set_timer) || + !TakeFunction(isolate, context, d, "clearTimer", &g_clear_timer) || + !TakeFunction(isolate, context, d, "refTimer", &g_ref_timer) || + !TakeFunction(isolate, context, d, "refreshTimer", &g_refresh_timer) || + !TakeFunction(isolate, context, d, "setImmediate", &g_set_immediate) || + !TakeFunction(isolate, context, d, "clearImmediate", &g_clear_immediate) || + !TakeFunction(isolate, context, d, "refImmediate", &g_ref_immediate) || + !TakeFunction(isolate, context, d, "nextTick", &g_next_tick) || + !TakeFunction(isolate, context, d, "emit", &g_emit)) + return Copy("the Node island bootstrap left an incomplete dispatcher", nullptr); global->Delete(context, Utf8(isolate, "__scriptcNodeIsland")).FromMaybe(false); global->Delete(context, Utf8(isolate, "__scriptcNodeIslandHost")).FromMaybe(false); return nullptr; @@ -185,12 +266,14 @@ char *Start(const ScrNodeBridgeHost *host, const char *bootstrap, const char *ar char *Call(uint32_t id, int sync, const char *file, const char *specifier, const char *name, const char *args, size_t args_len, size_t *len) { Isolate *isolate = g_setup->isolate(); + StackLimit limit(isolate); HandleScope handles(isolate); Local context = g_setup->context(); Context::Scope context_scope(context); std::string outcome; { - node::CallbackScope scope(isolate, context->Global(), node::async_context{0, 0}); + // No callback scope: the runtime's checkpoint drains what the call + // queued, at the end of the runtime callback that made it (NI-3). TryCatch try_catch(isolate); Local argv[] = {Integer::NewFromUnsigned(isolate, id), v8::Boolean::New(isolate, sync != 0), @@ -218,25 +301,286 @@ char *Call(uint32_t id, int sync, const char *file, const char *specifier, const void Release(void *memory) { std::free(memory); } -int BackendFd() { return uv_backend_fd(g_setup->event_loop()); } +// ── the hosted loop ────────────────────────────────────────────────────── -int BackendTimeout() { return uv_backend_timeout(g_setup->event_loop()); } +struct Enter { + Isolate *isolate; + StackLimit limit; + HandleScope handles; + Local context; + Context::Scope scope; + Enter() + : isolate(g_setup->isolate()), limit(isolate), handles(isolate), + context(g_setup->context()), scope(context) {} +}; -int Alive() { return uv_loop_alive(g_setup->event_loop()); } +// Calls a bootstrap helper; helpers do not throw, a failure answers empty. +Local Helper(Enter &e, Global &fn, int argc, Local *argv) { + TryCatch try_catch(e.isolate); + Local result; + if (!fn.Get(e.isolate)->Call(e.context, v8::Undefined(e.isolate), argc, argv).ToLocal(&result)) + return Local(); + return result; +} -void Pump() { - Isolate *isolate = g_setup->isolate(); - HandleScope handles(isolate); - Context::Scope context_scope(g_setup->context()); +Local Num(Enter &e, double value) { return v8::Number::New(e.isolate, value); } + +uint32_t HelperHandle(Enter &e, Local result) { + if (result.IsEmpty()) return 0; + return result->Uint32Value(e.context).FromMaybe(0); +} + +struct PollEntry { + uv_poll_t handle; + int fd; + bool seen; + bool active; +}; + +uv_prepare_t g_prepare; +uv_check_t g_check; +uv_idle_t g_idle; +uv_timer_t g_cap; +bool g_hosted = false; +bool g_trace = false; // SCRIPTC_NODE_ISLANDS_TRACE=loop: the I/O state on stderr +bool g_dispatched = false; // the runtime's stations ran this iteration +std::unordered_map g_polls; + +void Dispatch() { + if (g_dispatched) return; + g_dispatched = true; + g_host->io_dispatch(); +} + +void SyncIo(int phase); + +void OnPoll(uv_poll_t *, int, int) { + Dispatch(); + SyncIo(SCR_NODE_IO_JOBS); +} + +void OnIdle(uv_idle_t *) { + Dispatch(); + SyncIo(SCR_NODE_IO_JOBS); +} + +void OnCap(uv_timer_t *) { + Dispatch(); + SyncIo(SCR_NODE_IO_JOBS); +} + +void OnPrepare(uv_prepare_t *) { SyncIo(SCR_NODE_IO_PREPARE); } + +// The end of an iteration: the next one may dispatch again (from its idle +// phase when work is ready, else from its poll phase). +void OnCheck(uv_check_t *) { + g_dispatched = false; + SyncIo(SCR_NODE_IO_CHECK); +} + +void ClosePoll(uv_handle_t *handle) { delete reinterpret_cast(handle->data); } + +// Mirrors the runtime's I/O state onto the loop: one unref'd poll handle +// per fd, the prepare handle's ref as the runtime's liveness, the idle +// handle while work is ready, the cap timer for work no fd announces. A +// poll handle whose fd leaves the set is stopped, not closed, and started +// again when the fd returns (the worker pool's wake pipe comes and goes +// with every job): libuv cannot watch an fd with a second handle while the +// first is still closing. +void SyncIo(int phase) { + if (!g_hosted) return; + ScrNodeIoState state; + state.phase = phase; + g_host->io_state(&state); + if (g_trace) { + std::fprintf(stderr, "[node-islands] %s alive=%d ready=%d cap=%d fds=", + phase == SCR_NODE_IO_CHECK ? "check" : phase == SCR_NODE_IO_JOBS ? "jobs" : "prepare", + state.alive, state.ready, state.cap_ms); + for (int i = 0; i < state.nfds; i++) std::fprintf(stderr, "%s%d", i ? "," : "", state.fds[i]); + std::fprintf(stderr, "\n"); + } + uv_loop_t *loop = g_setup->event_loop(); + for (auto &entry : g_polls) entry.second->seen = false; + for (int i = 0; i < state.nfds; i++) { + int fd = state.fds[i]; + auto found = g_polls.find(fd); + PollEntry *entry; + if (found != g_polls.end()) { + entry = found->second; + } else { + entry = new PollEntry(); + entry->fd = fd; + entry->active = false; + if (uv_poll_init(loop, &entry->handle, fd) != 0) { + delete entry; + continue; + } + entry->handle.data = entry; + uv_unref(reinterpret_cast(&entry->handle)); + g_polls.emplace(fd, entry); + } + entry->seen = true; + if (!entry->active && uv_poll_start(&entry->handle, UV_READABLE, OnPoll) == 0) + entry->active = true; + } + for (auto &it : g_polls) { + if (it.second->seen || !it.second->active) continue; + uv_poll_stop(&it.second->handle); + it.second->active = false; + } + if (state.alive) uv_ref(reinterpret_cast(&g_prepare)); + else uv_unref(reinterpret_cast(&g_prepare)); + if (state.ready) uv_idle_start(&g_idle, OnIdle); + else uv_idle_stop(&g_idle); + if (state.cap_ms >= 0) { + if (!uv_is_active(reinterpret_cast(&g_cap))) + uv_timer_start(&g_cap, OnCap, static_cast(state.cap_ms), 0); + } else { + uv_timer_stop(&g_cap); + } +} + +// After every microtask checkpoint, which ends every callback Node +// dispatches (closing-phase ones included): the runtime's rejections, and +// its liveness, which the callback may have changed after the turn's check. +void OnMicrotasksDone(Isolate *, void *) { + if (!g_hosted) return; + g_host->microtasks_done(); + SyncIo(SCR_NODE_IO_JOBS); +} + +void HostLoop() { + if (g_hosted) return; + uv_loop_t *loop = g_setup->event_loop(); + uv_prepare_init(loop, &g_prepare); + uv_prepare_start(&g_prepare, OnPrepare); + uv_check_init(loop, &g_check); + uv_check_start(&g_check, OnCheck); + uv_unref(reinterpret_cast(&g_check)); + uv_idle_init(loop, &g_idle); + uv_unref(reinterpret_cast(&g_idle)); + uv_timer_init(loop, &g_cap); + uv_unref(reinterpret_cast(&g_cap)); + g_setup->isolate()->AddMicrotasksCompletedCallback(OnMicrotasksDone, nullptr); + const char *trace = std::getenv("SCRIPTC_NODE_ISLANDS_TRACE"); + g_trace = trace != nullptr && std::strcmp(trace, "loop") == 0; + g_hosted = true; + SyncIo(SCR_NODE_IO_JOBS); +} + +void UnhostLoop() { + if (!g_hosted) return; + g_hosted = false; + g_setup->isolate()->RemoveMicrotasksCompletedCallback(OnMicrotasksDone, nullptr); + for (auto &entry : g_polls) { + uv_poll_stop(&entry.second->handle); + uv_close(reinterpret_cast(&entry.second->handle), ClosePoll); + } + g_polls.clear(); + uv_close(reinterpret_cast(&g_prepare), nullptr); + uv_close(reinterpret_cast(&g_check), nullptr); + uv_close(reinterpret_cast(&g_idle), nullptr); + uv_close(reinterpret_cast(&g_cap), nullptr); uv_run(g_setup->event_loop(), UV_RUN_NOWAIT); - g_platform->DrainTasks(isolate); +} + +void RunMicrotask(void *data) { + g_host->run(SCR_NODE_JOB_MICROTASK, static_cast(reinterpret_cast(data))); +} + +void Microtask(uint32_t token) { + g_setup->isolate()->EnqueueMicrotask(RunMicrotask, + reinterpret_cast(static_cast(token))); +} + +void Tick(uint32_t token) { + Enter e; + Local argv[] = {Num(e, token)}; + Helper(e, g_next_tick, 1, argv); +} + +uint32_t Timer(double delay, double repeat, int ref, uint32_t token) { + Enter e; + Local argv[] = {Num(e, delay), Num(e, repeat), v8::Boolean::New(e.isolate, ref != 0), + Num(e, token)}; + return HelperHandle(e, Helper(e, g_set_timer, 4, argv)); +} + +void TimerClear(uint32_t handle) { + Enter e; + Local argv[] = {Num(e, handle)}; + Helper(e, g_clear_timer, 1, argv); +} + +void TimerRef(uint32_t handle, int ref) { + Enter e; + Local argv[] = {Num(e, handle), v8::Boolean::New(e.isolate, ref != 0)}; + Helper(e, g_ref_timer, 2, argv); +} + +void TimerRefresh(uint32_t handle) { + Enter e; + Local argv[] = {Num(e, handle)}; + Helper(e, g_refresh_timer, 1, argv); +} + +uint32_t Immediate(int ref, uint32_t token) { + Enter e; + Local argv[] = {v8::Boolean::New(e.isolate, ref != 0), Num(e, token)}; + return HelperHandle(e, Helper(e, g_set_immediate, 2, argv)); +} + +void ImmediateClear(uint32_t handle) { + Enter e; + Local argv[] = {Num(e, handle)}; + Helper(e, g_clear_immediate, 1, argv); +} + +void ImmediateRef(uint32_t handle, int ref) { + Enter e; + Local argv[] = {Num(e, handle), v8::Boolean::New(e.isolate, ref != 0)}; + Helper(e, g_ref_immediate, 2, argv); +} + +// The checkpoint after a runtime macrotask: an empty callback scope, whose +// close runs Node's tick queue and V8's microtasks (InternalCallbackScope:: +// Close) unless Node's own dispatch is on the stack, which drains after. +void Checkpoint(int microtasks_first) { + Enter e; + if (microtasks_first) e.context->GetMicrotaskQueue()->PerformCheckpoint(e.isolate); + node::CallbackScope scope(e.isolate, Object::New(e.isolate), node::async_context{0, 0}); +} + +int RunLoop(int continue_turn) { + Enter e; + // The rest of the turn the switch interrupted: libuv's UV_RUN_ONCE skips + // the leading timers phase and runs poll, check, closing, then timers. + if (continue_turn) { + uv_run(g_setup->event_loop(), UV_RUN_ONCE); + g_platform->DrainTasks(e.isolate); + } + return node::SpinEventLoop(g_setup->env()).FromMaybe(0); +} + +// An island listener of a process event: through MakeCallback, so a throw +// is an uncaught exception as in Node's own emit. +void Emit(uint32_t event, uint32_t listener, const char *name, int arg) { + Enter e; + Local argv[] = {Num(e, event), Num(e, listener), Utf8(e.isolate, name), Num(e, arg)}; + node::MakeCallback(e.isolate, e.context->Global(), g_emit.Get(e.isolate), 4, argv, + node::async_context{0, 0}); } void Stop() { if (!g_setup) return; + UnhostLoop(); Isolate *isolate = g_setup->isolate(); node::Stop(g_setup->env()); g_call.Reset(); + for (Global *fn : + {&g_set_timer, &g_clear_timer, &g_ref_timer, &g_refresh_timer, &g_set_immediate, + &g_clear_immediate, &g_ref_immediate, &g_next_tick, &g_emit}) + fn->Reset(); isolate->Exit(); delete g_locker; g_locker = nullptr; @@ -247,8 +591,26 @@ void Stop() { } const ScrNodeBridge kBridge = { - SCR_NODE_BRIDGE_ABI, NODE_VERSION_STRING, NODE_MODULE_VERSION, Start, Call, Release, - BackendFd, BackendTimeout, Alive, Pump, Stop, + SCR_NODE_BRIDGE_ABI, + NODE_VERSION_STRING, + NODE_MODULE_VERSION, + Start, + Call, + Release, + HostLoop, + Microtask, + Tick, + Timer, + TimerClear, + TimerRef, + TimerRefresh, + Immediate, + ImmediateClear, + ImmediateRef, + Checkpoint, + RunLoop, + Emit, + Stop, }; } // namespace diff --git a/packages/libnode-common/test/bridge-smoke.c b/packages/libnode-common/test/bridge-smoke.c index b4437333a..9c37946f1 100644 --- a/packages/libnode-common/test/bridge-smoke.c +++ b/packages/libnode-common/test/bridge-smoke.c @@ -2,10 +2,10 @@ * check its ABI, start Node from the snapshot with the runtime's embedded * bootstrap) and makes calls through the bridge: synchronous results and * errors, and an asynchronous call settled by Node's own loop, which this - * host drives like the runtime's loop slot. bridge.test.ts compares the - * transcript. Usage: bridge-smoke */ + * host runs the way the runtime's hosted loop does (host_loop, a + * checkpoint, run_loop) with no runtime work of its own. bridge.test.ts + * compares the transcript. Usage: bridge-smoke */ #include -#include #include #include #include @@ -26,7 +26,28 @@ static void on_exit_code(int code) { _Exit(code); } -static const ScrNodeBridgeHost host = {settle, on_exit_code}; +static void run(uint32_t kind, uint32_t token) { + printf("run %u %u\n", kind, token); +} + +static void io_state(ScrNodeIoState *state) { + state->nfds = 0; + state->alive = 0; + state->ready = 0; + state->cap_ms = -1; +} + +static void io_dispatch(void) {} +static void microtasks_done(void) {} + +static void listen(uint32_t event, const char *name, uint32_t listener, int add, int once) { + printf("listen %u %s %u %d %d\n", event, name, listener, add, once); +} + +static uintptr_t stack_limit(void) { return 0; } + +static const ScrNodeBridgeHost host = {settle, on_exit_code, run, io_state, io_dispatch, + microtasks_done, listen, stack_limit}; static void call(const ScrNodeBridge *b, uint32_t id, int sync, const char *module, const char *name, const char *args) { @@ -64,11 +85,9 @@ int main(int argc, char **argv) { call(b, 0, 1, file, "fails", "[]"); call(b, 0, 1, file, "missing", "[]"); call(b, 7, 0, file, "later", "[5]"); - while (settled == 0 || b->alive()) { - struct pollfd fd = {b->backend_fd(), POLLIN, 0}; - (void)poll(&fd, 1, b->backend_timeout()); - b->pump(); - } + b->host_loop(); + b->checkpoint(0); + b->run_loop(0); call(b, 0, 1, file, "quit", "[3]"); return 0; } diff --git a/packages/libnode-common/test/bridge.test.ts b/packages/libnode-common/test/bridge.test.ts index 1ab6ab95b..4c37b31a3 100644 --- a/packages/libnode-common/test/bridge.test.ts +++ b/packages/libnode-common/test/bridge.test.ts @@ -45,7 +45,7 @@ exports.quit = (code) => process.exit(code); expect(result.stderr).toBe(""); expect(result.stdout).toBe( [ - "abi 2 ok 1", + "abi 3 ok 1", "sync add -> R5", 'sync fails -> T{"error":{"kind":2,"message":"out of range","code":"ERR_X"}}', `sync missing -> T{"error":{"kind":1,"message":"'missing' exported by './smoke.cjs' is not a function"}}`, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index e6b7c863a..1058ccf81 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -188,6 +188,17 @@ export function variantsFor(source) { when: { stdioExtended: true }, defines: ["SCR_STDIO_EXTENDED"], }); + // Island listeners for signals and 'exit' join the registries. + variants.push({ + id: "node-island", + when: { nodeIsland: true }, + defines: ["SCR_NODE_ISLANDS"], + }); + variants.push({ + id: "stdio-extended-node-island", + when: { stdioExtended: true, nodeIsland: true }, + defines: ["SCR_STDIO_EXTENDED", "SCR_NODE_ISLANDS"], + }); } // Programs with Node islands link the event loop with the Node island slot; // every other program keeps the default object, byte for byte. diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 672621f6b..d5108101d 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -26,6 +26,9 @@ #define _DEFAULT_SOURCE 1 #endif #include "scr_runtime.h" +#ifdef SCR_NODE_ISLANDS +#include "scr_node_bridge.h" +#endif #include #include @@ -815,7 +818,44 @@ void scr_wasi_coro_started(void *handle) { static SCR_TL ScrFiber **scr_ready = NULL; static SCR_TL size_t scr_ready_head = 0, scr_ready_len = 0, scr_ready_cap = 0; +#ifdef SCR_NODE_ISLANDS +/* The hosted loop (scope-B NI-3, defined below the loop hooks): once Node + * is loaded, the runtime's queues live on Node's and this names the + * libnode bridge that schedules them. NULL until then, and in programs + * that never load Node. */ +static SCR_TL const ScrNodeBridge *scr_loop_hosted = NULL; +/* Runtime ticks and microtasks queued on Node and not yet run: the + * dispatch stations' yield signal (scr_loop_has_ready). */ +static SCR_TL size_t scr_hosted_queued = 0; +/* The end of the check phase the loop pass is running (0: none). */ +static SCR_TL size_t scr_check_end = 0; +/* Where the loop pass is, for a switch in the middle of it: the turn + * continues at Node's poll phase after a timers or I/O phase, and a + * microtask drain in progress finishes before the ticks. */ +enum { SCR_PASS_START, SCR_PASS_IO, SCR_PASS_TIMERS, SCR_PASS_CHECK }; +static SCR_TL int scr_pass_phase = SCR_PASS_START; +static SCR_TL bool scr_in_microtask = false; +static SCR_TL bool scr_hosted_continue_turn = false; +static SCR_TL bool scr_hosted_microtasks_first = false; +#define SCR_PASS_PHASE(phase) (scr_pass_phase = (phase)) +#define SCR_RESUME_JOB(f) (scr_in_microtask = true, scr_resume_fiber(f), scr_in_microtask = false) +static void scr_hosted_microtask(ScrFiber *f); +/* A microtask turn may continue in place only while the runtime's own + * queue is the whole queue: hosted, V8's queue may hold Node's jobs. */ +#define SCR_READY_IDLE() (scr_ready_len == 0 && scr_loop_hosted == NULL) +#else +#define SCR_READY_IDLE() (scr_ready_len == 0) +#define SCR_PASS_PHASE(phase) ((void)0) +#define SCR_RESUME_JOB(f) scr_resume_fiber(f) +#endif + static void scr_ready_push(ScrFiber *f) { +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) { + scr_hosted_microtask(f); + return; + } +#endif if (scr_ready_head + scr_ready_len == scr_ready_cap) { if (scr_ready_len > 0) { memmove(scr_ready, scr_ready + scr_ready_head, scr_ready_len * sizeof *scr_ready); @@ -948,7 +988,25 @@ static void scr_timer_swap(size_t i, size_t j) { scr_timer_set(j, tmp); } +#ifdef SCR_NODE_ISLANDS +static void scr_hosted_timer_push(ScrTimer t); +/* The hosted timer, immediate and tick operations (defined with the + * hosted loop below). Each answers false when the handle names nothing. */ +enum { SCR_HOSTED_CLEAR, SCR_HOSTED_REF, SCR_HOSTED_UNREF, SCR_HOSTED_REFRESH, SCR_HOSTED_HAS_REF }; +static bool scr_hosted_timer_op(unsigned long id, int op); +static double scr_hosted_set_immediate(ScrClosure *cb); +static bool scr_hosted_immediate_op(unsigned long id, int op); +static size_t scr_hosted_count(int kind); +static void scr_hosted_teardown(void); +#endif + static void scr_timer_push(ScrTimer t) { +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) { + scr_hosted_timer_push(t); + return; + } +#endif if (scr_ntimers == scr_timers_cap) { scr_timers_cap = scr_timers_cap ? scr_timers_cap * 2 : 16; scr_timers = realloc(scr_timers, scr_timers_cap * sizeof *scr_timers); @@ -1081,6 +1139,12 @@ void scr_timer_unref(double handle) { scr_firing_reffed = false; return; } +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) { + scr_hosted_timer_op(id, SCR_HOSTED_UNREF); + return; + } +#endif ScrTimer *t = scr_timer_find(id); if (t && t->reffed) { t->reffed = false; @@ -1095,6 +1159,12 @@ void scr_timer_ref(double handle) { scr_firing_reffed = true; return; } +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) { + scr_hosted_timer_op(id, SCR_HOSTED_REF); + return; + } +#endif ScrTimer *t = scr_timer_find(id); if (t && !t->reffed) { t->reffed = true; @@ -1118,6 +1188,12 @@ void scr_timer_refresh(double handle) { scr_firing_refresh = true; return; } +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) { + scr_hosted_timer_op(id, SCR_HOSTED_REFRESH); + return; + } +#endif ScrTimer *found = scr_timer_find(id); if (found) { ScrTimer t = *found; @@ -1133,6 +1209,9 @@ bool scr_timer_has_ref(double handle) { if (!(handle >= 1)) return false; unsigned long id = (unsigned long)handle; if (id == scr_firing_id) return scr_firing_reffed; +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) return scr_hosted_timer_op(id, SCR_HOSTED_HAS_REF); +#endif ScrTimer *t = scr_timer_find(id); return t != NULL && t->reffed; } @@ -1149,8 +1228,19 @@ static SCR_TL size_t scr_pending_immediates; /* defined below with the queue */ ScrArr *scr_active_resources(void) { ScrArr *arr = scr_arr_new(SCR_ELEM_STR, 8); size_t timeouts = scr_ntimers + ((scr_firing_id != 0 && !scr_firing_cleared) ? 1 : 0); +#ifdef SCR_NODE_ISLANDS + /* Hosted: the runtime's own (Node's are Node's to report). */ + size_t immediates = scr_pending_immediates; + if (scr_loop_hosted != NULL) { + timeouts = scr_hosted_count(0); + immediates = scr_hosted_count(1); + } + for (size_t i = 0; i < timeouts; i++) scr_arr_push_ref(arr, scr_str_new("Timeout", 7)); + for (size_t i = 0; i < immediates; i++) scr_arr_push_ref(arr, scr_str_new("Immediate", 9)); +#else for (size_t i = 0; i < timeouts; i++) scr_arr_push_ref(arr, scr_str_new("Timeout", 7)); for (size_t i = 0; i < scr_pending_immediates; i++) scr_arr_push_ref(arr, scr_str_new("Immediate", 9)); +#endif return arr; } @@ -1176,10 +1266,20 @@ static SCR_TL ScrNtick *scr_nt_tail = NULL; * it, because 'exit' listeners run AFTER the loop's teardown and may * enqueue ticks that must never run (Node) yet must not leak. */ +#ifdef SCR_NODE_ISLANDS +static void scr_hosted_tick(ScrNtick *t); +#endif + void scr_next_tick(ScrClosure *cb /*moves*/) { ScrNtick *t = calloc(1, sizeof *t); if (!t) scr_oom(); t->cb = cb; +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) { + scr_hosted_tick(t); + return; + } +#endif if (scr_nt_tail) scr_nt_tail->next = t; else scr_nt_head = t; scr_nt_tail = t; @@ -1194,6 +1294,12 @@ void scr_process_write_callback(ScrClosure *cb /*moves*/, ScrFsRenameFn fn) { if (!t) scr_oom(); t->cb = cb; t->error_cb = fn; +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) { + scr_hosted_tick(t); + return; + } +#endif if (scr_nt_tail) scr_nt_tail->next = t; else scr_nt_head = t; scr_nt_tail = t; @@ -1208,6 +1314,12 @@ void scr_next_tick_raw(void (*fn)(void)) { ScrNtick *t = calloc(1, sizeof *t); if (!t) scr_oom(); t->raw = fn; +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) { + scr_hosted_tick(t); + return; + } +#endif if (scr_nt_tail) scr_nt_tail->next = t; else scr_nt_head = t; scr_nt_tail = t; @@ -1219,6 +1331,13 @@ static void scr_next_tick_raw_first(void (*fn)(void)) { ScrNtick *t = calloc(1, sizeof *t); if (!t) scr_oom(); t->raw = fn; + /* Hosted, Node's tick queue has no front: the tick queues last. */ +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) { + scr_hosted_tick(t); + return; + } +#endif t->next = scr_nt_head; scr_nt_head = t; if (scr_nt_tail == NULL) scr_nt_tail = t; @@ -1236,6 +1355,9 @@ void scr_nticks_teardown(void) { static void scr_immediates_teardown(void); void scr_timers_teardown(void) { +#ifdef SCR_NODE_ISLANDS + scr_hosted_teardown(); +#endif for (size_t i = 0; i < scr_ntimers; i++) scr_closure_release(scr_timers[i].cb); scr_ntimers = 0; scr_reffed_timers = 0; @@ -1253,6 +1375,12 @@ void scr_clear_interval(double handle) { scr_firing_cleared = true; /* the run loop drops the callback */ return; } +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) { + scr_hosted_timer_op(id, SCR_HOSTED_CLEAR); + return; + } +#endif ScrTimer *found = scr_timer_find(id); if (found) { scr_closure_release(found->cb); @@ -1282,6 +1410,9 @@ static SCR_TL size_t scr_pending_immediates = 0; static SCR_TL size_t scr_reffed_immediates = 0; double scr_set_immediate(ScrClosure *cb) { +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) return scr_hosted_set_immediate(cb); +#endif if (scr_nimmediates == scr_immediates_cap) { scr_immediates_cap = scr_immediates_cap ? scr_immediates_cap * 2 : 16; scr_immediates = realloc(scr_immediates, scr_immediates_cap * sizeof *scr_immediates); @@ -1306,6 +1437,12 @@ static ScrImmediate *scr_immediate_find(unsigned long id) { void scr_clear_immediate(double handle) { if (!(handle >= 1)) return; /* NaN/0/negative: never a handle; Node tolerates */ +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) { + scr_hosted_immediate_op((unsigned long)handle, SCR_HOSTED_CLEAR); + return; + } +#endif ScrImmediate *im = scr_immediate_find((unsigned long)handle); if (im == NULL) return; /* already fired/cleared: no-op, like Node */ scr_closure_release(im->cb); @@ -1322,6 +1459,12 @@ void scr_clear_immediate(double handle) { * (Node's Immediate methods on a dead handle do nothing). */ void scr_immediate_unref(double handle) { if (!(handle >= 1)) return; +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) { + scr_hosted_immediate_op((unsigned long)handle, SCR_HOSTED_UNREF); + return; + } +#endif ScrImmediate *im = scr_immediate_find((unsigned long)handle); if (im && im->reffed) { im->reffed = false; @@ -1331,6 +1474,12 @@ void scr_immediate_unref(double handle) { void scr_immediate_ref(double handle) { if (!(handle >= 1)) return; +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) { + scr_hosted_immediate_op((unsigned long)handle, SCR_HOSTED_REF); + return; + } +#endif ScrImmediate *im = scr_immediate_find((unsigned long)handle); if (im && !im->reffed) { im->reffed = true; @@ -1340,6 +1489,9 @@ void scr_immediate_ref(double handle) { bool scr_immediate_has_ref(double handle) { if (!(handle >= 1)) return false; +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) return scr_hosted_immediate_op((unsigned long)handle, SCR_HOSTED_HAS_REF); +#endif ScrImmediate *im = scr_immediate_find((unsigned long)handle); return im != NULL && im->reffed; } @@ -2432,7 +2584,7 @@ void scr_gen_coro_finish(ScrFiber *self) { * continues a fiber in place: resumed by the loop's drain with nothing * else queued, this body is the very next job either way. */ static bool scr_coro_hop_needed(ScrFiber *self) { - if (self->return_to == &scr_loop_ctx && scr_ready_len == 0) return false; + if (self->return_to == &scr_loop_ctx && SCR_READY_IDLE()) return false; scr_ready_push(self); return true; } @@ -2498,7 +2650,7 @@ static void scr_await_yield(void) { * resumed by its spawner (the eager prefix) must still hop: the spawner * has code to run first. */ ScrCtx *to = scr_fiber_park_awaiters(self); - if (to == &scr_loop_ctx && scr_ready_len == 0) return; + if (to == &scr_loop_ctx && SCR_READY_IDLE()) return; scr_ready_push(self); scr_switch(&self->ctx, to, NULL); } @@ -4059,28 +4211,6 @@ void scr_loop_set_watch(bool (*pending)(void), void (*dispatch)(void), int (*pol scr_watch_pollfd_fn = pollfd; } -#ifdef SCR_NODE_ISLANDS -/* The Node island hook (scr_node_island.c): the watch hook's shape plus a - * sleep bound, in the SCR_NODE_ISLANDS build of this unit only, so - * programs without Node islands link the plain object byte for byte. Once - * Node is loaded, `pending` keeps the loop alive while Node's own loop has - * ref'd work, `dispatch` runs one non-blocking turn of it at every turn - * top, `pollfd` is its backend fd for the idle poll(2) sleep and `timeout` - * the milliseconds to its next timer (-1: none). NI-3 replaces this with - * the runtime's queues hosted on Node's loop. */ -static SCR_TL bool (*scr_node_island_pending_fn)(void) = NULL; -static SCR_TL void (*scr_node_island_dispatch_fn)(void) = NULL; -static SCR_TL int (*scr_node_island_pollfd_fn)(void) = NULL; -static SCR_TL int (*scr_node_island_timeout_fn)(void) = NULL; - -void scr_loop_set_node_island(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void), - int (*timeout)(void)) { - scr_node_island_pending_fn = pending; - scr_node_island_dispatch_fn = dispatch; - scr_node_island_pollfd_fn = pollfd; - scr_node_island_timeout_fn = timeout; -} -#endif /* The child-process hook (scr_child.c, installed with the first spawned * child or IPC channel). Programs that never create one keep it NULL and @@ -4164,7 +4294,12 @@ static int scr_workers_pollfd(void) { * callbacks or fibers are already queued, so ticks and promise jobs * interleave with event delivery (Node's checkpoint after each * macrotask). */ -bool scr_loop_has_ready(void) { return scr_ready_len > 0 || scr_nt_head != NULL; } +bool scr_loop_has_ready(void) { +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) return scr_hosted_queued > 0; +#endif + return scr_ready_len > 0 || scr_nt_head != NULL; +} /* The poll phase runs once per loop iteration, like libuv's: the net unit * drains the poller at most once between check phases. I/O that becomes @@ -4185,6 +4320,657 @@ bool scr_loop_io_take(void) { bool scr_loop_io_more(void) { return ++scr_loop_io_batches < SCR_LOOP_IO_BATCHES; } void scr_loop_io_reopen(void) { scr_loop_io_open = true; } +#ifdef SCR_NODE_ISLANDS +/* ── the hosted loop (scope-B NI-3) ─────────────────────────────────────── + * Once Node is loaded into the process there is one event loop, Node's, + * and one set of queues. scr_loop_host (called by scr_node_island.c right + * after Node starts) migrates the runtime's pending work onto Node's + * queues in order and points every scheduling primitive at the bridge: + * + * - promise jobs and queueMicrotask envelopes (the READY queue) become V8 + * microtasks, ticks become process.nextTick callbacks, so the jobs of + * both sides interleave in one FIFO each, exactly as in Node; + * - timers become Node timers, created in (deadline, sequence) order with + * their remaining delay measured from the moment Node began loading (the + * load itself is invisible to timer order, as it is to a program that + * runs on Node from the start); immediates become Node immediates; + * - the runtime's I/O hooks (the scr_loop_set_* slots) are watched by the + * bridge: their fds as uv_poll handles, their liveness as the ref of one + * handle, always-ready work as an idle handle; scr_loop_hosted_io runs + * the dispatch stations with Node's checkpoint after each. + * + * Every runtime job runs through scr_loop_hosted_run on the stack of + * Node's own dispatch, so Node drains ticks and microtasks after it as + * after any callback. A throw that escapes a job goes through the + * runtime's uncaught machinery (process 'uncaughtException' listeners) + * and, unhandled, ends the process as the runtime's main would. */ + +typedef enum { + SCR_HJ_FREE = 0, + SCR_HJ_MICRO = SCR_NODE_JOB_MICROTASK, + SCR_HJ_TICK = SCR_NODE_JOB_TICK, + SCR_HJ_TIMER = SCR_NODE_JOB_TIMER, + SCR_HJ_IMMEDIATE = SCR_NODE_JOB_IMMEDIATE, +} ScrHJobKind; + +/* One scheduled runtime job; its token (index + 1) travels through the + * bridge. Entries move when the table grows: never hold a pointer across + * a callback. */ +typedef struct { + uint8_t kind; + bool reffed; + bool firing; /* a timer whose callback is running */ + bool cleared; /* ... and cleared from inside it */ + bool refreshed; /* ... and refreshed from inside it */ + bool due; /* runs before Node's loop starts (handle 0 until then) */ + uint32_t handle; /* the bridge's timer or immediate */ + uint32_t next_free; + unsigned long id; /* the program's handle; 0 for plain setTimeout */ + double repeat_ms; + double delay_ms; /* the original delay: refresh() of a due timer re-arms with it */ + void *p; /* ScrFiber *, ScrNtick *, or the owned ScrClosure * */ +} ScrHJob; + + +static SCR_TL ScrHJob *scr_hjobs = NULL; +static SCR_TL uint32_t scr_hjobs_len = 0, scr_hjobs_cap = 0, scr_hjobs_free = 0; + +static uint32_t scr_hjob_new(uint8_t kind, void *p) { + uint32_t token; + if (scr_hjobs_free != 0) { + token = scr_hjobs_free; + scr_hjobs_free = scr_hjobs[token - 1].next_free; + } else { + if (scr_hjobs_len == scr_hjobs_cap) { + scr_hjobs_cap = scr_hjobs_cap ? scr_hjobs_cap * 2 : 64; + scr_hjobs = realloc(scr_hjobs, scr_hjobs_cap * sizeof *scr_hjobs); + if (!scr_hjobs) scr_oom(); + } + token = ++scr_hjobs_len; + } + ScrHJob *j = &scr_hjobs[token - 1]; + memset(j, 0, sizeof *j); + j->kind = kind; + j->p = p; + return token; +} + +static ScrHJob *scr_hjob(uint32_t token, uint8_t kind) { + if (token == 0 || token > scr_hjobs_len) return NULL; + ScrHJob *j = &scr_hjobs[token - 1]; + return j->kind == kind ? j : NULL; +} + +static void scr_hjob_free(uint32_t token) { + ScrHJob *j = &scr_hjobs[token - 1]; + j->kind = SCR_HJ_FREE; + j->due = false; + j->p = NULL; + j->next_free = scr_hjobs_free; + scr_hjobs_free = token; +} + +/* Work the loop pass was about to run when Node loaded: immediates left in + * the check phase being run, and timers already due. They run, in that + * order, before Node's loop starts (scr_loop_run_hosted), as they would + * have in the phase the switch interrupted; Node would never have started + * a new turn first. */ +static SCR_TL uint32_t *scr_hosted_due = NULL; +static SCR_TL size_t scr_hosted_due_len = 0, scr_hosted_due_cap = 0; + +static void scr_hosted_due_push(uint32_t token) { + if (scr_hosted_due_len == scr_hosted_due_cap) { + scr_hosted_due_cap = scr_hosted_due_cap ? scr_hosted_due_cap * 2 : 16; + scr_hosted_due = realloc(scr_hosted_due, scr_hosted_due_cap * sizeof *scr_hosted_due); + if (!scr_hosted_due) scr_oom(); + } + scr_hosted_due[scr_hosted_due_len++] = token; + scr_hjobs[token - 1].due = true; +} + +/* Program handle → token, for timers (key id*2) and immediates (id*2+1). + * Open addressing with linear probing and backward-shift deletion. */ +typedef struct { + unsigned long key; /* 0 = empty */ + uint32_t token; +} ScrHSlot; +static SCR_TL ScrHSlot *scr_hmap = NULL; +static SCR_TL size_t scr_hmap_cap = 0, scr_hmap_len = 0; + +static size_t scr_hmap_home(unsigned long key) { + uint64_t h = (uint64_t)key * UINT64_C(0x9E3779B97F4A7C15); + return (size_t)(h >> 32) & (scr_hmap_cap - 1); +} + +static void scr_hmap_put(unsigned long key, uint32_t token); + +static void scr_hmap_grow(void) { + ScrHSlot *old = scr_hmap; + size_t old_cap = scr_hmap_cap; + scr_hmap_cap = old_cap ? old_cap * 2 : 64; + scr_hmap = calloc(scr_hmap_cap, sizeof *scr_hmap); + if (!scr_hmap) scr_oom(); + scr_hmap_len = 0; + for (size_t i = 0; i < old_cap; i++) + if (old[i].key != 0) scr_hmap_put(old[i].key, old[i].token); + free(old); +} + +static void scr_hmap_put(unsigned long key, uint32_t token) { + if ((scr_hmap_len + 1) * 2 > scr_hmap_cap) scr_hmap_grow(); + size_t mask = scr_hmap_cap - 1, i = scr_hmap_home(key); + while (scr_hmap[i].key != 0 && scr_hmap[i].key != key) i = (i + 1) & mask; + if (scr_hmap[i].key == 0) scr_hmap_len++; + scr_hmap[i].key = key; + scr_hmap[i].token = token; +} + +static size_t scr_hmap_find(unsigned long key) { + if (scr_hmap_cap == 0) return SIZE_MAX; + size_t mask = scr_hmap_cap - 1; + for (size_t i = scr_hmap_home(key); scr_hmap[i].key != 0; i = (i + 1) & mask) + if (scr_hmap[i].key == key) return i; + return SIZE_MAX; +} + +static void scr_hmap_delete(unsigned long key) { + size_t i = scr_hmap_find(key); + if (i == SIZE_MAX) return; + size_t mask = scr_hmap_cap - 1; + for (size_t j = (i + 1) & mask; scr_hmap[j].key != 0; j = (j + 1) & mask) { + size_t home = scr_hmap_home(scr_hmap[j].key); + bool stays = i <= j ? (i < home && home <= j) : (i < home || home <= j); + if (stays) continue; + scr_hmap[i] = scr_hmap[j]; + i = j; + } + scr_hmap[i].key = 0; + scr_hmap_len--; +} + +static uint32_t scr_hmap_token(unsigned long key) { + size_t i = scr_hmap_find(key); + return i == SIZE_MAX ? 0 : scr_hmap[i].token; +} + +/* ── scheduling ── */ + +static void scr_hosted_microtask(ScrFiber *f) { + uint32_t token = scr_hjob_new(SCR_HJ_MICRO, f); + scr_hosted_queued++; + scr_loop_hosted->microtask(token); +} + +static void scr_hosted_tick(ScrNtick *t) { + uint32_t token = scr_hjob_new(SCR_HJ_TICK, t); + scr_hosted_queued++; + scr_loop_hosted->tick(token); +} + +/* Node's timer delay for a remaining time: whole milliseconds, at least + * one (Node's clamp), rounded up so a timer never fires before its + * deadline. */ +static double scr_hosted_delay(double remaining) { + if (!(remaining >= 1)) return 1; + if (remaining > 2147483647.0) return 2147483647.0; + double delay = (double)(long long)remaining; + return delay < remaining - 1e-6 ? delay + 1 : delay; +} + +/* A runtime timer on Node's loop; a `due` one waits for scr_loop_run_hosted + * and is armed only if it runs again. */ +static void scr_hosted_timer_arm(ScrClosure *cb, double delay, double repeat, double delay_ms, + unsigned long id, bool reffed, bool due) { + uint32_t token = scr_hjob_new(SCR_HJ_TIMER, cb); + ScrHJob *j = &scr_hjobs[token - 1]; + j->id = id; + j->repeat_ms = repeat; + j->delay_ms = delay_ms; + j->reffed = reffed; + if (id != 0) scr_hmap_put(id * 2, token); + if (due) { + scr_hosted_due_push(token); + return; + } + uint32_t handle = scr_loop_hosted->timer(delay, repeat, reffed ? 1 : 0, token); + scr_hjobs[token - 1].handle = handle; +} + +static void scr_hosted_timer_push(ScrTimer t) { + scr_hosted_timer_arm(t.cb, scr_hosted_delay(t.deadline_ms - scr_now_ms()), t.repeat_ms, + t.delay_ms, t.id, t.reffed, false); +} + +static void scr_hosted_timer_drop(uint32_t token) { + ScrHJob *j = &scr_hjobs[token - 1]; + ScrClosure *cb = (ScrClosure *)j->p; + if (j->id != 0) scr_hmap_delete(j->id * 2); + scr_hjob_free(token); + scr_closure_release(cb); +} + +static bool scr_hosted_timer_op(unsigned long id, int op) { + uint32_t token = scr_hmap_token(id * 2); + ScrHJob *j = scr_hjob(token, SCR_HJ_TIMER); + if (j == NULL) return false; + switch (op) { + case SCR_HOSTED_CLEAR: + scr_loop_hosted->timer_clear(j->handle); + if (j->firing) { + j->cleared = true; /* dropped when its callback returns */ + scr_hmap_delete(id * 2); + } else { + scr_hosted_timer_drop(token); + } + return true; + case SCR_HOSTED_REF: + case SCR_HOSTED_UNREF: + j->reffed = op == SCR_HOSTED_REF; + scr_loop_hosted->timer_ref(j->handle, j->reffed ? 1 : 0); + return true; + case SCR_HOSTED_REFRESH: + if (j->firing) j->refreshed = true; + if (j->handle != 0) scr_loop_hosted->timer_refresh(j->handle); + return true; + default: + return j->reffed; + } +} + +static double scr_hosted_set_immediate(ScrClosure *cb) { + unsigned long id = ++scr_immediate_seq; + uint32_t token = scr_hjob_new(SCR_HJ_IMMEDIATE, cb); + scr_hjobs[token - 1].id = id; + scr_hjobs[token - 1].reffed = true; + scr_hmap_put(id * 2 + 1, token); + uint32_t handle = scr_loop_hosted->immediate(1, token); + scr_hjobs[token - 1].handle = handle; + return (double)id; +} + +static bool scr_hosted_immediate_op(unsigned long id, int op) { + uint32_t token = scr_hmap_token(id * 2 + 1); + ScrHJob *j = scr_hjob(token, SCR_HJ_IMMEDIATE); + if (j == NULL) return false; + switch (op) { + case SCR_HOSTED_CLEAR: { + ScrClosure *cb = (ScrClosure *)j->p; + scr_loop_hosted->immediate_clear(j->handle); + scr_hmap_delete(id * 2 + 1); + scr_hjob_free(token); + scr_closure_release(cb); + return true; + } + case SCR_HOSTED_REF: + case SCR_HOSTED_UNREF: + j->reffed = op == SCR_HOSTED_REF; + scr_loop_hosted->immediate_ref(j->handle, j->reffed ? 1 : 0); + return true; + default: + return j->reffed; + } +} + +/* Live runtime timers (kind 0) or immediates (kind 1). */ +static size_t scr_hosted_count(int kind) { + uint8_t want = kind == 0 ? SCR_HJ_TIMER : SCR_HJ_IMMEDIATE; + size_t n = 0; + for (uint32_t i = 0; i < scr_hjobs_len; i++) + if (scr_hjobs[i].kind == want && !scr_hjobs[i].cleared) n++; + return n; +} + +/* Loop exit: timers and immediates that never ran (unref'd ones, or what + * remained at process exit) release their closures, like the plain heap's + * teardown; Node keeps its objects, which can no longer fire. */ +static void scr_hosted_teardown(void) { + for (uint32_t i = 0; i < scr_hjobs_len; i++) { + ScrHJob *j = &scr_hjobs[i]; + if ((j->kind == SCR_HJ_TIMER || j->kind == SCR_HJ_IMMEDIATE) && !j->firing) { + ScrClosure *cb = (ScrClosure *)j->p; + j->kind = SCR_HJ_FREE; + j->p = NULL; + j->next_free = scr_hjobs_free; + scr_hjobs_free = i + 1; + scr_closure_release(cb); + } + } + free(scr_hmap); + scr_hmap = NULL; + scr_hmap_cap = scr_hmap_len = 0; +} + +/* ── running jobs ── */ + +/* After a runtime callback: an escaped throw goes to the runtime's + * uncaught machinery; unhandled, it ends the process like main's + * uncaught path (report, exit listeners, the noted code). */ +static void scr_hosted_settle_exception(void) { + if (!scr_exc_pending()) return; + if (scr_exc_handle_uncaught(false)) return; + scr_exc_print_uncaught(); + scr_process_exit((double)scr_exit_code_hint_get()); +} + +void scr_loop_hosted_run(uint32_t kind, uint32_t token) { + switch (kind) { + case SCR_HJ_MICRO: { + ScrHJob *j = scr_hjob(token, SCR_HJ_MICRO); + if (j == NULL) return; + ScrFiber *f = (ScrFiber *)j->p; + scr_hjob_free(token); + scr_hosted_queued--; + scr_resume_fiber(f); + break; + } + case SCR_HJ_TICK: { + ScrHJob *j = scr_hjob(token, SCR_HJ_TICK); + if (j == NULL) return; + ScrNtick *t = (ScrNtick *)j->p; + scr_hjob_free(token); + scr_hosted_queued--; + ScrClosure *cb = t->cb; + ScrFsRenameFn error_cb = t->error_cb; + void (*raw)(void) = t->raw; + free(t); + if (cb) { + if (error_cb) error_cb(cb, NULL); + else ((void (*)(ScrClosure *))cb->fn)(cb); + scr_closure_release(cb); + } else { + raw(); + } + break; + } + case SCR_HJ_TIMER: { + ScrHJob *j = scr_hjob(token, SCR_HJ_TIMER); + if (j == NULL || j->cleared) return; + ScrClosure *cb = (ScrClosure *)j->p; + j->firing = true; + j->refreshed = false; + ((void (*)(ScrClosure *))cb->fn)(cb); + j = &scr_hjobs[token - 1]; + j->firing = false; + if (j->cleared) { + j->cleared = false; + scr_hjob_free(token); + scr_closure_release(cb); + } else if (j->due) { + /* A due timer ran before Node's loop: arm it only to run again. */ + j->due = false; + if (j->repeat_ms > 0 || j->refreshed) { + double delay = j->repeat_ms > 0 ? j->repeat_ms : j->delay_ms; + uint32_t handle = + scr_loop_hosted->timer(delay, j->repeat_ms, j->reffed ? 1 : 0, token); + scr_hjobs[token - 1].handle = handle; + } else { + scr_hosted_timer_drop(token); + } + } else if (j->repeat_ms <= 0 && !j->refreshed) { + /* A fired one-shot: Node drops its Timeout; the bridge forgets it. */ + scr_loop_hosted->timer_clear(j->handle); + scr_hosted_timer_drop(token); + } + break; + } + case SCR_HJ_IMMEDIATE: { + ScrHJob *j = scr_hjob(token, SCR_HJ_IMMEDIATE); + if (j == NULL) return; + ScrClosure *cb = (ScrClosure *)j->p; + scr_hmap_delete(j->id * 2 + 1); + scr_hjob_free(token); + ((void (*)(ScrClosure *))cb->fn)(cb); + scr_closure_release(cb); + break; + } + default: + return; + } + scr_hosted_settle_exception(); +} + +/* V8 finished a microtask checkpoint: the runtime's unhandled rejections + * are decided here, where Node decides its own. An unhandled one ends the + * process like main's reported-rejection path. */ +void scr_loop_hosted_microtasks_done(void) { + if (scr_nunhandled == 0) return; + if (!scr_report_unhandled_rejections()) { + scr_hosted_settle_exception(); + return; + } + scr_discard_unhandled_rejections(); + scr_process_exit((double)scr_exit_code_hint_get()); +} + +/* ── the runtime's I/O on Node's loop ── */ + +static void scr_hosted_io_fd(ScrNodeIoState *st, int fd) { + if (fd < 0 || st->nfds >= SCR_NODE_IO_MAX_FDS) return; + st->fds[st->nfds++] = fd; +} + +static void scr_hosted_io_cap(ScrNodeIoState *st, double ms) { + int cap = (int)ms; + if (st->cap_ms < 0 || cap < st->cap_ms) st->cap_ms = cap; +} + +/* The loop pass's liveness and sleep decisions, as Node's loop needs them + * before each poll phase and after each check phase. */ +void scr_loop_hosted_io_state(ScrNodeIoState *st) { + st->nfds = 0; + st->alive = 0; + st->ready = 0; + st->cap_ms = -1; + /* Quiescent before the poll phase: the turn's scheduled cycle collection. */ + if (st->phase == SCR_NODE_IO_PREPARE) scr_cyc_collect_scheduled(); + bool kids = scr_loop_children != NULL && scr_loop_children->pending(); + bool kids_reffed = scr_loop_children != NULL && scr_loop_children->reffed_pending(); + bool kids_failed = scr_loop_children != NULL && scr_loop_children->failed_pending(); + bool events = scr_events_pending_fn != NULL && scr_events_pending_fn(); + bool evw = scr_events_watching_fn != NULL && scr_events_watching_fn(); + bool net = scr_net_pending_fn != NULL && scr_net_pending_fn(); + bool net_io = net || (scr_net_io_fn != NULL && scr_net_io_fn()); + bool dgram = scr_dgram_pending_fn != NULL && scr_dgram_pending_fn(); + bool watch = scr_watch_pending_fn != NULL && scr_watch_pending_fn(); + bool ffi = scr_ffi_pending_fn != NULL && scr_ffi_pending_fn(); + bool stream = scr_stream_pending_fn != NULL && scr_stream_pending_fn(); + bool renames = scr_fs_renames_pending(); + bool crypto = scr_crypto_bytes_pending(); + bool deferred = scr_loop_deferred_pending(); + bool workers = scr_workers_pending(); + /* Deferred native callbacks (the fs callbacks) become deliverable at the + * end of the turn that queued them and run in the next one, as the loop + * pass delivers them; Node's own fs requests take a turn or more too. */ + if (st->phase == SCR_NODE_IO_CHECK) scr_loop_deferred_ready = scr_loop_deferred_count; + st->alive = kids_reffed || kids_failed || events || net || dgram || watch || ffi || stream || + renames || crypto || deferred || workers; + /* Queued deferred callbacks keep the poll from sleeping (their work is + * done; only the delivery waits for the next turn). */ + st->ready = stream || crypto || deferred || kids_failed || + (scr_loop_children != NULL && scr_loop_children->ready()); +#ifdef SCR_WORKERS + if (scr_workers_ready_fn && scr_workers_ready_fn()) st->ready = 1; +#endif +#if !defined(_WIN32) && !defined(__wasi__) + if (evw && scr_events_pollfds_fn != NULL) { + int evfds[2]; + int nev = scr_events_pollfds_fn(evfds); + for (int i = 0; i < nev; i++) scr_hosted_io_fd(st, evfds[i]); + } + if (net_io) { + int fd = scr_net_pollfd_fn != NULL ? scr_net_pollfd_fn() : -1; + if (fd >= 0) scr_hosted_io_fd(st, fd); + else scr_hosted_io_cap(st, SCR_SIGNAL_POLL_MS); + } + if (dgram) { + int fd = scr_dgram_pollfd_fn != NULL ? scr_dgram_pollfd_fn() : -1; + if (fd >= 0) scr_hosted_io_fd(st, fd); + else scr_hosted_io_cap(st, SCR_SIGNAL_POLL_MS); + } + if (watch) { + int fd = scr_watch_pollfd_fn != NULL ? scr_watch_pollfd_fn() : -1; + if (fd >= 0) scr_hosted_io_fd(st, fd); + else scr_hosted_io_cap(st, SCR_SIGNAL_POLL_MS); + } + if (ffi) { + int fd = scr_ffi_pollfd_fn != NULL ? scr_ffi_pollfd_fn() : -1; + if (fd >= 0) scr_hosted_io_fd(st, fd); + else scr_hosted_io_cap(st, SCR_CHILD_POLL_MS); + } + if (kids) { + int fd = scr_loop_children->wake_fd(); + if (fd >= 0) scr_hosted_io_fd(st, fd); + else scr_hosted_io_cap(st, SCR_CHILD_POLL_MS); + } + if (renames) { + int fd = scr_fs_renames_pollfd(); + if (fd >= 0) scr_hosted_io_fd(st, fd); + else scr_hosted_io_cap(st, SCR_CHILD_POLL_MS); + } + if (workers) { + int fd = scr_workers_pollfd(); + if (fd >= 0) scr_hosted_io_fd(st, fd); + else scr_hosted_io_cap(st, SCR_CHILD_POLL_MS); + } +#else + (void)evw; + if (evw || net_io || dgram || watch || ffi || kids || renames || workers) + scr_hosted_io_cap(st, SCR_CHILD_POLL_MS); +#endif +} + +/* The end of one runtime macrotask inside a dispatch station: the throw + * it left, then Node's checkpoint (ticks and microtasks of both sides). */ +static void scr_hosted_macrotask_done(void) { + scr_hosted_settle_exception(); + scr_loop_hosted->checkpoint(0); +} + +/* One pass over the dispatch stations, in the loop pass's order: worker + * completions and deferred callbacks one per checkpoint, then the event, + * socket, watch and child stations. */ +void scr_loop_hosted_io(void) { + /* Once per loop iteration (the bridge calls this at most once per turn): + * the poll phase is open again, as after the pass's check phase. */ + scr_loop_io_open = true; + scr_loop_io_batches = 0; + while (scr_fs_renames_pending() && scr_fs_renames_dispatch()) scr_hosted_macrotask_done(); + while (scr_crypto_bytes_pending() && scr_crypto_bytes_dispatch()) scr_hosted_macrotask_done(); + while (scr_loop_deferred_dispatch()) scr_hosted_macrotask_done(); + if (scr_ffi_dispatch_fn != NULL && scr_ffi_dispatch_fn()) scr_hosted_macrotask_done(); + if (scr_stream_dispatch_fn != NULL) { + scr_stream_dispatch_fn(); + scr_hosted_macrotask_done(); + } + scr_workers_dispatch(); + scr_hosted_macrotask_done(); + if (scr_events_dispatch_fn != NULL) { + scr_events_dispatch_fn(); + scr_hosted_macrotask_done(); + } + if (scr_net_dispatch_fn != NULL) { + scr_net_dispatch_fn(); + scr_hosted_macrotask_done(); + } + if (scr_dgram_dispatch_fn != NULL) { + scr_dgram_dispatch_fn(); + scr_hosted_macrotask_done(); + } + if (scr_watch_dispatch_fn != NULL) { + scr_watch_dispatch_fn(); + scr_hosted_macrotask_done(); + } + if (scr_loop_children != NULL && scr_loop_children->pending()) { +#if !defined(_WIN32) && !defined(__wasi__) + (void)scr_loop_children->wait(0); +#endif + scr_loop_children->poll(); + scr_hosted_macrotask_done(); + } +} + +/* ── the switch ── */ + +void scr_loop_host(const ScrNodeBridge *bridge, double since_ms) { + if (scr_loop_hosted != NULL) return; + scr_loop_hosted = bridge; + scr_hosted_continue_turn = scr_pass_phase == SCR_PASS_IO || scr_pass_phase == SCR_PASS_TIMERS; + scr_hosted_microtasks_first = scr_in_microtask; + bridge->host_loop(); + /* Promise jobs, FIFO. */ + while (scr_ready_len > 0) { + ScrFiber *f = scr_ready[scr_ready_head++]; + scr_ready_len--; + scr_hosted_microtask(f); + } + scr_ready_head = 0; + /* Ticks, FIFO. */ + ScrNtick *t = scr_nt_head; + scr_nt_head = scr_nt_tail = NULL; + while (t != NULL) { + ScrNtick *next = t->next; + t->next = NULL; + scr_hosted_tick(t); + t = next; + } + /* Immediates in queue order, keeping their handles. Those of a check + * phase the switch interrupted finish it before Node's loop starts. */ + for (size_t i = scr_immediates_head; i < scr_nimmediates; i++) { + ScrImmediate *im = &scr_immediates[i]; + if (im->cb == NULL) continue; + ScrClosure *cb = im->cb; + bool reffed = im->reffed; + unsigned long id = im->id; + im->cb = NULL; + uint32_t token = scr_hjob_new(SCR_HJ_IMMEDIATE, cb); + scr_hjobs[token - 1].id = id; + scr_hjobs[token - 1].reffed = reffed; + scr_hmap_put(id * 2 + 1, token); + if (i < scr_check_end) { + scr_hosted_due_push(token); + continue; + } + uint32_t handle = bridge->immediate(reffed ? 1 : 0, token); + scr_hjobs[token - 1].handle = handle; + } + scr_check_end = 0; + /* Timers in (deadline, sequence) order; those already due when Node + * began loading run before Node's loop starts, as the interrupted pass + * would have run them. */ + while (scr_ntimers > 0) { + ScrTimer tm = scr_timer_pop(); + scr_hosted_timer_arm(tm.cb, scr_hosted_delay(tm.deadline_ms - since_ms), tm.repeat_ms, + tm.delay_ms, tm.id, tm.reffed, tm.deadline_ms <= since_ms); + } + scr_reffed_timers = 0; + scr_nimmediates = scr_immediates_head = 0; + scr_pending_immediates = scr_reffed_immediates = 0; +} + +bool scr_loop_is_hosted(void) { return scr_loop_hosted != NULL; } + +static void scr_loop_run_hosted(bool first_checkpoint) { + scr_loop_hosted->checkpoint(first_checkpoint || scr_hosted_microtasks_first ? 1 : 0); + /* The interrupted phase's work, each followed by Node's checkpoint. A + * due entry cleared meanwhile is gone (or its token reused, not due). */ + for (size_t i = 0; i < scr_hosted_due_len; i++) { + uint32_t token = scr_hosted_due[i]; + if (token == 0 || token > scr_hjobs_len || !scr_hjobs[token - 1].due) continue; + uint8_t kind = scr_hjobs[token - 1].kind; + if (kind == SCR_HJ_IMMEDIATE) scr_hjobs[token - 1].due = false; + scr_loop_hosted_run(kind, token); + scr_loop_hosted->checkpoint(0); + } + free(scr_hosted_due); + scr_hosted_due = NULL; + scr_hosted_due_len = scr_hosted_due_cap = 0; + /* process.exitCode is one property in Node; island code's assignment + * becomes the program's code when the loop ends. */ + int code = scr_loop_hosted->run_loop(scr_hosted_continue_turn ? 1 : 0); + if (code != 0) scr_process_set_exit_code_tracked((double)code); +} +#endif /* SCR_NODE_ISLANDS */ + /* The process.nextTick queue to exhaustion, ticks queued meanwhile * included. false: a tick threw (uncaught, pending). */ static bool scr_loop_run_ticks(void) { @@ -4217,7 +5003,7 @@ static bool scr_loop_run_checkpoint(void) { while (scr_ready_len > 0) { ScrFiber *f = scr_ready[scr_ready_head++]; scr_ready_len--; - scr_resume_fiber(f); + SCR_RESUME_JOB(f); } } return true; @@ -4232,6 +5018,10 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { * pinned. Every later checkpoint drains ticks first. */ bool rejection_failed = false; for (;;) { +#ifdef SCR_NODE_ISLANDS + /* Node loaded during this turn: its loop takes over (scr_loop_run). */ + if (scr_loop_hosted != NULL) return rejection_failed; +#endif if (top_level != NULL && top_level->state == SCR_PROM_REJECTED) break; /* process.nextTick callbacks BEFORE promise jobs (Node's checkpoint * order): the tick queue to exhaustion, then the microtask queue to @@ -4244,7 +5034,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { while (scr_ready_len > 0) { ScrFiber *f = scr_ready[scr_ready_head++]; scr_ready_len--; - scr_resume_fiber(f); + SCR_RESUME_JOB(f); } first_checkpoint = false; /* The ESM loader observes a rejected entry evaluation at a promise-job @@ -4278,6 +5068,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { * occupied that thread. Deliver exactly one completion per checkpoint: * Node drains nextTicks and microtasks after each native callback before * invoking the next ready callback. */ + SCR_PASS_PHASE(SCR_PASS_IO); if (scr_fs_renames_pending()) { bool dispatched = scr_fs_renames_dispatch(); if (scr_exc_pending()) return false; @@ -4351,16 +5142,6 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { if (scr_exc_pending()) return false; /* uncaught throw in a listener */ if (scr_ready_len > 0 || scr_nt_head != NULL) continue; } -#ifdef SCR_NODE_ISLANDS - /* Node island dispatch (scr_node_island.c): one non-blocking turn of - * Node's loop, whose callbacks may settle calls into Node — the watch - * hook's station. */ - if (scr_node_island_dispatch_fn != NULL) { - scr_node_island_dispatch_fn(); - if (scr_exc_pending()) return false; - if (scr_ready_len > 0 || scr_nt_head != NULL) continue; - } -#endif /* Reap spawned children and fire their listeners (before timers, * like Node's nextTick-ish spawn-failure "error"). A callback may * enqueue microtasks or spawn again — restart the turn if so. @@ -4386,9 +5167,6 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { (scr_ffi_pending_fn != NULL && scr_ffi_pending_fn()) || scr_fs_renames_pending() || scr_crypto_bytes_pending() || scr_loop_deferred_pending() || scr_workers_pending() -#ifdef SCR_NODE_ISLANDS - || (scr_node_island_pending_fn != NULL && scr_node_island_pending_fn()) -#endif ; if (held) { scr_loop_children->poll(); @@ -4402,6 +5180,10 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { * nextTicks enqueued by a station listener are the same story. */ if (scr_stream_pending_fn != NULL && scr_stream_pending_fn()) continue; if (scr_nt_head != NULL) continue; +#ifdef SCR_NODE_ISLANDS + /* Node loaded during this turn: its loop takes over (scr_loop_run). */ + if (scr_loop_hosted != NULL) return rejection_failed; +#endif bool kids = scr_loop_children != NULL && scr_loop_children->pending(); bool kids_reffed = scr_loop_children != NULL && scr_loop_children->reffed_pending(); bool events = scr_events_pending_fn != NULL && scr_events_pending_fn(); @@ -4415,20 +5197,13 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { bool renames = scr_fs_renames_pending(); bool crypto = scr_crypto_bytes_pending() || scr_loop_deferred_pending(); bool workers = scr_workers_pending(); -#ifdef SCR_NODE_ISLANDS - bool node_island = scr_node_island_pending_fn != NULL && scr_node_island_pending_fn(); -#endif /* Timer liveness counts only REF'd timers: an unref'd timer stays in * the heap (and fires if the loop runs on for other reasons) but does * not by itself keep the process alive — Node's unref semantics. * Children follow the same rule: an unref'd child is still REAPED * while the loop runs (kids drives the sweeps and sleeps above) but * only reffed ones keep the process alive. */ - if (scr_reffed_timers == 0 && scr_reffed_immediates == 0 && !kids_reffed && !events && !net && !dgram && !watch && !ffi && !renames && !crypto && !workers -#ifdef SCR_NODE_ISLANDS - && !node_island -#endif - ) break; + if (scr_reffed_timers == 0 && scr_reffed_immediates == 0 && !kids_reffed && !events && !net && !dgram && !watch && !ffi && !renames && !crypto && !workers) break; /* Sleep to the earliest deadline, then run every due timer (each may * enqueue microtasks, which the next iteration drains first). Who * sleeps depends on what is pending: @@ -4486,20 +5261,12 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { #endif bool evw = scr_events_watching_fn != NULL && scr_events_watching_fn(); #ifdef SCR_WORKERS - if (workers && scr_workers_wait_fn && !kids && !evw && !net_io && !dgram && !watch && !ffi && !renames && !crypto -#ifdef SCR_NODE_ISLANDS - && !node_island -#endif - ) { + if (workers && scr_workers_wait_fn && !kids && !evw && !net_io && !dgram && !watch && !ffi && !renames && !crypto) { scr_workers_wait_fn(due > now ? due - now : 0); now = scr_now_ms(); } else #endif - if (evw || net_io || dgram || watch || ffi || workers || renames_fd >= 0 -#ifdef SCR_NODE_ISLANDS - || node_island -#endif - ) { + if (evw || net_io || dgram || watch || ffi || workers || renames_fd >= 0) { #if defined(_WIN32) || defined(__wasi__) /* The win32 arm, and WASI hosts whose poll_oneoff adapters do not * reliably wake for a closed inherited stdin pipe: the sleep is a capped nanosleep and @@ -4515,9 +5282,6 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { if (evw && due > now + SCR_SIGNAL_POLL_MS) due = now + SCR_SIGNAL_POLL_MS; if ((net_io || dgram || watch) && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS; if ((ffi || workers) && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS; -#ifdef SCR_NODE_ISLANDS - if (node_island && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS; -#endif if (kids && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS; if (due > now) { double wait = due - now; @@ -4611,22 +5375,6 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { fds[nfds].events = POLLIN; fds[nfds++].revents = 0; } -#ifdef SCR_NODE_ISLANDS - if (node_island) { - /* Node's backend fd ends the sleep on its I/O; its next timer - * bounds the sleep. */ - int left = scr_node_island_timeout_fn != NULL ? scr_node_island_timeout_fn() : -1; - if (left >= 0 && due > now + left) due = now + left; - int ifd = scr_node_island_pollfd_fn != NULL ? scr_node_island_pollfd_fn() : -1; - if (ifd >= 0) { - fds[nfds].fd = ifd; - fds[nfds].events = POLLIN; - fds[nfds++].revents = 0; - } else if (due > now + SCR_CHILD_POLL_MS) { - due = now + SCR_CHILD_POLL_MS; - } - } -#endif if (workers) { int worker_fd = scr_workers_pollfd(); if (worker_fd >= 0) { @@ -4667,6 +5415,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { #ifdef SCR_WORKERS scr_sa_loop_waiting(false); #endif + if (scr_ntimers > 0 && scr_timers[0].deadline_ms <= now) SCR_PASS_PHASE(SCR_PASS_TIMERS); while (scr_ntimers > 0 && scr_timers[0].deadline_ms <= now) { ScrTimer t = scr_timer_pop(); /* Timer callbacks are plain sync closures, run on the main stack. @@ -4700,6 +5449,10 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { } scr_firing_id = 0; if (scr_exc_pending()) return false; /* uncaught throw in a callback: main handles it */ +#ifdef SCR_NODE_ISLANDS + /* Node loaded during this turn: its loop takes over (scr_loop_run). */ + if (scr_loop_hosted != NULL) return rejection_failed; +#endif /* Drain ticks and microtasks, and report the rejections the callback * left, before more timers (Node's runNextTicks between timers). */ if (scr_ready_len > 0 || scr_nt_head != NULL || scr_nunhandled > 0) break; @@ -4715,6 +5468,10 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { bool check_ran = false; if (scr_ready_len == 0 && scr_nt_head == NULL && scr_nunhandled == 0 && scr_pending_immediates > 0) { size_t end = scr_nimmediates; +#ifdef SCR_NODE_ISLANDS + scr_check_end = end; /* a switch inside this phase finishes it first */ + SCR_PASS_PHASE(SCR_PASS_CHECK); +#endif while (scr_immediates_head < end) { size_t i = scr_immediates_head++; ScrClosure *cb = scr_immediates[i].cb; @@ -4728,6 +5485,9 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { ((void (*)(ScrClosure *))cb->fn)(cb); scr_closure_release(cb); if (scr_exc_pending()) return false; /* uncaught throw: main handles it */ +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) return rejection_failed; +#endif if (!scr_loop_run_checkpoint()) return false; /* Node's runNextTicks between immediates also reports the * rejections the callback left before the next one runs; a @@ -4738,6 +5498,9 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { } if (top_level != NULL && top_level->state == SCR_PROM_REJECTED) break; } +#ifdef SCR_NODE_ISLANDS + scr_check_end = 0; +#endif /* Fully drained: reset the cursor so the array reuses its slots. */ if (scr_immediates_head == scr_nimmediates) { scr_immediates_head = 0; @@ -4773,12 +5536,26 @@ bool scr_loop_run(ScrPromise *top_level) { bool first_checkpoint = !scr_loop_cjs_entry; bool rejection_failed; for (;;) { +#ifdef SCR_NODE_ISLANDS + /* Node is loaded (in the main body or a callback of an earlier pass): + * its loop runs everything from here, the runtime's queues included, + * and ends the process itself on an uncaught exception or an + * unhandled rejection. */ + if (scr_loop_hosted != NULL) { + scr_loop_run_hosted(first_checkpoint); + rejection_failed = false; + break; + } +#endif rejection_failed = scr_loop_run_pass(top_level && !top_level->top_level_handled ? top_level : NULL, first_checkpoint); first_checkpoint = false; if (scr_exc_pending()) { if (scr_exc_handle_uncaught(false)) continue; break; /* main reports the original or handler-thrown exception */ } +#ifdef SCR_NODE_ISLANDS + if (scr_loop_hosted != NULL) continue; +#endif if (rejection_failed) break; if (top_level && top_level->state == SCR_PROM_REJECTED && !top_level->top_level_handled) { scr_promise_rethrow(top_level); diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index 3391f54b1..066cff8c4 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -10,6 +10,9 @@ * zero bytes and keep their exact historical link line. */ #define _XOPEN_SOURCE 700 #include "scr_runtime.h" +#ifdef SCR_NODE_ISLANDS +#include "scr_node_island.h" +#endif #include #include @@ -80,6 +83,11 @@ typedef struct { ScrStr *name; /* aliases of one signum remain distinct event names */ bool once; uint64_t id; +#ifdef SCR_NODE_ISLANDS + /* Nonzero: a listener island code added (its cb is undefined); the + * delivery calls it through the bridge at this position. */ + uint32_t island; +#endif } ScrSigListener; typedef struct ScrSigReg { @@ -280,6 +288,63 @@ void scr_signal_off(ScrStr *name, ScrDyn *cb) { } } +#ifdef SCR_NODE_ISLANDS +/* Island code's signal listeners (scr_node_island.c forwards Node's + * newListener/removeListener for signal events here): each takes its place + * in the registry like a native listener, so scriptc owns every signal + * disposition and libuv never installs one. Removing the last listener of + * either side restores the previous disposition, as for native ones. */ +void scr_signal_on_island(ScrStr *name, uint32_t island) { +#if defined(_WIN32) || defined(__wasi__) + (void)name; + (void)island; +#else + if (!scr_context_is_main()) return; + int sig = scr_signal_from_name(name); + if (sig < 0) sig = 0; + ScrSigReg *reg = scr_sig_regs; + while (reg && reg->sig != sig) reg = reg->next; + if (!reg) { + reg = calloc(1, sizeof *reg); + if (!reg) scr_events_oom(); + reg->sig = sig; + if (sig > 0) { + struct sigaction sa; + memset(&sa, 0, sizeof sa); + sa.sa_handler = scr_sig_handler; + sigemptyset(&sa.sa_mask); + if (sig >= SCR_SIG_MAX || sigaction(sig, &sa, ®->prev) != 0) { + free(reg); + return; + } + scr_sig_watched++; + scr_wake_pipe_init(); + } + reg->next = scr_sig_regs; + scr_sig_regs = reg; + } + if (reg->n == reg->cap) { + reg->cap = reg->cap ? reg->cap * 2 : 2; + reg->ls = realloc(reg->ls, reg->cap * sizeof *reg->ls); + if (!reg->ls) scr_events_oom(); + } + reg->ls[reg->n++] = + (ScrSigListener){scr_dyn_retain(scr_dyn_undefined()), scr_str_retain(name), false, ++scr_sig_id, island}; +#endif +} + +void scr_signal_off_island(uint32_t island) { + for (ScrSigReg *reg = scr_sig_regs; reg; reg = reg->next) { + for (size_t i = 0; i < reg->n; i++) { + if (reg->ls[i].island == island) { + scr_sig_remove(reg, i); + return; + } + } + } +} +#endif + /* Snapshot all pending deliveries before invoking user code. No live * registry pointer survives a callback: off() can remove this signal or * another pending signal and free either registry. */ @@ -315,11 +380,19 @@ static void scr_signals_drain(void) { if (found) break; } } - ScrDyn *args[2] = {scr_dyn_new_str(snap[i].name), scr_dyn_new_num(scr_signal_from_name(snap[i].name))}; - ScrDyn *result = scr_dyn_call(snap[i].cb, args, 2, "listener"); - scr_dyn_release(result); - scr_dyn_release(args[0]); - scr_dyn_release(args[1]); +#ifdef SCR_NODE_ISLANDS + if (snap[i].island != 0) { + scr_node_island_emit(SCR_NODE_ISLAND_EVENT_SIGNAL, snap[i].island, snap[i].name, + scr_signal_from_name(snap[i].name)); + } else +#endif + { + ScrDyn *args[2] = {scr_dyn_new_str(snap[i].name), scr_dyn_new_num(scr_signal_from_name(snap[i].name))}; + ScrDyn *result = scr_dyn_call(snap[i].cb, args, 2, "listener"); + scr_dyn_release(result); + scr_dyn_release(args[0]); + scr_dyn_release(args[1]); + } } scr_dyn_release(snap[i].cb); scr_str_release(snap[i].name); @@ -1390,6 +1463,9 @@ typedef struct { ScrClosure *cb; /* owned */ void (*fn)(ScrClosure *cb, double code); /* adapter: 0-param or (code) */ bool once; +#ifdef SCR_NODE_ISLANDS + uint32_t island; /* nonzero: island code's listener (cb and fn NULL) */ +#endif } ScrExitListener; static SCR_TL ScrExitListener *scr_exit_ls = NULL; @@ -1403,6 +1479,13 @@ void scr_run_exit_listeners(double code) { for (size_t i = 0; i < scr_exit_n; i++) { #ifdef SCR_WORKERS if (scr_context_stopping()) break; +#endif +#ifdef SCR_NODE_ISLANDS + if (scr_exit_ls[i].island != 0) { + ScrStr *exit_name = scr_str_new("exit", 4); + scr_node_island_emit(SCR_NODE_ISLAND_EVENT_EXIT, scr_exit_ls[i].island, exit_name, (int)code); + scr_str_release(exit_name); + } else #endif scr_exit_ls[i].fn(scr_exit_ls[i].cb, code); #ifdef SCR_WORKERS @@ -1420,7 +1503,12 @@ void scr_run_exit_listeners(double code) { _Exit(7); } } - for (size_t i = 0; i < scr_exit_n; i++) scr_closure_release(scr_exit_ls[i].cb); + for (size_t i = 0; i < scr_exit_n; i++) { +#ifdef SCR_NODE_ISLANDS + if (scr_exit_ls[i].cb == NULL) continue; +#endif + scr_closure_release(scr_exit_ls[i].cb); + } free(scr_exit_ls); scr_exit_ls = NULL; scr_exit_n = scr_exit_cap = 0; @@ -1444,11 +1532,43 @@ void scr_process_on_exit(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, doub scr_exit_ls[scr_exit_n].cb = cb; scr_exit_ls[scr_exit_n].fn = fn; scr_exit_ls[scr_exit_n].once = once; +#ifdef SCR_NODE_ISLANDS + scr_exit_ls[scr_exit_n].island = 0; +#endif scr_exit_n++; } +#ifdef SCR_NODE_ISLANDS +/* Island code's 'exit' listeners, at their registration position among + * the program's own. */ +void scr_process_on_exit_island(uint32_t island) { + if (scr_exit_ran) return; + if (scr_exit_n == scr_exit_cap) { + scr_exit_cap = scr_exit_cap ? scr_exit_cap * 2 : 2; + scr_exit_ls = realloc(scr_exit_ls, scr_exit_cap * sizeof *scr_exit_ls); + if (!scr_exit_ls) scr_events_oom(); + } + scr_exit_ls[scr_exit_n] = (ScrExitListener){NULL, NULL, false, island}; + scr_exit_n++; +} + +void scr_process_off_exit_island(uint32_t island) { + if (scr_exit_ran) return; + for (size_t i = 0; i < scr_exit_n; i++) { + if (scr_exit_ls[i].island == island) { + memmove(scr_exit_ls + i, scr_exit_ls + i + 1, (scr_exit_n - i - 1) * sizeof *scr_exit_ls); + scr_exit_n--; + return; + } + } +} +#endif + void scr_process_off_exit(ScrClosure *cb /*borrowed*/) { for (size_t i = 0; i < scr_exit_n; i++) { +#ifdef SCR_NODE_ISLANDS + if (scr_exit_ls[i].cb == NULL) continue; +#endif if (scr_closure_identity_equal(scr_exit_ls[i].cb, cb)) { scr_closure_release(scr_exit_ls[i].cb); memmove(scr_exit_ls + i, scr_exit_ls + i + 1, (scr_exit_n - i - 1) * sizeof *scr_exit_ls); diff --git a/packages/runtime/src/scr_node_bridge.h b/packages/runtime/src/scr_node_bridge.h index 8ccdeaf8d..d8ee5544b 100644 --- a/packages/runtime/src/scr_node_bridge.h +++ b/packages/runtime/src/scr_node_bridge.h @@ -9,17 +9,56 @@ * are "R" (returned; "R" alone is undefined) or "T" (threw; * the JSON is {"error":{kind,message,code?}}, {"value":...} or * {"undefined":true}, see scr_node_island_host.cjs). Every entry point - * runs on the thread that called start. */ + * runs on the thread that called start, and every host function is called + * on it. */ #ifndef SCR_NODE_BRIDGE_H #define SCR_NODE_BRIDGE_H #include #include -#define SCR_NODE_BRIDGE_ABI 2u +#define SCR_NODE_BRIDGE_ABI 3u /* The symbol a bridge library exports: `const ScrNodeBridge *(void)`. */ #define SCR_NODE_BRIDGE_ENTRY "scr_node_bridge_v1" +/* Kinds of runtime jobs the bridge schedules on Node's queues (`run`). */ +enum { + SCR_NODE_JOB_MICROTASK = 1, /* V8's microtask queue */ + SCR_NODE_JOB_TICK = 2, /* Node's process.nextTick queue */ + SCR_NODE_JOB_TIMER = 3, /* a Node timer (setTimeout / setInterval) */ + SCR_NODE_JOB_IMMEDIATE = 4, /* Node's check phase (setImmediate) */ +}; + +/* Process events that island code listens to (`listen`): the listener + * joins the runtime's own registry at its registration position, so one + * registry orders the listeners of both sides and libuv never owns a + * signal. */ +enum { + SCR_NODE_EVENT_SIGNAL = 1, + SCR_NODE_EVENT_EXIT = 2, +}; + +/* The runtime's I/O as Node's loop sees it (io_state): the fds to watch + * for readability, whether runtime work keeps the loop alive, whether + * runtime work is ready without any fd (the loop must not block), and a + * bound in milliseconds on the sleep for work no fd announces (-1: none). + * The bridge sets `phase` (input): when it asks, so that liveness is + * current wherever Node's loop next decides it. */ +enum { + SCR_NODE_IO_PREPARE = 0, /* before a poll phase */ + SCR_NODE_IO_CHECK = 1, /* after a check phase: the end of a turn */ + SCR_NODE_IO_JOBS = 2, /* after a microtask checkpoint (any callback) */ +}; +#define SCR_NODE_IO_MAX_FDS 16 +typedef struct ScrNodeIoState { + int phase; + int fds[SCR_NODE_IO_MAX_FDS]; + int nfds; + int alive; + int ready; + int cap_ms; +} ScrNodeIoState; + /* What the runtime provides to the bridge. */ typedef struct ScrNodeBridgeHost { /* An asynchronous call settled with this outcome. */ @@ -27,6 +66,27 @@ typedef struct ScrNodeBridgeHost { /* Node ends the process (process.exit, an uncaught exception): the * runtime runs its exit sequence with this code and never returns. */ void (*exit)(int code); + /* A runtime job came due on Node's queues (a SCR_NODE_JOB_* kind and the + * token the runtime passed when it scheduled it). Runs on the stack of + * Node's own dispatch, so Node drains ticks and microtasks after it + * exactly as after any of its callbacks. */ + void (*run)(uint32_t kind, uint32_t token); + /* The runtime's I/O hooks: its state before each poll phase and after + * each check phase, and one dispatch pass (when a watched fd is readable + * or `ready` was set). */ + void (*io_state)(ScrNodeIoState *state); + void (*io_dispatch)(void); + /* V8 finished a microtask checkpoint: the runtime reports the unhandled + * rejections of its own promises, as Node does at the same point. */ + void (*microtasks_done)(void); + /* Island code added (add=1, once=1 for once listeners) or removed + * (add=0) a listener for a process event (SCR_NODE_EVENT_*, `name` its + * event name). `listener` identifies it in later `emit` calls. */ + void (*listen)(uint32_t event, const char *name, uint32_t listener, int add, int once); + /* The lowest stack address JavaScript may use on the stack the runtime + * is calling in from (its main thread's or an async fiber's), for V8's + * stack limit; 0 leaves V8's own. */ + uintptr_t (*stack_limit)(void); } ScrNodeBridgeHost; typedef struct ScrNodeBridge { @@ -48,14 +108,41 @@ typedef struct ScrNodeBridge { char *(*call)(uint32_t id, int sync, const char *file, const char *specifier, const char *name, const char *args, size_t args_len, size_t *len); void (*release)(void *memory); - /* Node's event loop, driven from the runtime's loop until NI-3 hosts the - * runtime's queues on it: a pollable backend fd, the timeout to its next - * timer (-1: none), whether ref'd work remains, and one non-blocking - * turn (libuv callbacks plus platform tasks). */ - int (*backend_fd)(void); - int (*backend_timeout)(void); - int (*alive)(void); - void (*pump)(void); + /* The hosted loop (scope-B NI-3). Once Node is loaded, the runtime's + * queues live on Node's: `host_loop` starts the integration (the I/O + * handles that watch the runtime's fds, the microtask-checkpoint hook) + * and holds a callback scope open until the next `checkpoint`, so work + * already pending in the runtime and work the current runtime callback + * goes on to queue drain together, at the end of that callback. */ + void (*host_loop)(void); + /* Schedules runtime jobs (SCR_NODE_JOB_*). Timers take Node's delay + * coercion; `repeat` > 0 re-arms after each run (setInterval), and + * `delay` may differ from `repeat` for intervals migrated mid-period. + * Handles are nonzero and name the Node Timeout or Immediate until the + * runtime clears them; a cleared or fired handle is never reused. */ + void (*microtask)(uint32_t token); + void (*tick)(uint32_t token); + uint32_t (*timer)(double delay, double repeat, int ref, uint32_t token); + void (*timer_clear)(uint32_t handle); + void (*timer_ref)(uint32_t handle, int ref); + void (*timer_refresh)(uint32_t handle); + uint32_t (*immediate)(int ref, uint32_t token); + void (*immediate_clear)(uint32_t handle); + void (*immediate_ref)(uint32_t handle, int ref); + /* The checkpoint after a macrotask: closes the scope `host_loop` held + * (or opens and closes one), which runs Node's tick queue and V8's + * microtasks to exhaustion. `microtasks_first` runs the microtasks once + * before the ticks: the end of an ES module main body. */ + void (*checkpoint)(int microtasks_first); + /* Runs Node's event loop until nothing keeps it alive (beforeExit + * included). Returns when the program ends normally, with the exit code + * island code assigned to process.exitCode (0: none). `continue_turn`: + * the switch interrupted a timers or poll phase, so the first iteration + * starts at the poll phase instead of the timers phase. */ + int (*run_loop)(int continue_turn); + /* Calls island listener `listener` of a process event: a signal (`name`, + * signal number `arg`) or exit (code `arg`). */ + void (*emit)(uint32_t event, uint32_t listener, const char *name, int arg); /* Tears Node down at process exit. */ void (*stop)(void); } ScrNodeBridge; diff --git a/packages/runtime/src/scr_node_island.c b/packages/runtime/src/scr_node_island.c index 69aa6b225..22a9d6a91 100644 --- a/packages/runtime/src/scr_node_island.c +++ b/packages/runtime/src/scr_node_island.c @@ -18,20 +18,36 @@ * the bridge (a callback scope, so ticks and microtasks drain when it * returns) and parses the outcome: the result's JSON copy, or the copied * error (see scr_node_island_host.cjs). An asynchronous call registers its - * promise first, because the bridge may settle it before returning. Until - * NI-3 hosts the runtime's queues on Node's libuv, the runtime's loop drives - * Node's: the loop slot keeps the program alive while Node has ref'd work, - * sleeps on its backend fd up to its next timer, and runs one non-blocking - * turn of it per loop turn. + * promise first, because the bridge may settle it before returning. + * + * One loop (scope-B NI-3). Right after Node starts, scr_loop_host moves the + * runtime's pending ticks, promise jobs, timers and immediates onto Node's + * queues and from then on schedules them there, and the bridge watches the + * runtime's I/O on Node's libuv loop: there is one loop and one set of + * queues, so the jobs of both sides interleave as in Node. Calls into + * JavaScript do not drain queues themselves; the runtime's checkpoints do + * (the bridge's `checkpoint`, and Node's own after every callback it + * dispatches). + * + * Process state is shared, not mirrored: island listeners for signals and + * 'exit' join the runtime's registries (scr_events.c) at their position, + * so the runtime keeps owning every signal disposition; the exit code + * island code leaves in process.exitCode becomes the program's when the + * loop ends; Node's process.exit runs the runtime's exit sequence. * * Node's stack. V8 fixes an isolate's stack limit where it is created, and * calls into Node can come from any async fiber (512 KB stacks without a - * guard page) or from the main stack. Every entry into Node therefore runs - * on one dedicated stack of NI_STACK bytes with a guard page, reached by a - * context switch; the runtime's own stack guard follows it there. */ + * guard page) or from the main stack. Every entry into Node from outside + * it runs on one dedicated stack of NI_STACK bytes with a guard page, + * reached by a context switch; the runtime's own stack guard follows it + * there. Node's loop runs there too once it hosts the runtime's queues, so + * a runtime job it dispatches may resume an async fiber that calls into + * Node again from the fiber's stack: the bridge sets V8's limit for the + * stack each entry comes from (ni_stack_limit). */ #define _XOPEN_SOURCE 700 #define _DARWIN_C_SOURCE 1 #include "scr_runtime.h" +#include "scr_events.h" #include "scr_node_bridge.h" #include "scr_node_island.h" @@ -46,6 +62,7 @@ #include #include #include +#include #ifdef __APPLE__ #define NI_BRIDGE_FILE "libscr_node_bridge.dylib" @@ -180,18 +197,47 @@ static void ni_exit(int code) { ni_fatal(""); } -static const ScrNodeBridgeHost ni_host = {&ni_settle, &ni_exit}; +/* An island listener for a process event was added or removed. */ +static void ni_listen(uint32_t event, const char *name, uint32_t listener, int add, int once) { + (void)once; /* Node's once wrapper removes itself through removeListener */ + if (event == SCR_NODE_EVENT_SIGNAL) { + if (add) { + ScrStr *s = scr_str_new(name, strlen(name)); + scr_signal_on_island(s, listener); + scr_str_release(s); + } else { + scr_signal_off_island(listener); + } + } else if (event == SCR_NODE_EVENT_EXIT) { + if (add) scr_process_on_exit_island(listener); + else scr_process_off_exit_island(listener); + } +} -/* ── the loop slot ────────────────────────────────────────────────────── */ +/* The main thread's stack, for V8's stack limit (ni_stack_limit). */ +static uintptr_t ni_main_lo = 0, ni_main_hi = 0; + +static void ni_main_stack(void) { +#if defined(__APPLE__) + pthread_t self = pthread_self(); + ni_main_hi = (uintptr_t)pthread_get_stackaddr_np(self); + ni_main_lo = ni_main_hi - pthread_get_stacksize_np(self); +#elif defined(__linux__) + pthread_attr_t attr; + if (pthread_getattr_np(pthread_self(), &attr) == 0) { + void *addr = NULL; + size_t size = 0; + if (pthread_attr_getstack(&attr, &addr, &size) == 0) { + ni_main_lo = (uintptr_t)addr; + ni_main_hi = ni_main_lo + size; + } + pthread_attr_destroy(&attr); + } +#endif +} -static bool ni_alive(void) { return ni_bridge->alive() != 0; } -static void ni_pump_job(void *p); -static void ni_stop_job(void *p); static void ni_on_node_stack(void (*job)(void *), void *arg); -static void ni_pump(void) { ni_on_node_stack(ni_pump_job, NULL); } -static int ni_backend_fd(void) { return ni_bridge->backend_fd(); } -static int ni_backend_timeout(void) { return ni_bridge->backend_timeout(); } -static void ni_stop_atexit(void) { ni_on_node_stack(ni_stop_job, NULL); } +static const ScrNodeBridgeHost ni_host; /* ── Node's stack ─────────────────────────────────────────────────────── */ @@ -284,9 +330,43 @@ static void ni_call_job(void *p) { ni_bridge->call(a->id, a->sync, a->file, a->specifier, a->name, a->args, a->args_len, &a->len); } -static void ni_pump_job(void *p) { - (void)p; - ni_bridge->pump(); +/* V8's stack limit for the stack an entry comes from: Node's own stack + * (a reserve above its guard page), the main thread's (Node's default + * --stack-size, 984 KiB, below its top), or an async fiber's (the + * runtime's guard for it: its mapping's start plus the reserve compiled + * code keeps). */ +static uintptr_t ni_stack_limit(void) { + char here; + uintptr_t sp = (uintptr_t)&here; + if (ni_stack != NULL && sp > (uintptr_t)ni_stack && sp <= (uintptr_t)ni_stack + NI_STACK) + return (uintptr_t)(ni_stack + NI_GUARD) + NI_STACK / 8; + if (sp > ni_main_lo && sp <= ni_main_hi) { + uintptr_t limit = ni_main_hi > 984 * 1024 ? ni_main_hi - 984 * 1024 : 0; + return limit > ni_main_lo ? limit : ni_main_lo + 64 * 1024; + } + if (scr_stack_guard != 0 && scr_stack_guard < sp) return scr_stack_guard; + return 0; +} + +static const ScrNodeBridgeHost ni_host = { + &ni_settle, + &ni_exit, + &scr_loop_hosted_run, + &scr_loop_hosted_io_state, + &scr_loop_hosted_io, + &scr_loop_hosted_microtasks_done, + &ni_listen, + &ni_stack_limit, +}; + +void scr_node_island_emit(uint32_t event, uint32_t listener, ScrStr *name, int arg) { + if (ni_bridge == NULL) return; + char *n = malloc(name->len + 1); + if (n == NULL) ni_oom(); + memcpy(n, name->data, name->len); + n[name->len] = '\0'; + ni_bridge->emit(event, listener, n, arg); + free(n); } static void ni_stop_job(void *p) { @@ -294,10 +374,40 @@ static void ni_stop_job(void *p) { ni_bridge->stop(); } +/* Process end after a normal return: the 'exit' listeners of both sides + * run while Node can still call into JavaScript (this handler runs before + * the events unit's own, which then finds them done), then Node stops. */ +static void ni_stop_atexit(void) { + scr_run_exit_listeners((double)scr_exit_code_hint_get()); + ni_on_node_stack(ni_stop_job, NULL); +} + +/* The hosted loop's run_loop on Node's stack. */ +typedef struct { + int continue_turn, code; +} NiRunLoop; + +static void ni_run_loop_job(void *p) { + NiRunLoop *a = p; + a->code = ni_bridge->run_loop(a->continue_turn); +} + +static int ni_run_loop(int continue_turn) { + NiRunLoop a = {continue_turn, 0}; + ni_on_node_stack(ni_run_loop_job, &a); + return a.code; +} + +/* The bridge as the hosted loop sees it: Node's loop runs on Node's stack. */ +static ScrNodeBridge ni_hosted; + /* ── loading Node ─────────────────────────────────────────────────────── */ static void ni_start(void) { if (ni_bridge != NULL) return; + /* Timers pending now keep their deadlines relative to this moment: the + * time Node takes to load is invisible to their order. */ + double since = scr_now_ms(); const char *dir = ni_pack_dir(); char path[4352]; snprintf(path, sizeof path, "%s/%s", dir, NI_BRIDGE_FILE); @@ -337,6 +447,7 @@ static void ni_start(void) { /* Flush first: Node writes to the same stdout. */ fflush(stdout); ni_bridge = bridge; + ni_main_stack(); NiStart start = {scr_node_island_host_js, argv0, NULL}; ni_on_node_stack(ni_start_job, &start); char *error = start.error; @@ -346,8 +457,14 @@ static void ni_start(void) { bridge->release(error); ni_fatal(message); } + /* Island listeners need the runtime's event registries and their + * delivery; installed before the stop handler, whose 'exit' listeners + * run first. */ + scr_events_install(); scr_atexit(ni_stop_atexit); - scr_loop_set_node_island(&ni_alive, &ni_pump, &ni_backend_fd, &ni_backend_timeout); + ni_hosted = *bridge; + ni_hosted.run_loop = &ni_run_loop; + scr_loop_host(&ni_hosted, since); } /* ── calls ────────────────────────────────────────────────────────────── */ @@ -427,6 +544,13 @@ ScrPromise *scr_node_island_call(ScrStr *file, ScrStr *specifier, ScrStr *name, #else /* _WIN32 || __wasi__ */ +void scr_node_island_emit(uint32_t event, uint32_t listener, ScrStr *name, int arg) { + (void)event; + (void)listener; + (void)name; + (void)arg; +} + /* Node islands need a libnode pack, which these targets do not have: the * compiler refuses them there, so these entry points only report that. */ void scr_node_island_install(const char *libnode, const char *node_version) { diff --git a/packages/runtime/src/scr_node_island.h b/packages/runtime/src/scr_node_island.h index 9904047f5..961737b0e 100644 --- a/packages/runtime/src/scr_node_island.h +++ b/packages/runtime/src/scr_node_island.h @@ -25,10 +25,31 @@ ScrDyn *scr_node_island_call_sync(ScrStr *file, ScrStr *specifier, ScrStr *name, * error (also when the arguments cannot be copied). Never throws. */ ScrPromise *scr_node_island_call(ScrStr *file, ScrStr *specifier, ScrStr *name, ScrDyn *args); -/* The loop slot (SCR_NODE_ISLANDS build of scr_async.c): `pending` keeps - * the loop alive, `dispatch` runs at every turn top, `pollfd` joins the - * idle poll and `timeout` (milliseconds, -1 for none) bounds its sleep. */ -void scr_loop_set_node_island(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void), - int (*timeout)(void)); +/* The hosted loop (SCR_NODE_ISLANDS build of scr_async.c, scope-B NI-3). + * scr_loop_host moves the runtime's pending ticks, promise jobs, timers + * and immediates onto Node's queues through `bridge` and schedules every + * later one there; `since_ms` (scr_now_ms() before Node began loading) is + * the clock pending timers count their remaining delay from. The rest are + * the bridge's host callbacks (scr_node_bridge.h). */ +struct ScrNodeBridge; +struct ScrNodeIoState; +void scr_loop_host(const struct ScrNodeBridge *bridge, double since_ms); +bool scr_loop_is_hosted(void); +void scr_loop_hosted_run(uint32_t kind, uint32_t token); +void scr_loop_hosted_io_state(struct ScrNodeIoState *state); +void scr_loop_hosted_io(void); +void scr_loop_hosted_microtasks_done(void); + +/* 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, + * and a delivery calls them through scr_node_island_emit: a signal with + * its number, exit with the code. */ +enum { SCR_NODE_ISLAND_EVENT_SIGNAL = 1, SCR_NODE_ISLAND_EVENT_EXIT = 2 }; +void scr_node_island_emit(uint32_t event, uint32_t listener, ScrStr *name, int arg); +void scr_signal_on_island(ScrStr *name, uint32_t listener); +void scr_signal_off_island(uint32_t listener); +void scr_process_on_exit_island(uint32_t listener); +void scr_process_off_exit_island(uint32_t listener); #endif /* SCR_NODE_ISLAND_H */ diff --git a/packages/runtime/src/scr_node_island_host.cjs b/packages/runtime/src/scr_node_island_host.cjs index b18af6319..ad04b351d 100644 --- a/packages/runtime/src/scr_node_island_host.cjs +++ b/packages/runtime/src/scr_node_island_host.cjs @@ -4,16 +4,19 @@ * scr_node_island_host.h embeds this file as a C string; regenerate it * with `node scripts/node-island-host.mjs` after every change. * - * The libnode bridge (scr_node_bridge.cc) installs a native `settle` - * function as globalThis.__scriptcNodeIslandHost before this script runs, - * and takes the dispatcher this script leaves as - * globalThis.__scriptcNodeIsland afterwards; it deletes both globals. + * The libnode bridge (scr_node_bridge.cc) installs its natives (`settle`, + * and for the hosted loop `run` and `listen`) as + * globalThis.__scriptcNodeIslandHost before this script runs, and takes the + * dispatcher this script leaves as globalThis.__scriptcNodeIsland + * afterwards (`call` and the hosted-loop helpers); it deletes both globals. * * A copy of the call semantics lives in packages/node/index.js, which runs * them in-process under Node: both must produce the same results, errors * and messages. Outcomes are "R" (returned; "R" alone is undefined) * or "T" (threw), the encoding of scr_node_bridge.h. */ const { createRequire } = require("node:module"); +const { constants: osConstants } = require("node:os"); +const timers = require("node:timers"); const nativeHost = globalThis.__scriptcNodeIslandHost; @@ -118,6 +121,161 @@ function call(id, sync, file, specifier, name, argsJson) { return undefined; } +// ── the hosted loop (scope-B NI-3) ───────────────────────────────────── +// The runtime's ticks, timers and immediates are Node's, created here with +// Node's own timers (captured now, so island code replacing the globals +// does not reach them) and run through the native `run`. Handles name the +// Node objects until the runtime clears them. + +const JOB_TICK = 2; +const JOB_TIMER = 3; +const JOB_IMMEDIATE = 4; +const handles = new Map(); +let nextHandle = 1; + +function onTick(token) { + nativeHost.run(JOB_TICK, token); +} + +function onTimer(token) { + nativeHost.run(JOB_TIMER, token); +} + +function onImmediate(handle, token) { + handles.delete(handle); + nativeHost.run(JOB_IMMEDIATE, token); +} + +function setTimer(delay, repeat, ref, token) { + let timer; + if (repeat > 0 && delay === repeat) { + timer = timers.setInterval(onTimer, repeat, token); + } else { + timer = timers.setTimeout(onTimer, delay, token); + // An interval migrated mid-period: its first run comes after the rest + // of the period, later ones every `repeat` (Node re-arms a Timeout + // whose _repeat is set, exactly as for setInterval). + if (repeat > 0) timer._repeat = repeat; + } + if (!ref) timer.unref(); + const handle = nextHandle++; + handles.set(handle, timer); + return handle; +} + +function clearTimer(handle) { + const timer = handles.get(handle); + if (timer === undefined) return; + handles.delete(handle); + timers.clearTimeout(timer); +} + +function refTimer(handle, ref) { + const timer = handles.get(handle); + if (timer !== undefined) { + if (ref) timer.ref(); + else timer.unref(); + } +} + +function refreshTimer(handle) { + const timer = handles.get(handle); + if (timer !== undefined) timer.refresh(); +} + +function setImmediateJob(ref, token) { + const handle = nextHandle++; + const immediate = timers.setImmediate(onImmediate, handle, token); + if (!ref) immediate.unref(); + handles.set(handle, immediate); + return handle; +} + +function clearImmediateJob(handle) { + const immediate = handles.get(handle); + if (immediate === undefined) return; + handles.delete(handle); + timers.clearImmediate(immediate); +} + +function refImmediate(handle, ref) { + refTimer(handle, ref); +} + +function nextTick(token) { + process.nextTick(onTick, token); +} + +// ── process state (scope-B NI-3) ───────────────────────────────────────── +// Signal and 'exit' listeners of island code join the runtime's registries +// at their registration position, so one registry orders both sides and +// the runtime keeps owning every signal disposition: Node's own handlers, +// which would hand a signal to libuv and reset it to SIG_DFL when the last +// listener goes, are taken off. The listeners stay on `process` (listeners +// and listenerCount answer as in Node); the runtime calls them through +// `emit`, and Node's own 'exit' emission is left to the runtime's exit +// sequence. + +const EVENT_SIGNAL = 1; +const EVENT_EXIT = 2; +const signalNames = new Set(Object.keys(osConstants.signals)); +const islandListeners = []; +let nextListener = 1; + +function eventKind(event) { + if (event === "exit") return EVENT_EXIT; + return typeof event === "string" && signalNames.has(event) ? EVENT_SIGNAL : 0; +} + +for (const [event, name] of [ + ["newListener", "startListeningIfSignal"], + ["removeListener", "stopListeningIfSignal"], +]) { + for (const listener of process.listeners(event)) { + if (listener.name === name) process.removeListener(event, listener); + } +} + +process.on("newListener", (event, listener) => { + const kind = eventKind(event); + if (kind === 0) return; + const id = nextListener++; + islandListeners.push({ event, listener, id }); + nativeHost.listen(kind, event, id, 1, 0); +}); + +process.on("removeListener", (event, listener) => { + const kind = eventKind(event); + if (kind === 0) return; + for (let i = islandListeners.length - 1; i >= 0; i--) { + const entry = islandListeners[i]; + if (entry.event === event && entry.listener === listener) { + islandListeners.splice(i, 1); + nativeHost.listen(kind, event, entry.id, 0, 0); + return; + } + } +}); + +function emit(kind, id, name, arg) { + const entry = islandListeners.find((e) => e.id === id); + if (entry === undefined) return; + // The registered function: the listener itself, or its once wrapper, + // which removes itself before calling it. + const registered = process + .rawListeners(entry.event) + .find((raw) => (raw.listener ?? raw) === entry.listener); + if (registered === undefined) return; + if (kind === EVENT_SIGNAL) Reflect.apply(registered, process, [name, arg]); + else Reflect.apply(registered, process, [arg]); +} + +const nodeEmit = process.emit; +process.emit = function emit(event, ...args) { + if (event === "exit") return this.listenerCount("exit") > 0; + return Reflect.apply(nodeEmit, this, [event, ...args]); +}; + // Writes reach the shared stdout and stderr before control returns to the // program, as the program's own writes do. for (const stream of [process.stdout, process.stderr]) { @@ -125,4 +283,15 @@ for (const stream of [process.stdout, process.stderr]) { stream._handle.setBlocking(true); } -globalThis.__scriptcNodeIsland = { call }; +globalThis.__scriptcNodeIsland = { + call, + setTimer, + clearTimer, + refTimer, + refreshTimer, + setImmediate: setImmediateJob, + clearImmediate: clearImmediateJob, + refImmediate, + nextTick, + emit, +}; diff --git a/packages/runtime/src/scr_node_island_host.h b/packages/runtime/src/scr_node_island_host.h index 8a963535c..6a8bbb4b6 100644 --- a/packages/runtime/src/scr_node_island_host.h +++ b/packages/runtime/src/scr_node_island_host.h @@ -10,16 +10,19 @@ static const char scr_node_island_host_js[] = " * scr_node_island_host.h embeds this file as a C string; regenerate it\012" " * with `node scripts/node-island-host.mjs` after every change.\012" " *\012" - " * The libnode bridge (scr_node_bridge.cc) installs a native `settle`\012" - " * function as globalThis.__scriptcNodeIslandHost before this script runs,\012" - " * and takes the dispatcher this script leaves as\012" - " * globalThis.__scriptcNodeIsland afterwards; it deletes both globals.\012" + " * The libnode bridge (scr_node_bridge.cc) installs its natives (`settle`,\012" + " * and for the hosted loop `run` and `listen`) as\012" + " * globalThis.__scriptcNodeIslandHost before this script runs, and takes the\012" + " * dispatcher this script leaves as globalThis.__scriptcNodeIsland\012" + " * afterwards (`call` and the hosted-loop helpers); it deletes both globals.\012" " *\012" " * A copy of the call semantics lives in packages/node/index.js, which runs\012" " * them in-process under Node: both must produce the same results, errors\012" " * and messages. Outcomes are \"R\" (returned; \"R\" alone is undefined)\012" " * or \"T\" (threw), the encoding of scr_node_bridge.h. */\012" "const { createRequire } = require(\"node:module\");\012" + "const { constants: osConstants } = require(\"node:os\");\012" + "const timers = require(\"node:timers\");\012" "\012" "const nativeHost = globalThis.__scriptcNodeIslandHost;\012" "\012" @@ -124,6 +127,161 @@ static const char scr_node_island_host_js[] = " return undefined;\012" "}\012" "\012" + "// \342\224\200\342\224\200 the hosted loop (scope-B NI-3) \342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\012" + "// The runtime's ticks, timers and immediates are Node's, created here with\012" + "// Node's own timers (captured now, so island code replacing the globals\012" + "// does not reach them) and run through the native `run`. Handles name the\012" + "// Node objects until the runtime clears them.\012" + "\012" + "const JOB_TICK = 2;\012" + "const JOB_TIMER = 3;\012" + "const JOB_IMMEDIATE = 4;\012" + "const handles = new Map();\012" + "let nextHandle = 1;\012" + "\012" + "function onTick(token) {\012" + " nativeHost.run(JOB_TICK, token);\012" + "}\012" + "\012" + "function onTimer(token) {\012" + " nativeHost.run(JOB_TIMER, token);\012" + "}\012" + "\012" + "function onImmediate(handle, token) {\012" + " handles.delete(handle);\012" + " nativeHost.run(JOB_IMMEDIATE, token);\012" + "}\012" + "\012" + "function setTimer(delay, repeat, ref, token) {\012" + " let timer;\012" + " if (repeat > 0 && delay === repeat) {\012" + " timer = timers.setInterval(onTimer, repeat, token);\012" + " } else {\012" + " timer = timers.setTimeout(onTimer, delay, token);\012" + " // An interval migrated mid-period: its first run comes after the rest\012" + " // of the period, later ones every `repeat` (Node re-arms a Timeout\012" + " // whose _repeat is set, exactly as for setInterval).\012" + " if (repeat > 0) timer._repeat = repeat;\012" + " }\012" + " if (!ref) timer.unref();\012" + " const handle = nextHandle++;\012" + " handles.set(handle, timer);\012" + " return handle;\012" + "}\012" + "\012" + "function clearTimer(handle) {\012" + " const timer = handles.get(handle);\012" + " if (timer === undefined) return;\012" + " handles.delete(handle);\012" + " timers.clearTimeout(timer);\012" + "}\012" + "\012" + "function refTimer(handle, ref) {\012" + " const timer = handles.get(handle);\012" + " if (timer !== undefined) {\012" + " if (ref) timer.ref();\012" + " else timer.unref();\012" + " }\012" + "}\012" + "\012" + "function refreshTimer(handle) {\012" + " const timer = handles.get(handle);\012" + " if (timer !== undefined) timer.refresh();\012" + "}\012" + "\012" + "function setImmediateJob(ref, token) {\012" + " const handle = nextHandle++;\012" + " const immediate = timers.setImmediate(onImmediate, handle, token);\012" + " if (!ref) immediate.unref();\012" + " handles.set(handle, immediate);\012" + " return handle;\012" + "}\012" + "\012" + "function clearImmediateJob(handle) {\012" + " const immediate = handles.get(handle);\012" + " if (immediate === undefined) return;\012" + " handles.delete(handle);\012" + " timers.clearImmediate(immediate);\012" + "}\012" + "\012" + "function refImmediate(handle, ref) {\012" + " refTimer(handle, ref);\012" + "}\012" + "\012" + "function nextTick(token) {\012" + " process.nextTick(onTick, token);\012" + "}\012" + "\012" + "// \342\224\200\342\224\200 process state (scope-B NI-3) \342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\342\224\200\012" + "// Signal and 'exit' listeners of island code join the runtime's registries\012" + "// at their registration position, so one registry orders both sides and\012" + "// the runtime keeps owning every signal disposition: Node's own handlers,\012" + "// which would hand a signal to libuv and reset it to SIG_DFL when the last\012" + "// listener goes, are taken off. The listeners stay on `process` (listeners\012" + "// and listenerCount answer as in Node); the runtime calls them through\012" + "// `emit`, and Node's own 'exit' emission is left to the runtime's exit\012" + "// sequence.\012" + "\012" + "const EVENT_SIGNAL = 1;\012" + "const EVENT_EXIT = 2;\012" + "const signalNames = new Set(Object.keys(osConstants.signals));\012" + "const islandListeners = [];\012" + "let nextListener = 1;\012" + "\012" + "function eventKind(event) {\012" + " if (event === \"exit\") return EVENT_EXIT;\012" + " return typeof event === \"string\" && signalNames.has(event) \077 EVENT_SIGNAL : 0;\012" + "}\012" + "\012" + "for (const [event, name] of [\012" + " [\"newListener\", \"startListeningIfSignal\"],\012" + " [\"removeListener\", \"stopListeningIfSignal\"],\012" + "]) {\012" + " for (const listener of process.listeners(event)) {\012" + " if (listener.name === name) process.removeListener(event, listener);\012" + " }\012" + "}\012" + "\012" + "process.on(\"newListener\", (event, listener) => {\012" + " const kind = eventKind(event);\012" + " if (kind === 0) return;\012" + " const id = nextListener++;\012" + " islandListeners.push({ event, listener, id });\012" + " nativeHost.listen(kind, event, id, 1, 0);\012" + "});\012" + "\012" + "process.on(\"removeListener\", (event, listener) => {\012" + " const kind = eventKind(event);\012" + " if (kind === 0) return;\012" + " for (let i = islandListeners.length - 1; i >= 0; i--) {\012" + " const entry = islandListeners[i];\012" + " if (entry.event === event && entry.listener === listener) {\012" + " islandListeners.splice(i, 1);\012" + " nativeHost.listen(kind, event, entry.id, 0, 0);\012" + " return;\012" + " }\012" + " }\012" + "});\012" + "\012" + "function emit(kind, id, name, arg) {\012" + " const entry = islandListeners.find((e) => e.id === id);\012" + " if (entry === undefined) return;\012" + " // The registered function: the listener itself, or its once wrapper,\012" + " // which removes itself before calling it.\012" + " const registered = process\012" + " .rawListeners(entry.event)\012" + " .find((raw) => (raw.listener \077\077 raw) === entry.listener);\012" + " if (registered === undefined) return;\012" + " if (kind === EVENT_SIGNAL) Reflect.apply(registered, process, [name, arg]);\012" + " else Reflect.apply(registered, process, [arg]);\012" + "}\012" + "\012" + "const nodeEmit = process.emit;\012" + "process.emit = function emit(event, ...args) {\012" + " if (event === \"exit\") return this.listenerCount(\"exit\") > 0;\012" + " return Reflect.apply(nodeEmit, this, [event, ...args]);\012" + "};\012" + "\012" "// Writes reach the shared stdout and stderr before control returns to the\012" "// program, as the program's own writes do.\012" "for (const stream of [process.stdout, process.stderr]) {\012" @@ -131,6 +289,17 @@ static const char scr_node_island_host_js[] = " stream._handle.setBlocking(true);\012" "}\012" "\012" - "globalThis.__scriptcNodeIsland = { call };\012"; + "globalThis.__scriptcNodeIsland = {\012" + " call,\012" + " setTimer,\012" + " clearTimer,\012" + " refTimer,\012" + " refreshTimer,\012" + " setImmediate: setImmediateJob,\012" + " clearImmediate: clearImmediateJob,\012" + " refImmediate,\012" + " nextTick,\012" + " emit,\012" + "};\012"; #endif /* SCR_NODE_ISLAND_HOST_H */ diff --git a/tests/corpus-node-islands-order/e01-island-timer-keeps-alive/main.ts b/tests/corpus-node-islands-order/e01-island-timer-keeps-alive/main.ts new file mode 100644 index 000000000..144e2b479 --- /dev/null +++ b/tests/corpus-node-islands-order/e01-island-timer-keeps-alive/main.ts @@ -0,0 +1,20 @@ +// @requires-libnode +// X { T 30 { L i-late } } L h-end +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("T 30 { L i-late }"); +log("h-end"); diff --git a/tests/corpus-node-islands-order/e02-island-io-keeps-alive/main.ts b/tests/corpus-node-islands-order/e02-island-io-keeps-alive/main.ts new file mode 100644 index 000000000..9ac7c00b1 --- /dev/null +++ b/tests/corpus-node-islands-order/e02-island-io-keeps-alive/main.ts @@ -0,0 +1,20 @@ +// @requires-libnode +// X { F 50 { L i-done } } L h-end +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("F 50 { L i-done }"); +log("h-end"); diff --git a/tests/corpus-node-islands-order/e03-unref-server-exits/main.ts b/tests/corpus-node-islands-order/e03-unref-server-exits/main.ts new file mode 100644 index 000000000..53c2100cc --- /dev/null +++ b/tests/corpus-node-islands-order/e03-unref-server-exits/main.ts @@ -0,0 +1,20 @@ +// @requires-libnode +// X { O { L i-listening } } L h-end +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("O { L i-listening }"); +log("h-end"); diff --git a/tests/corpus-node-islands-order/e04-throw-in-io-step/main.ts b/tests/corpus-node-islands-order/e04-throw-in-io-step/main.ts new file mode 100644 index 000000000..3b6f06687 --- /dev/null +++ b/tests/corpus-node-islands-order/e04-throw-in-io-step/main.ts @@ -0,0 +1,22 @@ +// @requires-libnode +// X { G R { L i-io E } } B 30 L h-sync T 10 { L h-t10 } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("G R { L i-io E }"); +busy(30); +log("h-sync"); +setTimeout(() => log("h-t10"), 10); diff --git a/tests/corpus-node-islands-order/g01-fs-chain/main.ts b/tests/corpus-node-islands-order/g01-fs-chain/main.ts new file mode 100644 index 000000000..5847ffb24 --- /dev/null +++ b/tests/corpus-node-islands-order/g01-fs-chain/main.ts @@ -0,0 +1,23 @@ +// @requires-libnode +// X { T 0 { F 20 { L i-fs-chain } } } T 2 { B 80 L h-sync } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("T 0 { F 20 { L i-fs-chain } }"); +setTimeout(() => { + busy(80); + log("h-sync"); +}, 2); diff --git a/tests/corpus-node-islands-order/g02-net/main.ts b/tests/corpus-node-islands-order/g02-net/main.ts new file mode 100644 index 000000000..c3aa0159c --- /dev/null +++ b/tests/corpus-node-islands-order/g02-net/main.ts @@ -0,0 +1,23 @@ +// @requires-libnode +// X { T 0 { N { L i-net } } } T 2 { B 80 L h-sync } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("T 0 { N { L i-net } }"); +setTimeout(() => { + busy(80); + log("h-sync"); +}, 2); diff --git a/tests/corpus-node-islands-order/g03-child/main.ts b/tests/corpus-node-islands-order/g03-child/main.ts new file mode 100644 index 000000000..2f9f7171e --- /dev/null +++ b/tests/corpus-node-islands-order/g03-child/main.ts @@ -0,0 +1,24 @@ +// @requires-libnode +// @order: race +// X { T 0 { C { L i-child } } } T 2 { B 150 L h-sync } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("T 0 { C { L i-child } }"); +setTimeout(() => { + busy(150); + log("h-sync"); +}, 2); diff --git a/tests/corpus-node-islands-order/g04-dns/main.ts b/tests/corpus-node-islands-order/g04-dns/main.ts new file mode 100644 index 000000000..f9bb7cb4b --- /dev/null +++ b/tests/corpus-node-islands-order/g04-dns/main.ts @@ -0,0 +1,24 @@ +// @requires-libnode +// @order: race +// X { T 0 { D { L i-dns } } } T 2 { B 80 L h-sync } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("T 0 { D { L i-dns } }"); +setTimeout(() => { + busy(80); + log("h-sync"); +}, 2); diff --git a/tests/corpus-node-islands-order/g05-zlib/main.ts b/tests/corpus-node-islands-order/g05-zlib/main.ts new file mode 100644 index 000000000..14abe51b1 --- /dev/null +++ b/tests/corpus-node-islands-order/g05-zlib/main.ts @@ -0,0 +1,23 @@ +// @requires-libnode +// X { T 0 { Z { L i-zlib } } } T 2 { B 80 L h-sync } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("T 0 { Z { L i-zlib } }"); +setTimeout(() => { + busy(80); + log("h-sync"); +}, 2); diff --git a/tests/corpus-node-islands-order/g06-worker/main.ts b/tests/corpus-node-islands-order/g06-worker/main.ts new file mode 100644 index 000000000..5ee47a294 --- /dev/null +++ b/tests/corpus-node-islands-order/g06-worker/main.ts @@ -0,0 +1,23 @@ +// @requires-libnode +// X { T 0 { W { L i-worker } } } T 2 { B 300 L h-sync } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("T 0 { W { L i-worker } }"); +setTimeout(() => { + busy(300); + log("h-sync"); +}, 2); diff --git a/tests/corpus-node-islands-order/g07-internal-timer/main.ts b/tests/corpus-node-islands-order/g07-internal-timer/main.ts new file mode 100644 index 000000000..724a57cd6 --- /dev/null +++ b/tests/corpus-node-islands-order/g07-internal-timer/main.ts @@ -0,0 +1,23 @@ +// @requires-libnode +// X { T 0 { S { L i-internal-timer } } } T 2 { B 80 L h-sync } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("T 0 { S { L i-internal-timer } }"); +setTimeout(() => { + busy(80); + log("h-sync"); +}, 2); diff --git a/tests/corpus-node-islands-order/island.cjs b/tests/corpus-node-islands-order/island.cjs new file mode 100644 index 000000000..e02e2e857 --- /dev/null +++ b/tests/corpus-node-islands-order/island.cjs @@ -0,0 +1,183 @@ +"use strict"; +// The island half of the Node island ordering programs (scope-B design +// §5.2): `run(src)` interprets a small scheduling language inside Node, +// the same in a scriptc build (Node loaded in-process, one event loop) and +// under Node (@scriptc/node calls the module directly). Each program's +// main.ts is the native half. Ops (bodies in braces): +// L name log a line (synchronous write to fd 1) +// J log the AsyncLocalStorage store +// Y { .. } run the body inside als.run("s", ..) +// T ms { .. } setTimeout I { .. } setImmediate +// M { .. } queueMicrotask K { .. } process.nextTick +// P { .. } Promise.resolve().then(body) +// R { .. } fs.readFile, then the body +// A { .. } fs.promises.readFile, then the body +// F n { .. } n sequential fs.stat callbacks, then the body +// N { .. } a TCP round trip on localhost, the body on client data +// C { .. } child_process.execFile("/bin/true"), the body in its callback +// D { .. } dns.lookup("localhost") +// Z { .. } zlib.gzip +// W { .. } worker_threads: the body on the worker's first message +// S { .. } a socket idle timeout (a Node-internal timer) +// O { .. } an unref'd TCP server, the body on 'listening' +// E throw; G installs an uncaughtException handler that logs +// B ms busy-wait +const fs = require("node:fs"); +const { AsyncLocalStorage } = require("node:async_hooks"); + +const als = new AsyncLocalStorage(); + +function parse(src) { + const toks = src.trim().split(/\s+/).filter(Boolean); + let i = 0; + function body() { + if (toks[i++] !== "{") throw new Error(`expected { at ${i}`); + const start = i; + let depth = 1; + while (depth) { + const t = toks[i++]; + if (t === "{") depth++; + else if (t === "}") depth--; + } + return toks.slice(start, i - 1).join(" "); + } + const out = []; + while (i < toks.length) { + const k = toks[i++]; + if (k === "L") out.push({ k, s: toks[i++] }); + else if (k === "J" || k === "E" || k === "G") out.push({ k }); + else if (k === "B") out.push({ k, ms: +toks[i++] }); + else if (k === "T" || k === "F") { + const ms = +toks[i++]; + out.push({ k, ms, body: body() }); + } else out.push({ k, body: body() }); + } + return out; +} + +const log = (s) => fs.writeSync(1, `${s}\n`); +const busy = (ms) => { + const end = Date.now() + ms; + while (Date.now() < end); +}; + +function exec(src) { + for (const op of parse(src)) { + switch (op.k) { + case "L": + log(op.s); + break; + case "J": + log(`store=${als.getStore()}`); + break; + case "E": + throw new Error("boom"); + case "G": + process.on("uncaughtException", (e) => log(`uncaught-${e.message}`)); + break; + case "Y": + als.run("s", () => exec(op.body)); + break; + case "B": + busy(op.ms); + break; + default: + schedule(op); + } + } +} + +function schedule(op) { + const run = () => exec(op.body); + switch (op.k) { + case "T": + setTimeout(run, op.ms); + break; + case "I": + setImmediate(run); + break; + case "M": + queueMicrotask(run); + break; + case "K": + process.nextTick(run); + break; + case "P": + Promise.resolve().then(run); + break; + case "R": + fs.readFile(__filename, run); + break; + case "A": + fs.promises.readFile(__filename).then(run); + break; + case "F": { + let n = op.ms; + const next = () => (n-- > 0 ? fs.stat(__filename, next) : run()); + next(); + break; + } + case "N": { + const net = require("node:net"); + const srv = net + .createServer((s) => s.end("x")) + .listen(0, "127.0.0.1", () => { + const c = net.connect(srv.address().port, "127.0.0.1"); + c.once("data", () => { + c.destroy(); + srv.close(); + run(); + }); + }); + break; + } + case "C": + require("node:child_process").execFile("/bin/true", run); + break; + case "D": + require("node:dns").lookup("localhost", run); + break; + case "Z": + require("node:zlib").gzip("hello", run); + break; + case "W": { + const { Worker } = require("node:worker_threads"); + const w = new Worker('require("node:worker_threads").parentPort.postMessage(1)', { + eval: true, + }); + w.once("message", () => { + w.terminate(); + run(); + }); + break; + } + case "S": { + const net = require("node:net"); + const srv = net + .createServer((s) => + s.setTimeout(10, () => { + s.destroy(); + srv.close(); + run(); + }), + ) + .listen(0, "127.0.0.1", () => { + const c = net.connect(srv.address().port, "127.0.0.1"); + c.on("error", () => {}); + c.on("close", () => {}); + }); + break; + } + case "O": { + const s = require("node:net").createServer().listen(0, "127.0.0.1", run); + s.unref(); + break; + } + default: + throw new Error(`op ${op.k}`); + } +} + +exports.run = function run(src) { + exec(src); +}; diff --git a/tests/corpus-node-islands-order/p01-timers-interleave/main.ts b/tests/corpus-node-islands-order/p01-timers-interleave/main.ts new file mode 100644 index 000000000..1c002a494 --- /dev/null +++ b/tests/corpus-node-islands-order/p01-timers-interleave/main.ts @@ -0,0 +1,22 @@ +// @requires-libnode +// T 0 { L h1 } X { T 0 { L i1 } } T 0 { L h2 } X { T 0 { L i2 } } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +setTimeout(() => log("h1"), 0); +island("T 0 { L i1 }"); +setTimeout(() => log("h2"), 0); +island("T 0 { L i2 }"); diff --git a/tests/corpus-node-islands-order/p02-tick-micro-immediate/main.ts b/tests/corpus-node-islands-order/p02-tick-micro-immediate/main.ts new file mode 100644 index 000000000..c6492e227 --- /dev/null +++ b/tests/corpus-node-islands-order/p02-tick-micro-immediate/main.ts @@ -0,0 +1,23 @@ +// @requires-libnode +// I { L hI } X { I { L iI } M { L iM } K { L iK } } M { L hM } K { L hK } L sync +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +setImmediate(() => log("hI")); +island("I { L iI } M { L iM } K { L iK }"); +queueMicrotask(() => log("hM")); +process.nextTick(() => log("hK")); +log("sync"); diff --git a/tests/corpus-node-islands-order/p03-busy-vs-island-timer/main.ts b/tests/corpus-node-islands-order/p03-busy-vs-island-timer/main.ts new file mode 100644 index 000000000..1f7f81e26 --- /dev/null +++ b/tests/corpus-node-islands-order/p03-busy-vs-island-timer/main.ts @@ -0,0 +1,21 @@ +// @requires-libnode +// X { T 0 { L i-timer } } B 50 L h-after-busy +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("T 0 { L i-timer }"); +busy(50); +log("h-after-busy"); diff --git a/tests/corpus-node-islands-order/p05-io-and-promises/main.ts b/tests/corpus-node-islands-order/p05-io-and-promises/main.ts new file mode 100644 index 000000000..b8d7f7325 --- /dev/null +++ b/tests/corpus-node-islands-order/p05-io-and-promises/main.ts @@ -0,0 +1,22 @@ +// @requires-libnode +// @order: race +// X { R { L i-io M { L i-io-micro } } } R { L h-io } T 0 { L h-t0 } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("R { L i-io M { L i-io-micro } }"); +readFile(import.meta.filename, "utf8", () => log("h-io")); +setTimeout(() => log("h-t0"), 0); diff --git a/tests/corpus-node-islands-order/p07-microtask-fifo/main.ts b/tests/corpus-node-islands-order/p07-microtask-fifo/main.ts new file mode 100644 index 000000000..14de725d4 --- /dev/null +++ b/tests/corpus-node-islands-order/p07-microtask-fifo/main.ts @@ -0,0 +1,23 @@ +// @requires-libnode +// M { L hm1 } X { M { L im1 } } M { L hm2 } X { M { L im2 } } L sync +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +queueMicrotask(() => log("hm1")); +island("M { L im1 }"); +queueMicrotask(() => log("hm2")); +island("M { L im2 }"); +log("sync"); diff --git a/tests/corpus-node-islands-order/p08-timer-vs-immediate/main.ts b/tests/corpus-node-islands-order/p08-timer-vs-immediate/main.ts new file mode 100644 index 000000000..f448c8925 --- /dev/null +++ b/tests/corpus-node-islands-order/p08-timer-vs-immediate/main.ts @@ -0,0 +1,22 @@ +// @requires-libnode +// @order: race +// T 1 { L h-t1 } X { T 0 { L i-t0 } I { L i-imm } } I { L h-imm } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +setTimeout(() => log("h-t1"), 1); +island("T 0 { L i-t0 } I { L i-imm }"); +setImmediate(() => log("h-imm")); diff --git a/tests/corpus-node-islands-order/p09-deadline-order-under-busy/main.ts b/tests/corpus-node-islands-order/p09-deadline-order-under-busy/main.ts new file mode 100644 index 000000000..c89284633 --- /dev/null +++ b/tests/corpus-node-islands-order/p09-deadline-order-under-busy/main.ts @@ -0,0 +1,22 @@ +// @requires-libnode +// X { T 10 { L i-t10 } } T 20 { L h-t20 } B 30 L h-sync +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("T 10 { L i-t10 }"); +setTimeout(() => log("h-t20"), 20); +busy(30); +log("h-sync"); diff --git a/tests/corpus-node-islands-order/p10-island-chain/main.ts b/tests/corpus-node-islands-order/p10-island-chain/main.ts new file mode 100644 index 000000000..90a3fd566 --- /dev/null +++ b/tests/corpus-node-islands-order/p10-island-chain/main.ts @@ -0,0 +1,23 @@ +// @requires-libnode +// X { T 0 { L i-a K { L i-tick } M { L i-micro } I { L i-imm } } } T 0 { L h-t0 M { L h-micro } } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("T 0 { L i-a K { L i-tick } M { L i-micro } I { L i-imm } }"); +setTimeout(() => { + log("h-t0"); + queueMicrotask(() => log("h-micro")); +}, 0); diff --git a/tests/corpus-node-islands-order/p11-island-real-io/main.ts b/tests/corpus-node-islands-order/p11-island-real-io/main.ts new file mode 100644 index 000000000..6895cb6e0 --- /dev/null +++ b/tests/corpus-node-islands-order/p11-island-real-io/main.ts @@ -0,0 +1,24 @@ +// @requires-libnode +// @order: race +// X { R { L i-io M { L i-io-micro } } } R { L h-io } T 0 { L h-t0 } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("R { L i-io M { L i-io-micro } }"); +readFile(import.meta.filename, "utf8", () => { + log("h-io"); +}); +setTimeout(() => log("h-t0"), 0); diff --git a/tests/corpus-node-islands-order/p12-island-io-vs-busy/main.ts b/tests/corpus-node-islands-order/p12-island-io-vs-busy/main.ts new file mode 100644 index 000000000..7b342dd4d --- /dev/null +++ b/tests/corpus-node-islands-order/p12-island-io-vs-busy/main.ts @@ -0,0 +1,22 @@ +// @requires-libnode +// X { R { L i-io } } B 30 L h-sync T 0 { L h-t0 } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("R { L i-io }"); +busy(30); +log("h-sync"); +setTimeout(() => log("h-t0"), 0); diff --git a/tests/corpus-node-islands-order/q01-promise-job-fifo/main.ts b/tests/corpus-node-islands-order/q01-promise-job-fifo/main.ts new file mode 100644 index 000000000..36fb5d76b --- /dev/null +++ b/tests/corpus-node-islands-order/q01-promise-job-fifo/main.ts @@ -0,0 +1,23 @@ +// @requires-libnode +// M { L hm1 } X { P { L ip1 } } M { L hm2 } X { P { L ip2 } } L sync +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +queueMicrotask(() => log("hm1")); +island("P { L ip1 }"); +queueMicrotask(() => log("hm2")); +island("P { L ip2 }"); +log("sync"); diff --git a/tests/corpus-node-islands-order/q02-promise-batch/main.ts b/tests/corpus-node-islands-order/q02-promise-batch/main.ts new file mode 100644 index 000000000..09f9eb191 --- /dev/null +++ b/tests/corpus-node-islands-order/q02-promise-batch/main.ts @@ -0,0 +1,23 @@ +// @requires-libnode +// X { P { L ip1 P { L ip2 } } } M { L hm1 M { L hm2 } } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("P { L ip1 P { L ip2 } }"); +queueMicrotask(() => { + log("hm1"); + queueMicrotask(() => log("hm2")); +}); diff --git a/tests/corpus-node-islands-order/q03-io-callback-queues/main.ts b/tests/corpus-node-islands-order/q03-io-callback-queues/main.ts new file mode 100644 index 000000000..18b973c13 --- /dev/null +++ b/tests/corpus-node-islands-order/q03-io-callback-queues/main.ts @@ -0,0 +1,21 @@ +// @requires-libnode +// X { R { L i-io K { L i-tick } P { L i-p } M { L i-m } I { L i-imm } } } B 40 L h-sync +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("R { L i-io K { L i-tick } P { L i-p } M { L i-m } I { L i-imm } }"); +busy(40); +log("h-sync"); diff --git a/tests/corpus-node-islands-order/q04-fs-promises/main.ts b/tests/corpus-node-islands-order/q04-fs-promises/main.ts new file mode 100644 index 000000000..d253223f1 --- /dev/null +++ b/tests/corpus-node-islands-order/q04-fs-promises/main.ts @@ -0,0 +1,23 @@ +// @requires-libnode +// @order: race +// X { A { L i-a P { L i-ap } } } B 40 L h-sync T 0 { L h-t0 } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("A { L i-a P { L i-ap } }"); +busy(40); +log("h-sync"); +setTimeout(() => log("h-t0"), 0); diff --git a/tests/corpus-node-islands-order/q06-async-context/main.ts b/tests/corpus-node-islands-order/q06-async-context/main.ts new file mode 100644 index 000000000..cd814a4ae --- /dev/null +++ b/tests/corpus-node-islands-order/q06-async-context/main.ts @@ -0,0 +1,19 @@ +// @requires-libnode +// X { Y { T 0 { J } I { J } K { J } M { J } R { J } } } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("Y { T 0 { J } I { J } K { J } M { J } R { J } }"); diff --git a/tests/corpus-node-islands-order/q07-timer-from-io-vs-host-timer/main.ts b/tests/corpus-node-islands-order/q07-timer-from-io-vs-host-timer/main.ts new file mode 100644 index 000000000..e4af331aa --- /dev/null +++ b/tests/corpus-node-islands-order/q07-timer-from-io-vs-host-timer/main.ts @@ -0,0 +1,22 @@ +// @requires-libnode +// X { R { T 0 { L i-t0 } } } B 40 T 0 { L h-t0 } L h-sync +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +island("R { T 0 { L i-t0 } }"); +busy(40); +setTimeout(() => log("h-t0"), 0); +log("h-sync"); diff --git a/tests/corpus-node-islands-order/q09-native-promise-jobs/main.ts b/tests/corpus-node-islands-order/q09-native-promise-jobs/main.ts new file mode 100644 index 000000000..b6d4b470f --- /dev/null +++ b/tests/corpus-node-islands-order/q09-native-promise-jobs/main.ts @@ -0,0 +1,25 @@ +// @requires-libnode +// P { L hp1 } X { P { L ip1 P { L ip3 } } } P { L hp2 P { L hp3 } } L sync +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +Promise.resolve().then(() => log("hp1")); +island("P { L ip1 P { L ip3 } }"); +Promise.resolve().then(() => { + log("hp2"); + Promise.resolve().then(() => log("hp3")); +}); +log("sync"); diff --git a/tests/corpus-node-islands-order/q10-async-await-vs-island-jobs/main.ts b/tests/corpus-node-islands-order/q10-async-await-vs-island-jobs/main.ts new file mode 100644 index 000000000..7e813acb1 --- /dev/null +++ b/tests/corpus-node-islands-order/q10-async-await-vs-island-jobs/main.ts @@ -0,0 +1,29 @@ +// @requires-libnode +// async f: await, ha1, await, ha2, await, ha3 || X { P { L ip1 P { L ip2 P { L ip3 } } } } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +async function steps(): Promise { + await null; + log("ha1"); + await null; + log("ha2"); + await Promise.resolve(); + log("ha3"); +} +void steps(); +island("P { L ip1 P { L ip2 P { L ip3 } } }"); +log("sync"); diff --git a/tests/corpus-node-islands-order/q11-ticks-vs-island-ticks/main.ts b/tests/corpus-node-islands-order/q11-ticks-vs-island-ticks/main.ts new file mode 100644 index 000000000..12e9647db --- /dev/null +++ b/tests/corpus-node-islands-order/q11-ticks-vs-island-ticks/main.ts @@ -0,0 +1,25 @@ +// @requires-libnode +// K { L hk1 K { L hk3 } } X { K { L ik1 M { L im1 } } } K { L hk2 } M { L hm1 } +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +process.nextTick(() => { + log("hk1"); + process.nextTick(() => log("hk3")); +}); +island("K { L ik1 M { L im1 } }"); +process.nextTick(() => log("hk2")); +queueMicrotask(() => log("hm1")); diff --git a/tests/corpus-node-islands-order/s01-pending-in-main-body/main.ts b/tests/corpus-node-islands-order/s01-pending-in-main-body/main.ts new file mode 100644 index 000000000..e7992afa5 --- /dev/null +++ b/tests/corpus-node-islands-order/s01-pending-in-main-body/main.ts @@ -0,0 +1,38 @@ +// @requires-libnode +// @order: race +// Switch: work of every kind is pending in the program's queues when Node loads +// during the main body: timers (one an interval), an immediate, a tick, a +// microtask and a promise job (fs callbacks: s06). +import { callSync } from "@scriptc/node"; +import { writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +let fired = 0; +setTimeout(() => log("h-t5"), 5); +setTimeout(() => log("h-t0"), 0); +const interval = setInterval(() => { + log("h-iv"); + if (++fired === 3) clearInterval(interval); +}, 3); +setImmediate(() => log("h-imm")); +queueMicrotask(() => log("h-m")); +process.nextTick(() => log("h-k")); +void Promise.resolve().then(() => log("h-p")); +island("T 0 { L i-t0 } T 5 { L i-t5 } I { L i-imm } K { L i-k } M { L i-m } P { L i-p }"); +setTimeout(() => log("h-t0b"), 0); +setImmediate(() => log("h-imm2")); +queueMicrotask(() => log("h-m2")); +process.nextTick(() => log("h-k2")); +log("sync"); diff --git a/tests/corpus-node-islands-order/s02-load-in-timer-callback/main.ts b/tests/corpus-node-islands-order/s02-load-in-timer-callback/main.ts new file mode 100644 index 000000000..fff8853d5 --- /dev/null +++ b/tests/corpus-node-islands-order/s02-load-in-timer-callback/main.ts @@ -0,0 +1,35 @@ +// @requires-libnode +// @order: race +// Switch: Node loads inside a timer callback while timers of the same and later +// deadlines, an immediate and an interval are pending. +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +let fired = 0; +const interval = setInterval(() => { + log("h-iv"); + if (++fired === 2) clearInterval(interval); +}, 1); +setTimeout(() => { + log("h-a"); + island("T 0 { L i-t0 } I { L i-imm } K { L i-k } M { L i-m }"); + queueMicrotask(() => log("h-a-m")); + process.nextTick(() => log("h-a-k")); + log("h-a-end"); +}, 1); +setTimeout(() => log("h-b"), 1); +setTimeout(() => log("h-c"), 4); +setImmediate(() => log("h-imm")); diff --git a/tests/corpus-node-islands-order/s03-load-in-immediate/main.ts b/tests/corpus-node-islands-order/s03-load-in-immediate/main.ts new file mode 100644 index 000000000..abbe2e637 --- /dev/null +++ b/tests/corpus-node-islands-order/s03-load-in-immediate/main.ts @@ -0,0 +1,28 @@ +// @requires-libnode +// Switch: Node loads inside an immediate while later immediates of the same check +// phase, and timers, are pending. +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +setImmediate(() => { + log("h-i1"); + island("I { L i-imm } T 0 { L i-t0 } K { L i-k }"); + setImmediate(() => log("h-i1-imm")); + process.nextTick(() => log("h-i1-k")); +}); +setImmediate(() => log("h-i2")); +setImmediate(() => log("h-i3")); +setTimeout(() => log("h-t0"), 0); diff --git a/tests/corpus-node-islands-order/s04-load-in-promise-job/main.ts b/tests/corpus-node-islands-order/s04-load-in-promise-job/main.ts new file mode 100644 index 000000000..fa6f30876 --- /dev/null +++ b/tests/corpus-node-islands-order/s04-load-in-promise-job/main.ts @@ -0,0 +1,35 @@ +// @requires-libnode +// Switch: Node loads in an async function's continuation while more promise +// jobs, ticks and timers are pending. +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +async function main(): Promise { + log("h-start"); + await null; + log("h-after-await"); + queueMicrotask(() => log("h-m-before")); + island("P { L i-p } M { L i-m } K { L i-k } T 0 { L i-t0 }"); + queueMicrotask(() => log("h-m-after")); + await null; + log("h-after-await2"); + process.nextTick(() => log("h-k")); + await new Promise((resolve) => setTimeout(resolve, 2)); + log("h-after-timer"); +} +void Promise.resolve().then(() => log("h-p")); +setTimeout(() => log("h-t0"), 0); +void main(); diff --git a/tests/corpus-node-islands-order/s05-load-with-open-socket/main.ts b/tests/corpus-node-islands-order/s05-load-with-open-socket/main.ts new file mode 100644 index 000000000..fe8181754 --- /dev/null +++ b/tests/corpus-node-islands-order/s05-load-with-open-socket/main.ts @@ -0,0 +1,41 @@ +// @requires-libnode +// Switch: a TCP server and a client connection of the program are open when Node +// loads: their I/O continues on Node's loop. +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; +import { createConnection, createServer, type Socket } from "node:net"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +const server = createServer((socket: Socket) => { + socket.on("data", (data: Buffer) => { + log(`h-server-data ${data.toString()}`); + socket.end("pong"); + }); +}); +server.listen(0, "127.0.0.1", () => { + const address = server.address(); + const port = typeof address === "object" && address !== null ? address.port : 0; + log("h-listening"); + const client = createConnection(port, "127.0.0.1", () => { + log("h-connected"); + island("K { L i-k } M { L i-m } T 200 { L i-late }"); + client.write("ping"); + }); + client.on("data", (data: Buffer) => log(`h-client-data ${data.toString()}`)); + client.on("end", () => { + log("h-client-end"); + server.close(() => log("h-closed")); + }); +}); diff --git a/tests/corpus-node-islands-order/s06-load-with-pending-fs-and-unref/main.ts b/tests/corpus-node-islands-order/s06-load-with-pending-fs-and-unref/main.ts new file mode 100644 index 000000000..a01815e05 --- /dev/null +++ b/tests/corpus-node-islands-order/s06-load-with-pending-fs-and-unref/main.ts @@ -0,0 +1,29 @@ +// @requires-libnode +// Switch: fs callbacks and an unref'd timer are pending when Node loads; the +// unref'd timer must not keep the program alive, the refreshed one must. +import { callSync } from "@scriptc/node"; +import { readFile, writeSync } from "node:fs"; + +function log(s: string): void { + writeSync(1, `${s}\n`); +} + +function busy(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +function island(src: string): void { + callSync("../island.cjs", "run", src); +} + +const unrefd = setTimeout(() => log("h-unref-never"), 50); +unrefd.unref(); +const refreshed = setTimeout(() => log("h-refreshed"), 5); +readFile(import.meta.filename, "utf8", () => { + log("h-io-1"); + readFile(import.meta.filename, "utf8", () => log("h-io-2")); +}); +island("T 2 { L i-t2 }"); +refreshed.refresh(); +log(`has-ref ${String(unrefd.hasRef())} ${String(refreshed.hasRef())}`); diff --git a/tests/corpus/6804-node-island-signals/main.mjs b/tests/corpus/6804-node-island-signals/main.mjs new file mode 100644 index 000000000..aaed001e7 --- /dev/null +++ b/tests/corpus/6804-node-island-signals/main.mjs @@ -0,0 +1,54 @@ +// @requires-libnode +// Signal listeners that island code adds and removes join the program's own +// signal registry at their registration position: every listener of both +// sides runs in registration order, a once listener runs once, and removing +// an island listener leaves the program's handler for the same signal in +// place (libuv never owns the signal, so the removal does not reset it to +// its default disposition and the next signal does not kill the process). +// JavaScript, for the signal names the typed declarations do not cover. +import { callSync } from "@scriptc/node"; + +function island(name, signal, label) { + callSync("./process.cjs", name, signal, label); +} + +function later(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function main() { + const native = (name) => console.log(`native ${name}`); + process.on("SIGUSR2", native); + island("listen", "SIGUSR2", "b"); + island("listenOnce", "SIGUSR2", "once"); + const second = () => console.log("native second"); + process.on("SIGUSR2", second); + + process.kill(process.pid, "SIGUSR2"); + await later(50); + console.log("-- island removes b"); + island("unlisten", "SIGUSR2", "b"); + process.kill(process.pid, "SIGUSR2"); + await later(50); + + console.log("-- island adds and removes a SIGHUP listener"); + const hup = () => console.log("native SIGHUP"); + process.on("SIGHUP", hup); + island("listen", "SIGHUP", "hup"); + island("unlisten", "SIGHUP", "hup"); + process.kill(process.pid, "SIGHUP"); + await later(50); + + console.log("-- island listens to a signal the program does not"); + island("listen", "SIGUSR1", "usr1"); + process.kill(process.pid, "SIGUSR1"); + await later(50); + island("unlisten", "SIGUSR1", "usr1"); + + process.removeListener("SIGUSR2", native); + process.removeListener("SIGUSR2", second); + process.removeListener("SIGHUP", hup); + console.log("done"); +} + +void main(); diff --git a/tests/corpus/6804-node-island-signals/process.cjs b/tests/corpus/6804-node-island-signals/process.cjs new file mode 100644 index 000000000..cb9c9f03c --- /dev/null +++ b/tests/corpus/6804-node-island-signals/process.cjs @@ -0,0 +1,23 @@ +// Process events from island code (scope-B NI-3). +const listeners = new Map(); + +function listener(label) { + let fn = listeners.get(label); + if (fn === undefined) { + fn = (name, number) => console.log(`island ${label} ${name} ${typeof number}`); + listeners.set(label, fn); + } + return fn; +} + +exports.listen = (signal, label) => { + process.on(signal, listener(label)); +}; + +exports.listenOnce = (signal, label) => { + process.once(signal, listener(label)); +}; + +exports.unlisten = (signal, label) => { + process.removeListener(signal, listener(label)); +}; diff --git a/tests/corpus/6805-node-island-exit-code/main.ts b/tests/corpus/6805-node-island-exit-code/main.ts new file mode 100644 index 000000000..f57740850 --- /dev/null +++ b/tests/corpus/6805-node-island-exit-code/main.ts @@ -0,0 +1,15 @@ +// @exit: 3 +// @requires-libnode +// The exit code island code leaves in process.exitCode is the program's, +// and 'exit' listeners of both sides run in registration order with it. +import { callSync } from "@scriptc/node"; + +process.on("exit", (code) => console.log("native exit listener 1", code)); +callSync("./process.cjs", "onExit", "a"); +process.on("exit", (code) => console.log("native exit listener 2", code)); +callSync("./process.cjs", "onExit", "b"); +setTimeout(() => { + callSync("./process.cjs", "setExitCode", 3); + console.log("timer done"); +}, 5); +console.log("main done"); diff --git a/tests/corpus/6805-node-island-exit-code/process.cjs b/tests/corpus/6805-node-island-exit-code/process.cjs new file mode 100644 index 000000000..bf1d67e56 --- /dev/null +++ b/tests/corpus/6805-node-island-exit-code/process.cjs @@ -0,0 +1,7 @@ +exports.setExitCode = (code) => { + process.exitCode = code; +}; + +exports.onExit = (label) => { + process.on("exit", (code) => console.log(`island exit listener ${label} ${code}`)); +}; diff --git a/tests/corpus/6806-node-island-exit-in-call/main.ts b/tests/corpus/6806-node-island-exit-in-call/main.ts new file mode 100644 index 000000000..9fde1d86b --- /dev/null +++ b/tests/corpus/6806-node-island-exit-in-call/main.ts @@ -0,0 +1,22 @@ +// @exit: 4 +// @requires-libnode +// process.exit in island code, called from a timer while more timers and +// an interval are pending: the program ends at once with the code, after +// the 'exit' listeners of both sides ran in registration order. +import { callSync } from "@scriptc/node"; + +process.on("exit", (code) => console.log("native exit listener", code)); +callSync("./process.cjs", "onExit", "a"); +let intervals = 0; +const interval = setInterval(() => { + intervals++; +}, 1); +setTimeout(() => { + console.log("timer", intervals > 0); + callSync("./process.cjs", "exitNow", 4); + console.log("not reached"); +}, 10); +setTimeout(() => { + clearInterval(interval); + console.log("never"); +}, 50); diff --git a/tests/corpus/6806-node-island-exit-in-call/process.cjs b/tests/corpus/6806-node-island-exit-in-call/process.cjs new file mode 100644 index 000000000..4d8a24dc0 --- /dev/null +++ b/tests/corpus/6806-node-island-exit-in-call/process.cjs @@ -0,0 +1,8 @@ +exports.onExit = (label) => { + process.on("exit", (code) => console.log(`island exit listener ${label} ${code}`)); +}; + +exports.exitNow = (code) => { + console.log("island exits"); + process.exit(code); +}; diff --git a/tests/corpus/6807-node-island-exit-in-island-timer/main.ts b/tests/corpus/6807-node-island-exit-in-island-timer/main.ts new file mode 100644 index 000000000..ec8d5300b --- /dev/null +++ b/tests/corpus/6807-node-island-exit-in-island-timer/main.ts @@ -0,0 +1,17 @@ +// @exit: 5 +// @requires-libnode +// An island timer calls process.exit while the program's own timers keep +// the loop busy: the code and the exit listeners are the program's. +import { callSync } from "@scriptc/node"; + +process.on("exit", (code) => console.log("native exit listener", code)); +callSync("./process.cjs", "exitLater", 5, 20); +let ticks = 0; +const interval = setInterval(() => { + ticks++; +}, 2); +setTimeout(() => { + clearInterval(interval); + console.log("never"); +}, 200); +setTimeout(() => console.log("native timer before", ticks > 0), 10); diff --git a/tests/corpus/6807-node-island-exit-in-island-timer/process.cjs b/tests/corpus/6807-node-island-exit-in-island-timer/process.cjs new file mode 100644 index 000000000..321a0a7f1 --- /dev/null +++ b/tests/corpus/6807-node-island-exit-in-island-timer/process.cjs @@ -0,0 +1,7 @@ +exports.exitLater = (code, ms) => { + process.on("exit", (exitCode) => console.log(`island exit listener ${exitCode}`)); + setTimeout(() => { + console.log("island timer exits"); + process.exit(code); + }, ms); +}; diff --git a/tests/corpus/6808-node-island-exit-from-program/main.ts b/tests/corpus/6808-node-island-exit-from-program/main.ts new file mode 100644 index 000000000..bcd8e1b3b --- /dev/null +++ b/tests/corpus/6808-node-island-exit-from-program/main.ts @@ -0,0 +1,15 @@ +// @exit: 6 +// @requires-libnode +// The program's process.exit runs the 'exit' listeners island code +// registered, at their position, with its code; pending island work is +// dropped, as is anything the listeners schedule. +import { callSync } from "@scriptc/node"; + +callSync("./process.cjs", "onExit", "a"); +process.on("exit", (code) => console.log("native exit listener", code)); +callSync("./process.cjs", "onExit", "b"); +callSync("./process.cjs", "pending"); +setTimeout(() => { + console.log("exiting"); + process.exit(6); +}, 5); diff --git a/tests/corpus/6808-node-island-exit-from-program/process.cjs b/tests/corpus/6808-node-island-exit-from-program/process.cjs new file mode 100644 index 000000000..c6f0f242b --- /dev/null +++ b/tests/corpus/6808-node-island-exit-from-program/process.cjs @@ -0,0 +1,10 @@ +exports.onExit = (label) => { + process.on("exit", (code) => { + console.log(`island exit listener ${label} ${code}`); + process.nextTick(() => console.log("tick from an exit listener never runs")); + }); +}; + +exports.pending = () => { + setTimeout(() => console.log("island timer never fires"), 100); +}; diff --git a/tests/corpus/6809-node-island-stdio/main.ts b/tests/corpus/6809-node-island-stdio/main.ts new file mode 100644 index 000000000..209dda8a5 --- /dev/null +++ b/tests/corpus/6809-node-island-stdio/main.ts @@ -0,0 +1,21 @@ +// @requires-libnode +// stdout and stderr are the same descriptors on both sides, and every +// write is complete when it returns, so the lines of both sides interleave +// in program order, large writes included. +import { callSync } from "@scriptc/node"; + +console.log("native log 1"); +callSync("./stdio.cjs", "write", "1"); +process.stdout.write("native write 1\n"); +console.error("native error 1"); +callSync("./stdio.cjs", "big", 200000); +process.stdout.write(`${"n".repeat(200000)}\n`); +callSync("./stdio.cjs", "later", 5); +setTimeout(() => { + console.log("native timer log"); + callSync("./stdio.cjs", "write", "2"); + console.error("native timer error"); +}, 10); +setImmediate(() => console.log("native immediate log")); +process.on("exit", () => console.log("native exit listener log")); +console.log("native log 2"); diff --git a/tests/corpus/6809-node-island-stdio/stdio.cjs b/tests/corpus/6809-node-island-stdio/stdio.cjs new file mode 100644 index 000000000..87adbb12d --- /dev/null +++ b/tests/corpus/6809-node-island-stdio/stdio.cjs @@ -0,0 +1,21 @@ +exports.write = (label) => { + console.log(`island log ${label}`); + process.stdout.write(`island write ${label}\n`); + console.error(`island error ${label}`); +}; + +exports.big = (n) => { + process.stdout.write(`${"i".repeat(n)}\n`); +}; + +exports.later = (ms) => { + setTimeout(() => { + console.log("island timer log"); + console.error("island timer error"); + }, ms); + setImmediate(() => process.stdout.write("island immediate write\n")); + process.on("exit", () => { + console.log("island exit listener log"); + console.error("island exit listener error"); + }); +}; diff --git a/tests/corpus/6810-node-island-children/children.cjs b/tests/corpus/6810-node-island-children/children.cjs new file mode 100644 index 000000000..6d6579db6 --- /dev/null +++ b/tests/corpus/6810-node-island-children/children.cjs @@ -0,0 +1,9 @@ +const { execFile } = require("node:child_process"); + +exports.run = () => + new Promise((resolve, reject) => { + execFile("/bin/echo", ["island child"], (error, stdout) => { + if (error) reject(error); + else resolve(stdout.trim()); + }); + }); diff --git a/tests/corpus/6810-node-island-children/main.ts b/tests/corpus/6810-node-island-children/main.ts new file mode 100644 index 000000000..d59a63fb6 --- /dev/null +++ b/tests/corpus/6810-node-island-children/main.ts @@ -0,0 +1,27 @@ +// @requires-libnode +// Children of both sides: island code's first spawn installs libuv's SIGCHLD +// handler, and the program's own children are still reaped (scriptc waits +// for its pids itself, libuv only reaps its own). +import { call } from "@scriptc/node"; +import { spawn, spawnSync } from "node:child_process"; + +function nativeChild(): Promise { + return new Promise((resolve) => { + const child = spawn("/bin/sh", ["-c", "echo native child; exit 3"], { + stdio: ["ignore", "inherit", "inherit"], + }); + child.on("close", (code: number | null) => resolve(code ?? -1)); + }); +} + +async function main(): Promise { + console.log("island:", await call("./children.cjs", "run")); + console.log("native exit", await nativeChild()); + console.log("island again:", await call("./children.cjs", "run")); + const sync = spawnSync("/bin/sh", ["-c", "exit 4"]); + console.log("spawnSync status", sync.status); + const [a, b] = await Promise.all([nativeChild(), call("./children.cjs", "run")]); + console.log("together", a, b); +} + +void main(); diff --git a/tests/corpus/6811-node-island-uncaught-listener/island.cjs b/tests/corpus/6811-node-island-uncaught-listener/island.cjs new file mode 100644 index 000000000..6cb97bed4 --- /dev/null +++ b/tests/corpus/6811-node-island-uncaught-listener/island.cjs @@ -0,0 +1,4 @@ +exports.timers = () => { + setTimeout(() => console.log("island timer 1"), 1); + setTimeout(() => console.log("island timer 2"), 20); +}; diff --git a/tests/corpus/6811-node-island-uncaught-listener/main.mjs b/tests/corpus/6811-node-island-uncaught-listener/main.mjs new file mode 100644 index 000000000..9195e7baa --- /dev/null +++ b/tests/corpus/6811-node-island-uncaught-listener/main.mjs @@ -0,0 +1,20 @@ +// @requires-libnode +// The program's uncaughtException listener handles a throw from a native +// callback after Node loaded, and the loop goes on with the work of both +// sides. JavaScript: the typed declarations have no uncaughtException. +import { callSync } from "@scriptc/node"; + +process.on("uncaughtException", (error) => { + console.log("caught", error.message); +}); +callSync("./island.cjs", "timers"); +setTimeout(() => { + console.log("native timer throws"); + throw new Error("native boom"); +}, 5); +setTimeout(() => console.log("native timer after"), 10); +setImmediate(() => { + queueMicrotask(() => { + throw new Error("from a microtask"); + }); +}); diff --git a/tests/fixtures/node-island-process/island-rejection.ts b/tests/fixtures/node-island-process/island-rejection.ts new file mode 100644 index 000000000..0d809aae9 --- /dev/null +++ b/tests/fixtures/node-island-process/island-rejection.ts @@ -0,0 +1,6 @@ +// An island promise rejects unhandled: exit 1 with Node's report. +import { callSync } from "@scriptc/node"; + +process.on("exit", (code) => console.log("native exit listener", code)); +callSync("./island.cjs", "rejectLater", 5); +setTimeout(() => console.log("native timer never"), 50); diff --git a/tests/fixtures/node-island-process/island-uncaught.ts b/tests/fixtures/node-island-process/island-uncaught.ts new file mode 100644 index 000000000..ae54321a5 --- /dev/null +++ b/tests/fixtures/node-island-process/island-uncaught.ts @@ -0,0 +1,7 @@ +// An island timer throws: Node's uncaught report, exit 1, and the program's +// 'exit' listener with the code. +import { callSync } from "@scriptc/node"; + +process.on("exit", (code) => console.log("native exit listener", code)); +callSync("./island.cjs", "throwLater", 5); +setTimeout(() => console.log("native timer never"), 50); diff --git a/tests/fixtures/node-island-process/island.cjs b/tests/fixtures/node-island-process/island.cjs new file mode 100644 index 000000000..4de8186b6 --- /dev/null +++ b/tests/fixtures/node-island-process/island.cjs @@ -0,0 +1,20 @@ +// The island half of the process-integration fixtures (scope-B NI-3). +exports.onExit = (label) => { + process.on("exit", (code) => console.log(`island exit listener ${label} ${code}`)); +}; + +exports.throwLater = (ms) => { + setTimeout(() => { + throw new Error("island boom"); + }, ms); +}; + +exports.rejectLater = (ms) => { + setTimeout(() => { + Promise.reject(new Error("island rejected")); + }, ms); +}; + +exports.timer = (ms, label) => { + setTimeout(() => console.log(`island timer ${label}`), ms); +}; diff --git a/tests/fixtures/node-island-process/native-rejection.ts b/tests/fixtures/node-island-process/native-rejection.ts new file mode 100644 index 000000000..e320309ed --- /dev/null +++ b/tests/fixtures/node-island-process/native-rejection.ts @@ -0,0 +1,10 @@ +// A native promise rejects unhandled after Node loaded: exit 1 with the +// report, the island's 'exit' listener runs. +import { callSync } from "@scriptc/node"; + +callSync("./island.cjs", "onExit", "a"); +setTimeout(() => { + console.log("native timer"); + void Promise.reject(new Error("native rejected")); +}, 5); +setTimeout(() => console.log("native timer never"), 50); diff --git a/tests/fixtures/node-island-process/native-uncaught.ts b/tests/fixtures/node-island-process/native-uncaught.ts new file mode 100644 index 000000000..2f6053e55 --- /dev/null +++ b/tests/fixtures/node-island-process/native-uncaught.ts @@ -0,0 +1,10 @@ +// A native timer throws after Node loaded: exit 1, the uncaught report, and +// the island's 'exit' listener with the code. +import { callSync } from "@scriptc/node"; + +callSync("./island.cjs", "onExit", "a"); +callSync("./island.cjs", "timer", 50, "never"); +setTimeout(() => { + console.log("native timer"); + throw new Error("native boom"); +}, 5); diff --git a/tests/harness/node-island-order.test.ts b/tests/harness/node-island-order.test.ts new file mode 100644 index 000000000..ce2bd1bef --- /dev/null +++ b/tests/harness/node-island-order.test.ts @@ -0,0 +1,115 @@ +/* Ordering fidelity of the hosted event loop (scope-B milestone NI-3, + * design §5.2). Once a scriptc program loads Node, its ticks, promise + * jobs, timers, immediates and I/O run on Node's loop together with island + * code's, so the interleaving of both sides must be one Node itself + * produces. Each program in tests/corpus-node-islands-order/ (the native + * half in main.ts, the island half interpreted by island.cjs) runs M times + * under Node, which collects the set of orders Node produces, and N times + * as a compiled binary; every native run must produce an output in Node's + * set. Programs whose order is fully determined by the queues produce one + * output; the I/O races (fs callbacks against busy timers) may produce + * several, and the native runs must stay within them. + * + * A program whose head says `// @order: race` races a timer against I/O + * whose duration differs between the sides (scriptc's own fs callbacks + * complete in one turn where Node's readFile takes several; module loading + * on the first call into Node takes longer than in a Node process), so + * Node permits orders it rarely produces in M runs. Its native runs must + * produce the lines of one of Node's outputs, in any order; the report + * still records whether the order itself was among Node's. + * + * The s* programs pin the one-time switch: work already pending in the + * program's queues (timers, immediates, ticks, microtasks, promise jobs, + * fs callbacks, an open socket) at the moment Node loads, in the main + * body, a timer, an immediate and a promise job. + * + * Needs a libnode pack in SCRIPTC_LIBNODE whose Node version is the `node` + * on PATH. NODE_ISLAND_ORDER_NODE_RUNS (default 20) and + * NODE_ISLAND_ORDER_NATIVE_RUNS (default 3) set M and N; + * NODE_ISLAND_ORDER_REPORT names a JSON file for the per-program counts. */ +import { execFile } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { afterAll, describe, expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const programsDir = join(repoRoot, "tests/corpus-node-islands-order"); +const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/node-island-order"); +const pack = process.env["SCRIPTC_LIBNODE"]; +const skip = (process.platform !== "linux" && process.platform !== "darwin") || !pack; +const nodeRuns = Number(process.env["NODE_ISLAND_ORDER_NODE_RUNS"] ?? 20); +const nativeRuns = Number(process.env["NODE_ISLAND_ORDER_NATIVE_RUNS"] ?? 3); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +const programs = existsSync(programsDir) + ? readdirSync(programsDir, { withFileTypes: true }) + .filter( + (entry) => entry.isDirectory() && existsSync(join(programsDir, entry.name, "main.ts")), + ) + .map((entry) => entry.name) + .sort() + : []; + +/** One run's observable outcome: stdout, stderr and the exit status. */ +async function outcome(cmd: string, args: string[]): Promise { + try { + const { stdout, stderr } = await execFileAsync(cmd, args, { + cwd: repoRoot, + encoding: "utf8", + timeout: 20_000, + env: { ...process.env, NODE_NO_WARNINGS: "1" }, + }); + return `${stdout}${stderr === "" ? "" : `[stderr]\n${stderr}`}[exit 0]`; + } catch (err) { + const e = err as { stdout?: string; stderr?: string; code?: unknown; signal?: unknown }; + if (typeof e.code !== "number") return `[failed ${String(e.signal ?? e.code)}]`; + return `${e.stdout ?? ""}${e.stderr ? `[stderr]\n${e.stderr}` : ""}[exit ${e.code}]`; + } +} + +const report: Record; native: Record }> = {}; + +afterAll(() => { + const path = process.env["NODE_ISLAND_ORDER_REPORT"]; + if (path) writeFileSync(path, `${JSON.stringify(report, null, 2)}\n`); +}); + +describe.skipIf(skip)(`Node island ordering (${programs.length} programs)`, () => { + test.for(programs)("%s", { timeout: 600_000 }, async (name) => { + const main = join(programsDir, name, "main.ts"); + const dir = join(outDir, name); + mkdirSync(dir, { recursive: true }); + const built = await compile(main, { outPath: join(dir, "program"), outDir: dir, sanitize }); + if (!built.ok) + throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const node: Record = {}; + for (let i = 0; i < nodeRuns; i++) { + const out = await outcome("node", [main]); + node[out] = (node[out] ?? 0) + 1; + } + const native: Record = {}; + for (let i = 0; i < nativeRuns; i++) { + const out = await outcome(built.binaryPath, []); + native[out] = (native[out] ?? 0) + 1; + } + report[name] = { node, native }; + const race = readFileSync(main, "utf8").split("\n", 3).includes("// @order: race"); + const lines = (out: string): string => out.split("\n").sort().join("\n"); + if (race) { + const nodeLines = Object.keys(node).map(lines); + for (const out of Object.keys(native)) + expect(nodeLines, `native lines not produced by Node in ${nodeRuns} runs`).toContain( + lines(out), + ); + return; + } + for (const out of Object.keys(native)) { + expect(Object.keys(node), `native output not produced by Node in ${nodeRuns} runs`).toContain( + out, + ); + } + }); +}); diff --git a/tests/harness/node-island-process.test.ts b/tests/harness/node-island-process.test.ts new file mode 100644 index 000000000..5b42400d9 --- /dev/null +++ b/tests/harness/node-island-process.test.ts @@ -0,0 +1,72 @@ +/* Process integration of in-process Node islands on the hosted loop + * (scope-B milestone NI-3) where the corpus cannot compare byte for byte: + * an uncaught exception or an unhandled rejection on either side once Node + * is loaded. Each side prints its own report format (scriptc's for native + * code, Node's for island code), and Node's report quotes source lines and + * stack frames the two runs do not share, so stderr is checked for the + * error, while stdout ('exit' listeners of both sides, with the code) and + * the exit status must match Node exactly. The deterministic cases (signals, + * exit codes, process.exit from either side, stdio) are corpus programs + * 6804-6811. Needs a libnode pack in SCRIPTC_LIBNODE. */ +import { execFile } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const fixtures = join(repoRoot, "tests/fixtures/node-island-process"); +const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/node-island-process"); +const pack = process.env["SCRIPTC_LIBNODE"]; +const skip = (process.platform !== "linux" && process.platform !== "darwin") || !pack; +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +interface Run { + stdout: string; + stderr: string; + code: number | null; +} + +async function run(cmd: string, args: string[]): Promise { + try { + const { stdout, stderr } = await execFileAsync(cmd, args, { + cwd: repoRoot, + encoding: "utf8", + timeout: 20_000, + }); + return { stdout, stderr, code: 0 }; + } catch (err) { + const e = err as { stdout?: string; stderr?: string; code?: unknown }; + if (typeof e.code !== "number") throw err; + return { stdout: e.stdout ?? "", stderr: e.stderr ?? "", code: e.code }; + } +} + +async function both(name: string): Promise<{ node: Run; native: Run }> { + const main = join(fixtures, `${name}.ts`); + const dir = join(outDir, name); + mkdirSync(dir, { recursive: true }); + const built = await compile(main, { outPath: join(dir, "program"), outDir: dir, sanitize }); + if (!built.ok) + throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const [node, native] = await Promise.all([run("node", [main]), run(built.binaryPath, [])]); + return { node, native }; +} + +describe.skipIf(skip)("Node island process integration", () => { + test.for([ + ["native-uncaught", "Error: native boom"], + ["island-uncaught", "Error: island boom"], + ["native-rejection", "native rejected"], + ["island-rejection", "island rejected"], + ] as const)("%s", { timeout: 300_000 }, async ([name, error]) => { + const { node, native } = await both(name); + expect(node.code).toBe(1); + expect(native.code).toBe(node.code); + expect(native.stdout).toBe(node.stdout); + expect(node.stderr).toContain(error); + expect(native.stderr).toContain(error); + }); +}); diff --git a/tests/harness/node-island.test.ts b/tests/harness/node-island.test.ts index 47a68e290..5b93a2af1 100644 --- a/tests/harness/node-island.test.ts +++ b/tests/harness/node-island.test.ts @@ -160,7 +160,7 @@ describe.skipIf(skip)("Node islands in-process", () => { const result = await run(binary, [], { ...process.env, SCRIPTC_LIBNODE: dir }); expect(result.code).toBe(1); expect(result.stderr).toBe( - `scriptc: the libnode pack at ${dir} has an incompatible bridge (ABI 999; this program needs 2)\n`, + `scriptc: the libnode pack at ${dir} has an incompatible bridge (ABI 999; this program needs 3)\n`, ); });