A call into Node from an async fiber runs in a context of its own on Node's
stack while the calling fiber stays suspended in it, but runtime code that
Node called back there still ran as that fiber: scr_current named it, and
a fiber resumed from there saved its resumer in scr_loop_ctx and, under
ASan, its fake stack in the calling fiber's slot. The calling fiber's next
park then returned to a context that no longer existed, or under ASan
resumed with a fake stack the nested context had destroyed
(6870-node-island-server-fibers in dev mode, sanitized: SEGV in
nh_keep_done). The nested entry now runs host callbacks as the loop's code
does (no current fiber, the main exception cell and ALS slot) and puts the
calling fiber's state and the loop context back when it returns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When an event-loop callback calls an async function or steps a generator
that enters a store before its first await, the store becomes the
callback's at the end of the prefix. On the main slot it now ends with
that callback, as an enterWith there does: the copy schedules the tick
that clears the main slot, so the store does not reach later callbacks.
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>
Scope-B milestone NI-1, runtime side. Programs whose IR has a
`nodeIsland.*` operation link scr_node_island.c and the SCR_NODE_ISLANDS
build of scr_async.c; every other program links neither, byte for byte.
- scr_node_island.c dlopens the bridge of a libnode pack on the first call
(RTLD_NOW | RTLD_GLOBAL), checks its bridge ABI and that it links exactly
the Node version recorded at build time, starts one Node environment and
runs the embedded bootstrap (scr_node_island_host.cjs). A run that never
calls into Node never maps libnode. The pack directory is SCRIPTC_LIBNODE
at run time, else the recorded path; a `$ORIGIN/` path resolves against
the executable's directory (the beside packaging, and the hook for
shipping Node with the program).
- scr_node_bridge.h is the C ABI between the runtime and the bridge; the
runtime includes no Node or V8 header. The bridge
(packages/libnode-common/bridge/scr_node_bridge.cc) initializes Node once
with no default signal handling, no debug-signal handler or inspector,
no stdio reinitialization and one platform worker, creates the
environment from the built-in snapshot, routes Node's process exit to
the runtime's exit sequence, promotes libnode to the global scope for
native addons, and runs calls in a CallbackScope with a TryCatch.
build-pack.mjs assembles a pack (libnode, the bridge, manifest.json) from
a Node --shared build and the matching headers.
- The SCR_NODE_ISLANDS build of scr_async.c adds a loop slot through which
the runtime's loop drives Node's until NI-3 hosts the runtime's queues on
Node's libuv: it keeps the program alive while Node has ref'd work,
sleeps on Node's backend fd up to its next timer, and runs one
non-blocking turn per loop turn.
- The `nodeIsland` runtime feature (rule, link features, unit table, pack
matrix row and scr_async.c variant), the nodeIsland.call/callSync
libCalls, the install call in main, and the build-time record of the
pack (node-islands/libnode.ts: --libnode=<pack>|beside or
SCRIPTC_LIBNODE, SC8005 when none is usable, Node >=24.13.1).
- The zero-cost gate and the bench nm test require that no good-code
executable defines a Node island symbol.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fs.promises.rm(path, options) accepts the { recursive, force, maxRetries,
retryDelay } literal options that rmSync accepts. It runs the same removal
and rejects with its errors.
Removing a directory without recursive now throws Node's ERR_FS_EISDIR
SystemError from rmSync and fs.promises.rm. A retryDelay outside int32 >= 0
or a maxRetries outside uint32 throws ERR_OUT_OF_RANGE. rmSync checks these
options before the path, and fs.promises.rm checks the path first, as Node
does.
Fixes#1615
Co-authored-by: jlucaso1 <55464917+jlucaso1@users.noreply.github.com>
A full batch of 64 events polled again without returning to the event
loop. When synchronous socket callbacks kept 64 events ready on every
drain, timers and setImmediate callbacks never ran. The poll phase now
drains at most 48 full batches, the count libuv's uv__io_poll uses, and
the budget carries across the reopen that lets microtasks run, so the
remaining events wait for the next loop iteration.
The trim helper returned parked workers' memory once no worker had run for
100 ms. A compiler in watch mode starts its workers again for each rebuild
and faulted the returned pages back in, and a rebuild that runs on the main
thread alone had the discards run alongside it.
- The delay starts at 1 s (SCR_SA_DISCARD_DELAY_MS) and doubles each time
workers start within ten delays of a discard, up to
SCR_SA_DISCARD_DELAY_MAX_MS (10 s by default).
- Once the event loop of the thread that is not a worker runs, the helper
discards one arena at a time, and only while that loop has been in one
wait with nothing ready for a step (scr_sa_loop_waiting, called by the
event loop around its sleep). A loop that does not wait gets the
discards SCR_SA_DISCARD_DELAY_MAX_MS after the delay.
- Programs without a running event loop discard after the delay, as before.
The net unit drained its poller every time the loop came back to the I/O
station, including the restarts that drain microtasks. I/O that became
ready during the iteration's own callbacks (a loopback connect started by
one of them) was delivered before the check phase, so setImmediate
callbacks queued earlier ran after it; Node delivers it in the next
iteration's poll phase. The poller now drains once per iteration (a full
batch keeps draining), reopening after the check phase. Back-to-back
undici requests now reuse connections like Node, so the fixtures drop
their setImmediate turn between requests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The event loop called scr_children_pending, scr_children_poll and the other
child hooks directly on native targets, so every async program linked the
child-process unit (the reap pass, IPC service, pipe readers and writers).
The loop now reaches them through a nullable ScrLoopChildren table that the
child unit installs with its first spawned child or IPC channel, the shape
the events, net, dgram and fs.watch hooks already use. The WASI stubs for
the old direct calls are removed.
A build now translates every WebAssembly module whose bytes it can see
(W2's discovery) into LLVM IR with a TypeScript translator
(packages/compiler/src/wasm/module.ts, translate.ts) and appends it to the
program module, so the existing LLVM helper optimizes, partitions and
caches it with the program. The program also emits scr_wasm_registry, a
portable table of module descriptors (scr_wasm_abi.h). At run time,
WebAssembly.compile, instantiate, validate, new Module and new Instance
find the translated module by the SHA-256 of its bytes; bytes the build did
not see reject with a CompileError. There is no interpreter and no JIT.
The runtime (scr_wasm.c, link-gated by the new `wasm` feature, handle tags
inside W0's headroom) implements the JavaScript API once, over
descriptors, with Node's semantics and messages: exports (functions,
memories, tables, globals), imports (functions, immutable globals, and
memories, tables and globals other instances export), every trap as
WebAssembly.RuntimeError, the CompileError, LinkError and RuntimeError
classes, compile and instantiate settling on a later turn, and
globalThis.WebAssembly as a value. On 64-bit Linux and macOS, memories sit
in 8 GiB guard reservations and a SIGSEGV/SIGBUS handler turns faults into
traps or stack exhaustion; async fibers get a guard page on first use.
Sanitizer builds and Windows check bounds and the stack explicitly. A
WebAssembly.Module crosses to workers through W0's transfer hook (W4).
W1's refusals become lowerings; the Memory, Table and Global constructors,
Module.imports/exports/customSections and the streaming functions still
refuse with SC2020. The globals: WebAssembly compatibility row moves to
partial.
Evidence: differential corpus programs for the API, TypeScript use and the
codemode worker shape, plus five programs executing about 9,300 commands
of the WebAssembly 2.0 spec suite; all match Node in the plain and
sanitized lanes. quickjs-wasi runs through its own glue, matching Node
(apart from a pre-existing non-wasm fence) and on par with or faster than
V8. The zero-cost gate leaves every non-wasm program byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
node:test changes, matching Node 24.22:
- getTestContext() returns the context of the test, suite or hook whose
code is running: undefined at module top level, the root's context in
module-level hooks, the suite's in a suite body and in the suite's
hooks, and the test's in its body. In the afterEach and t.after hooks
of a subtest cancelled by its parent's timeout, it returns each hook's
owner.
- The context rides the AsyncLocalStorage snapshot under a reserved id,
so it follows a test across its awaits, into the async functions,
promise callbacks, timers and other callbacks it schedules, and into
concurrent subtests, which each see their own.
- The root's fullName is "<root>", as its name is.
- Optional chains over a test context, such as getTestContext()?.name,
lower.
AsyncLocalStorage enter and restore move into the always-linked async
core so the test runner can use them. The snapshot holds the node's
index in a table of live nodes, so a snapshot that outlives its node
answers undefined.
The get-test-context and get-test-context-cancelled fixtures compare
the contexts seen at each point with Node, plain and with SCRIPTC_SAN=1.
scr_json.c and scr_inspect.c called scr_gen_tag, which lives in
scr_async.c. Libraries and the runtime's unit tests link those units
without scr_async.c, so their links failed on the undefined symbol.
Both now read the tag through scr_gen_tag_reader, defined in scr_json.c
and set at startup by scr_async.c. A generator capsule only exists when
the generator runtime is linked, so the pointer is always set when read.
console.log and util.inspect of a generator object, an iterator helper,
or an array or string iterator were refused, and printing a Map or Set
iterator threw "util.inspect of a dynamic Array Iterator is not
supported yet".
Compiled generators now carry the tag their object prints with:
"Generator" or "AsyncGenerator" by default, "Iterator Helper" for the
helpers, "Array Iterator" and "String Iterator" for the iterators the
compiler builds as generators. The spawn wrapper sets it through the new
scr_gen_set_tag; an adapter over another generator copies that
generator's tag (scr_gen_copy_tag), and generators that stand in for an
object of another class, such as a Set iterator adapted into an
iterator-typed slot, keep refusing to print. They print as Node's
Object [<tag>] {} at any depth, and Object.prototype.toString answers
[object <tag>].
The runtime's native iterators print as Node does: Object [Array
Iterator] {} and Object [String Iterator] {}, and for Map and Set
iterators the entries the next steps would produce, without advancing,
under [Set Iterator], [Set Entries], [Map Iterator] or [Map Entries],
as [Object [Set Iterator]] past the depth limit and { } when none are
left. Object.prototype.toString answers [object Set Iterator] and the
other tags.
AsyncLocalStorage changes, matching Node 24.22:
- setTimeout, setInterval, setImmediate, process.nextTick and
queueMicrotask callbacks run with the stores that were active when they
were scheduled. Before, getStore() returned undefined in them.
- An interval keeps that context on every tick, and a refreshed timer
keeps it too.
- enterWith inside one of these callbacks lasts until the callback
returns. An enterWith in top-level code ends when the main module's body
returns, and one inside a callback scheduled with no store active ends
when that callback returns, so the store does not reach callbacks that
were scheduled without it.
Programs that enter stores (store.run, exit, enterWith, or a
diagnostics_channel store binding) schedule through new *_als runtime
entry points, which wrap a callback scheduled with a store active in a
closure that installs the stores around the call. The emitter picks them
from the new asyncContext runtime feature; other programs keep the plain
entry points, so their scheduling is unchanged.
The limitations page notes that completion callbacks of callback-style
I/O APIs still run with no store.
5815-als-callback-context compares the stores each kind of callback sees
with Node, plain and with SCRIPTC_SAN=1.
map, filter, take, drop, flatMap, toArray, forEach, reduce, some, every
and find compiled only as one chain from arr.values() or arr.entries()
that ended in a consuming method. Every other form was refused, and in
JavaScript the refusal was thrown at run time.
IteratorObject now maps to the generator type. A consuming method over
a chain of stages fuses into one loop over the source: a generator
driven like for-of, a Map or Set walked live, or an array by index. A
stage on its own builds a helper object: a generator over its source
that supports next(), for...of, spreads, Array.from and yield*. A
for...of whose head is a chain of stages without flatMap runs the
stages inside the loop.
The source is closed where Node closes it: when take reaches its count,
when some, every or find has its answer, when a callback or the loop
body throws, when the loop exits early, and before the RangeError of an
invalid take or drop budget. It is not closed when it is exhausted or
its own next() throws. return() on a helper object before its first
next() closes the source through a new runtime hook,
scr_gen_set_unstarted_return.
When closing an iterator throws while another error propagates, the
first error reaches the caller, as in Node. This applies to for...of
over a generator too, where the error from the close used to replace
the body's error. The try statement takes a dropFinallyErrors flag,
whose cleanup handler calls the new scr_exc_keep_first.
throw() lowers only on values typed as generators. Iterator helpers and
the built-in iterators have no throw method, and a value typed as an
iterator may hold one.
Callbacks in JavaScript bind their unannotated parameters to the
element type, and flatMap callbacks may return generators. A generator
whose record yields share checked storage with its return channel has
each value converted to the declared element type as it is pulled.
Node runs runNextTicks after each timer and each immediate callback, which
reports the unhandled rejections the callback left before the next one runs.
The event loop now leaves the timer phase when a callback leaves the
rejection ledger non-empty, the check phase waits for that report, and the
check phase reports the ledger after each immediate's checkpoint.
When several timers expired in the same turn, a later timer ran before an
earlier timer's rejection was reported. On macOS the 2 ms and 3 ms timers in
5809-process-exit-and-rejections.cjs expire together, so its uncaught
'thrown string' was printed before the TypeError rejection.
5996-rejections-between-macrotasks.cjs makes five timers expire in the same
turn and checks the order across three immediates.
Resolve the scr_resume_fiber conflict with stackless async coroutines
(#1204): the region the fiber carries is made current before either
resume path and restored after both. scr_async_spawn_stackless now
captures the spawning region like the fiber spawns, so stackless bodies
(every async body in 5013 and 5023 after the merge) keep allocating in
their region after an await and keep it alive while they exist.
5024-lifetime-region-await-forms pins regions carried by stackless and
fiber (for await) bodies that spawn and await each other.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An async generator that fell off its end, with a return type other than
void, settled its last next() one microtask turn after Node, so for await
loops over it also ended a turn late. The appended implicit return now
marks the completion as settled, like a bare `return;`.
return() at a suspended yield ran the generator's finally blocks before
awaiting the return value. The request now awaits the value first and
resumes the body on the next turn, and the completion that follows settles
without a further turn, also when a finally block yields or awaits first.
An explicit `return x` reached during that unwind still takes its own turn
to await x.
Array.fromAsync accepts typed async generator sources: each next() result
is awaited and its value used as is, a throwing or rejecting mapping
function closes the generator, and a rejected next() rejects the result on
the turn Node rejects it.
Native async bodies whose only suspension points are their own awaits now
compile to LLVM switched coroutines, the lowering WASI async bodies and
native synchronous generators already use. A suspended call keeps a heap
frame of the values live across its awaits instead of a fiber with its own
stack. Bodies that reach other suspension points (async generator
consumption, dyn awaits, node:test subtests), module evaluation bodies,
dyn results, worker and library programs, and Windows keep their fibers.
The emitter selects bodies by emitting them as usual first: that text may
suspend only through await runtime calls and await-call entries, and may
call no function that parks. The coroutine form is then emitted and
checked again. Each await prepares its continuation in the runtime (a
pending promise's waiter list, or one READY-queue turn, continuing in place
exactly where a fiber's hop would) and extracts the settled result after
resuming. The eager prefix runs on the spawner's stack and moves to a fresh
stack near the stack guard, so deep async recursion keeps working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An async function called inside LifetimeRegion.run now keeps its region
after every await: its fiber records the region that was current at spawn,
and the event loop makes that region current while it resumes the fiber.
A region is not freed (or attempted) while a fiber that carries it exists,
so a region released while its async work is pending keeps taking that
work's objects and is freed once the work ends. Before, objects created
after an await went to the default region, which is never freed, and kept
the request's region allocated for good, so per-request regions in async
servers grew without bound.
Freed region chunks now go to a per-thread cache (at most 8 MiB resident,
the first 256 KiB of each kept cleared, fresh pages past it, no huge-page
chunks) instead of being unmapped, so a region per request or per batch no
longer maps and faults in new memory every time.
Adds the requests.ts region benchmark (concurrent async handlers), corpus
5023 and its region-stats checks, and a chunk-reuse runtime unit check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fs.cpSync, fs.cp and fs/promises cp compile with every option:
dereference, errorOnExist, filter, force, mode, preserveTimestamps,
recursive and verbatimSymlinks. They follow Node's two implementations.
cpSync follows cp-sync.js over the fs binding: copy errors are plain
Errors with a code, std::error_code failures throw an ErrnoException,
and links inside a copied directory point at their canonical targets.
fs.cp and fs/promises cp follow cp.js, whose copy errors are
SystemErrors with code, info, errno, syscall and path.
Options and paths validate in Node's order with Node's errors. filter
runs for the top-level paths and each entry in Node's order; an async
filter must return a promise that is already settled, and a pending one
throws SC2020. The callback and promise forms complete the copy before
the callback runs or the promise settles. Windows builds throw SC2020.
The copy lives in the new scr_fs_cp.c. Directory walks keep their frames
on the heap, so deep trees also copy on a fiber's stack. File contents
copy with sendfile on Linux, and COPYFILE_FICLONE uses FICLONE
where it exists. ScrError gains the SystemError context and the
ErrnoException key order, with matching fields in the compiler's Error
layout. The stored fs.cp callback uses the same implementation.
Corpus 5567-5571 cover the sync, callback and promise forms, errors,
symlinks and 1500-level trees.
A string path holding a null byte reached the system call, which saw the
path up to that byte: readFileSync("a\0b") read "a", writeFileSync wrote
it, and existsSync tested it. Node refuses such a path before any file
system access with an ERR_INVALID_ARG_VALUE TypeError naming the argument
("The argument 'path' must be a string, Uint8Array, or URL without null
bytes. Received '...'").
The fs runtime entry points now check their string paths first (each
with Node's argument name: path, prefix, oldPath/newPath, src/dest,
existingPath/newPath, target), so the sync forms throw, the promise forms
reject, and the callback form of rename throws before it queues the
operation. The Received text is util.inspect's quoted string, cut at 128
characters as Node cuts it; the JavaScript lane's validation ladders
share the same rendering, which now picks backticks for a string with
both quote kinds and escapes control characters as inspect does.
existsSync answers false.
Corpus fs-null-byte-paths.ts and fs-null-byte-paths-js.cjs cover the
sync, callback and promise forms in both lanes.
A native synchronous generator body whose yield channel is a number or a
boolean caches its ScrGen's OUT slot at entry and stores each yield into
it directly instead of calling scr_coro_yield_f64/bool; for-of, spread
and yield* drives read such values back inline instead of calling
scr_gen_take_out_f64/bool. The IR validator types every yield operand as
the channel, so OUT never holds a reference at a yield and the store
needs no release. The emitter mirrors the slot as %ScrGenHead and
%ScrGenSlot; scr_async.c pins both layouts and the tags with static
assertions. WASI keeps the call paths.
scr_gen_step, the compiler's own for-of/spread/yield* drive, now enters a
suspended or unstarted stackless generator directly: one function with
the hop inlined instead of resume -> switch_in -> coro_in calls. Its
emitted callers own the generator or lend it from a binding nothing can
rebind during the step, so that hop skips the generator retain/release
(the user-facing next/return/throw paths keep it). The IN slot is reset
only when it holds a value, the stackless hop skips the async-generator
state test (stackless bodies are always synchronous), and the resumer is
parked in the fiber so fewer values stay live across the body.
In lanes without exception alert state (no workers, no thread-instanced
libraries) scr_exc_cell_changed is empty, so runtime units now inline the
fiber switch's exception-cell swap as two pointer moves, mirroring the
existing inline pending test.