mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Host the runtime's event loop on Node's libuv once Node loads (NI-3)
Scope-B milestone NI-3. When a program loads Node in-process, its pending ticks, promise jobs, timers and immediates move onto Node's queues in order (timers keep their deadlines relative to the moment Node began loading; the work of a phase the switch interrupted finishes first) and every later one is scheduled there, so the jobs of both sides interleave as in Node. The runtime's I/O hooks are watched from Node's loop, and its liveness is re-read before each poll phase, after each check phase and after every microtask checkpoint. Node's loop runs on Node's stack, and V8's limit follows whichever stack an entry comes from. Island signal and exit listeners join the runtime's registries at their position, so the runtime keeps owning every signal disposition; Node's own 'exit' emission is left to the runtime's exit sequence, and the exit code island code leaves in process.exitCode becomes the program's. Bridge ABI 3. Ordering programs (tests/corpus-node-islands-order, harness node-island-order.test.ts), corpus programs 6804-6811 and the node-island-process harness pin the behavior against Node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
50a34224b9
commit
eaefbdf44f
@@ -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,
|
||||
|
||||
@@ -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/";
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6804-node-island-signals/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6805-node-island-exit-code/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6806-node-island-exit-in-call/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6807-node-island-exit-in-island-timer/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6808-node-island-exit-from-program/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6809-node-island-stdio/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6810-node-island-children/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6811-node-island-uncaught-listener/main.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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 <dlfcn.h>
|
||||
@@ -20,10 +29,12 @@
|
||||
#include <uv.h>
|
||||
#include <v8.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -49,6 +60,10 @@ node::EmbedderSnapshotData::Pointer g_snapshot;
|
||||
std::unique_ptr<node::CommonEnvironmentSetup> g_setup;
|
||||
Locker *g_locker = nullptr;
|
||||
Global<Function> g_call;
|
||||
// The bootstrap's hosted-loop helpers (scr_node_island_host.cjs).
|
||||
Global<Function> g_set_timer, g_clear_timer, g_ref_timer, g_refresh_timer;
|
||||
Global<Function> g_set_immediate, g_clear_immediate, g_ref_immediate;
|
||||
Global<Function> g_next_tick, g_emit;
|
||||
|
||||
char *Copy(const std::string &text, size_t *len) {
|
||||
char *out = static_cast<char *>(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<String> Utf8(Isolate *isolate, const char *text, size_t len) {
|
||||
return String::NewFromUtf8(isolate, text, NewStringType::kNormal, static_cast<int>(len))
|
||||
.ToLocalChecked();
|
||||
@@ -96,6 +134,39 @@ void Settle(const FunctionCallbackInfo<Value> &info) {
|
||||
g_host->settle(id, outcome.data(), outcome.size());
|
||||
}
|
||||
|
||||
uint32_t Uint32Arg(const FunctionCallbackInfo<Value> &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<Value> &info) {
|
||||
g_host->run(Uint32Arg(info, 0), Uint32Arg(info, 1));
|
||||
}
|
||||
|
||||
// listen(event, name, listener, add, once): island process listeners.
|
||||
void Listen(const FunctionCallbackInfo<Value> &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> context, Local<Object> 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> context, Local<Object> from, const char *name,
|
||||
Global<Function> *to) {
|
||||
Local<Value> value;
|
||||
if (!from->Get(context, Utf8(isolate, name)).ToLocal(&value) || !value->IsFunction())
|
||||
return false;
|
||||
to->Reset(isolate, value.As<Function>());
|
||||
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<Object> global = context->Global();
|
||||
Local<Object> 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<Value> dispatcher;
|
||||
Local<Value> call;
|
||||
if (!global->Get(context, Utf8(isolate, "__scriptcNodeIsland")).ToLocal(&dispatcher) ||
|
||||
!dispatcher->IsObject() ||
|
||||
!dispatcher.As<Object>()->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<Function>());
|
||||
Local<Object> d = dispatcher.As<Object>();
|
||||
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> 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<Value> 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;
|
||||
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<Value> Helper(Enter &e, Global<Function> &fn, int argc, Local<Value> *argv) {
|
||||
TryCatch try_catch(e.isolate);
|
||||
Local<Value> result;
|
||||
if (!fn.Get(e.isolate)->Call(e.context, v8::Undefined(e.isolate), argc, argv).ToLocal(&result))
|
||||
return Local<Value>();
|
||||
return result;
|
||||
}
|
||||
|
||||
void Pump() {
|
||||
Isolate *isolate = g_setup->isolate();
|
||||
HandleScope handles(isolate);
|
||||
Context::Scope context_scope(g_setup->context());
|
||||
Local<Value> Num(Enter &e, double value) { return v8::Number::New(e.isolate, value); }
|
||||
|
||||
uint32_t HelperHandle(Enter &e, Local<Value> 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<int, PollEntry *> 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<PollEntry *>(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<uv_handle_t *>(&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<uv_handle_t *>(&g_prepare));
|
||||
else uv_unref(reinterpret_cast<uv_handle_t *>(&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<uv_handle_t *>(&g_cap)))
|
||||
uv_timer_start(&g_cap, OnCap, static_cast<uint64_t>(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<uv_handle_t *>(&g_check));
|
||||
uv_idle_init(loop, &g_idle);
|
||||
uv_unref(reinterpret_cast<uv_handle_t *>(&g_idle));
|
||||
uv_timer_init(loop, &g_cap);
|
||||
uv_unref(reinterpret_cast<uv_handle_t *>(&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<uv_handle_t *>(&entry.second->handle), ClosePoll);
|
||||
}
|
||||
g_polls.clear();
|
||||
uv_close(reinterpret_cast<uv_handle_t *>(&g_prepare), nullptr);
|
||||
uv_close(reinterpret_cast<uv_handle_t *>(&g_check), nullptr);
|
||||
uv_close(reinterpret_cast<uv_handle_t *>(&g_idle), nullptr);
|
||||
uv_close(reinterpret_cast<uv_handle_t *>(&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<uint32_t>(reinterpret_cast<uintptr_t>(data)));
|
||||
}
|
||||
|
||||
void Microtask(uint32_t token) {
|
||||
g_setup->isolate()->EnqueueMicrotask(RunMicrotask,
|
||||
reinterpret_cast<void *>(static_cast<uintptr_t>(token)));
|
||||
}
|
||||
|
||||
void Tick(uint32_t token) {
|
||||
Enter e;
|
||||
Local<Value> 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<Value> 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<Value> argv[] = {Num(e, handle)};
|
||||
Helper(e, g_clear_timer, 1, argv);
|
||||
}
|
||||
|
||||
void TimerRef(uint32_t handle, int ref) {
|
||||
Enter e;
|
||||
Local<Value> 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<Value> argv[] = {Num(e, handle)};
|
||||
Helper(e, g_refresh_timer, 1, argv);
|
||||
}
|
||||
|
||||
uint32_t Immediate(int ref, uint32_t token) {
|
||||
Enter e;
|
||||
Local<Value> 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<Value> argv[] = {Num(e, handle)};
|
||||
Helper(e, g_clear_immediate, 1, argv);
|
||||
}
|
||||
|
||||
void ImmediateRef(uint32_t handle, int ref) {
|
||||
Enter e;
|
||||
Local<Value> 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<Value> 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<Function> *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
|
||||
|
||||
@@ -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 <pack dir> <module path> */
|
||||
* 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 <pack dir> <module path> */
|
||||
#include <dlfcn.h>
|
||||
#include <poll.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"}}`,
|
||||
|
||||
@@ -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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 <errno.h>
|
||||
#include <fcntl.h>
|
||||
@@ -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);
|
||||
|
||||
@@ -9,17 +9,56 @@
|
||||
* are "R<json>" (returned; "R" alone is undefined) or "T<json>" (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 <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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;
|
||||
|
||||
@@ -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 <dlfcn.h>
|
||||
#include <sys/mman.h>
|
||||
#include <ucontext.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#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) {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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<json>" (returned; "R" alone is undefined)
|
||||
* or "T<json>" (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,
|
||||
};
|
||||
|
||||
@@ -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<json>\" (returned; \"R\" alone is undefined)\012"
|
||||
" * or \"T<json>\" (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 */
|
||||
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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 }");
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
@@ -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"));
|
||||
@@ -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");
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
@@ -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"));
|
||||
});
|
||||
@@ -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");
|
||||
@@ -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);
|
||||
@@ -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 } }");
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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<void> {
|
||||
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");
|
||||
@@ -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"));
|
||||
@@ -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");
|
||||
@@ -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"));
|
||||
@@ -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);
|
||||
@@ -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<void> {
|
||||
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<void>((resolve) => setTimeout(resolve, 2));
|
||||
log("h-after-timer");
|
||||
}
|
||||
void Promise.resolve().then(() => log("h-p"));
|
||||
setTimeout(() => log("h-t0"), 0);
|
||||
void main();
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
@@ -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())}`);
|
||||
@@ -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();
|
||||
@@ -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));
|
||||
};
|
||||
@@ -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");
|
||||
@@ -0,0 +1,7 @@
|
||||
exports.setExitCode = (code) => {
|
||||
process.exitCode = code;
|
||||
};
|
||||
|
||||
exports.onExit = (label) => {
|
||||
process.on("exit", (code) => console.log(`island exit listener ${label} ${code}`));
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
});
|
||||
};
|
||||
@@ -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());
|
||||
});
|
||||
});
|
||||
@@ -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<number> {
|
||||
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<void> {
|
||||
console.log("island:", await call<string>("./children.cjs", "run"));
|
||||
console.log("native exit", await nativeChild());
|
||||
console.log("island again:", await call<string>("./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<string>("./children.cjs", "run")]);
|
||||
console.log("together", a, b);
|
||||
}
|
||||
|
||||
void main();
|
||||
@@ -0,0 +1,4 @@
|
||||
exports.timers = () => {
|
||||
setTimeout(() => console.log("island timer 1"), 1);
|
||||
setTimeout(() => console.log("island timer 2"), 20);
|
||||
};
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
+20
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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<string> {
|
||||
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<string, { node: Record<string, number>; native: Record<string, number> }> = {};
|
||||
|
||||
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<string, number> = {};
|
||||
for (let i = 0; i < nodeRuns; i++) {
|
||||
const out = await outcome("node", [main]);
|
||||
node[out] = (node[out] ?? 0) + 1;
|
||||
}
|
||||
const native: Record<string, number> = {};
|
||||
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,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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<Run> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user