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`, ); });