- A --libnode=beside build whose executable comes from the executable
cache places the pack next to it again (compile, index.ts): the pack
is an output of the build, and a rebuild after the outputs were
deleted restored the executable without it.
- The cache key names the libnode pack by the option, the pack
directory and a digest of its manifest.json (libnodeCacheKey), so a
pack upgraded in place gives cached executables a new key instead of
their old recorded Node version.
- nodeIsland.callSync marks the program's event loop live, as
nodeIsland.call does: the called function may leave Node with ref'd
timers or I/O, which the loop drives before the program exits.
- scr_runtime.h includes scr_node_island.h, so the LLVM backend's
declarations of the Node island entry points have prototypes there.
- Corpus costs for 6800-6803, from CI's differential and
llvm-differential timings of the programs.
Calls into Node now run on one dedicated 64 MB stack with a guard page,
reached by a context switch, and the bridge sets V8's stack limit on it
(bridge ABI 2). Before, a call from an async fiber (512 KB stacks without a
guard page) failed to bootstrap Node, which the dev-mode LLVM differential
showed. The runtime's stack guard follows the switch, and the switch
carries ASan's fiber annotations.
The Node island tests load Node from a libnode pack. Until the NI-13 packs
are published, .github/actions/libnode-dev-pack assembles one for the
Linux x64 test lanes (plain and sanitized): a pinned, checksum-verified
prebuilt libnode of the repository's Node release (24.22.0) and Node's
official headers (packages/libnode-common/dev-pack.json), with the bridge
built against them by build-pack.mjs. The pack is cached by its inputs and
the bridge source, so only a cache miss downloads and builds; the normal
build path never does.
tests/harness/libnode.ts makes a missing pack a failure in those lanes:
island tests skip only off Linux x64 CI without a configured pack. The
bridge smoke test (packages/libnode-common/test/bridge-smoke.c) loads the
pack the way scr_node_island.c does and makes synchronous, throwing and
asynchronous calls through the bridge with the runtime's bootstrap.
IR, LLVM and object outputs of programs that call into Node no longer need
a pack at build time; only executables record one.
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>
The first Object.freeze, Object.seal or Object.preventExtensions of a typed
function creates its property table from the function's dynamic box. The
box had no name, so later `f.name` reads answered "". The box now carries
the function's JS name, as a closure flowing into any checked-dynamic slot
does.
The runtime's own callers (published values, os and node constants, the
private-range block list, Wasm exports) freeze data they built, and now
call scr_dyn_freeze_data, the freeze for values that are not functions.
Object.isFrozen, Object.isSealed and Object.isExtensible read a
function's existing table and answer false, false and true when it has
none. Fully typed programs therefore link no dynamic-tier object code
for these calls again.
Revert 3a27a92cb ("Forward at most four emit arguments through
EventEmitter method values") and 0cf62ebec ("Answer EventEmitter methods
read as values off compiled emitters"). Under the scope-B plan,
EventEmitter methods read as values, `EventEmitter.prototype.on` in
JavaScript and constructor-function emitters belong to Node islands, not
the native compiler. The typed/untyped listener sharing and the
catchable throw in place of the trap stay.
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>
The runtime's ToIntegerOrInfinity for string index arguments (slice,
substring, charAt, indexOf, startsWith, padStart, repeat and the others
in scr_string.c) converts through int64 instead of calling trunc, which
is a libm call on baseline x86-64. Each slice made two such calls.
The frontend's syntax trees and program state refer to each other, so
reference counting frees almost none of them when a build disposes the
frontend. Whether a cycle pass reclaimed them before the build ended
depended on where the collector's schedule happened to be. Passes during
the frontend found its data live and widened the growth fractions, so the
next pass waited until the heap had grown well past its size at disposal,
and the dead frontend and the emitter's garbage stayed until the build
ended.
The native compiler now runs scr_collect_cycles_phase right after it
disposes the frontend, through a host function. The runtime entry point
sweeps, then restarts the growth fractions and the death rule's backoff
from the heap the sweep leaves, as after a productive pass. Under Node
the hook is unset.
When a script thread parks its allocator arena, each class slice still
holds the pages past its bump pointer that were faulted in but never
carved: the rest of the huge page the bump pointer is in, and the pages
the pre-fault helper faulted in ahead of it. The same applies to region
chunks when the thread exits. Until another thread claims the arena,
nothing carves there, so a program that continues on threads that never
claim it (parse threads ahead of a check on the main thread) kept them.
- scr_sa_park_arena returns those pages with MADV_DONTNEED. The next
thread to claim the arena faults them in again as it carves, and they
read as zero, as fresh pages do.
- scr_rgn_thread_exit returns each chunk's pages past its bump on worker
threads; nothing carves in an exited thread's regions.
- scr_mem_release_ahead drops pre-fault requests that overlap the range
and waits for one in progress before returning it, so the helper does
not fault the pages in again.
- scr_alloc.test.c checks that a worker that shares and carves past the
huge-page switch leaves no resident pages past its frontier, keeps its
blocks, and that the next thread carves on from the frontier, zeroed.
A WebAssembly module the build finds but does not translate (one that uses
a proposal outside the translated feature set, an invalid one, or one the
translator rejects) now rejects with a CompileError that says the module
was found and names the reason, for example "it uses simd, outside the
translated WebAssembly feature set". Bytes the build did not see keep the
existing message.
The build emits the hash, length and reason of each such module in a
second table, scr_wasm_untranslated, next to scr_wasm_registry; the runtime
reads it when a lookup misses. WebAssembly.validate still answers false for
these modules, and the error stays a catchable CompileError.
Fixes#1617
Co-authored-by: jlucaso1 <55464917+jlucaso1@users.noreply.github.com>
A plain function's own properties (name, length, prototype, and the ones a
program assigns, as in `hash.outputLen = 32`) live in its property table.
Object.freeze, Object.seal and Object.preventExtensions of a function now
act on that table and answer the function, and Object.isFrozen,
Object.isSealed and Object.isExtensible answer from it, as in Node:
- Object.freeze of a function was refused as a possibly-aliased value,
and Object.isFrozen of one threw SC2020;
- Object.isExtensible answered false and Object.isSealed true for every
function.
Adding a property to a frozen or non-extensible function throws Node's
TypeError; assigning a read-only one throws the runtime's existing
read-only TypeError. Class constructors keep their fences. This lets
@noble/hashes' createHasher return its frozen hasher, so the sha256
program in the issue prints the digest Node prints.
Fixes#1437
Co-authored-by: saboursepideh <25549001+saboursepideh@users.noreply.github.com>
A comparator that chains key differences with `||`, such as
`(b.score ?? 0) - (a.score ?? 0) || a.id - b.id`, ran through the
comparator sort: TimSort called the closure once per comparison. Each
term orders one number field, ascending or descending, and a term that is
0 or NaN falls through to the next, so the chain orders elements by their
keys with the first deciding. With no key NaN or -0 that order is
consistent, and a stable sort by the keys gives the comparator sort's
permutation.
The recognizer now accepts such chains of up to eight terms over number
fields of records and class instances, and a key may read an optional
number field with a number default (`x.k ?? 0`), also as a single key. The
runtime reads and checks every key of every element before it moves a
slot, then runs stable keyed passes from the last key to the first. A NaN
or -0 key, or a chain with any other term, keeps the comparator sort.
`a instanceof Uint8Array || (... a.BYTES_PER_ELEMENT === 1)`, the
@noble/hashes isBytes guard, narrows `a` to never on the right side when
the guard is specialized for a Uint8Array argument, so the read missed
the typed-array member lowering; it now answers the element size from
the receiver's storage. The checked-dynamic read of BYTES_PER_ELEMENT on
a typed array or Buffer (an untyped argument) answered undefined; it now
answers the element size, as each kind's prototype does in Node.
Refs #1437
Co-authored-by: saboursepideh <25549001+saboursepideh@users.noreply.github.com>
Each class slice a thread opens took one madvise(MADV_DODUMP) call,
which takes the mmap lock for writing, and the threads of a worker
executable open hundreds of slices as they start. The slices are now
made dumpable only when the kernel would write a core of the process:
when kernel.core_pattern starts with '|' or '@', or when RLIMIT_CORE is
above 0. The reservation stays MADV_DONTDUMP in every case.
forEach, map, filter, some, every, find, findIndex, findLast,
findLastIndex, reduce and reduceRight compile on every typed-array kind
through one shared helper per method shape. The receiver, the callback and
reduce's initial value evaluate in order before the helper runs; the
helper reads the length once and each element when it visits it, so the
callback's writes show in later elements. Callbacks receive the element,
its index and the typed array (reduce's receive the accumulator first).
map and filter return a new typed array of the receiver's kind, map
storing each result with that kind's element conversion, and a Buffer's
map and filter return a Buffer. reduce without an initial value throws
Node's TypeError on an empty typed array. A thisArg keeps the fence, as
for arrays.
Fixes#1620
Co-authored-by: ejc3 <1694850+ejc3@users.noreply.github.com>
A typed number-key Map or Set that is boxed and cast back to
Map<unknown, unknown> or Set<unknown> keeps its own storage, with
16-byte entries. Inline iteration over a checked collection used the
24-byte step, so it tested the wrong words for liveness and read past
the entry array: a Set skipped entries and a Map aborted with "map
iteration index out of range".
The iteration step for a collection with checked keys now reads the
map's key kind and takes the number-key step for SCR_MAP_KEY_F64 and the
24-byte step otherwise. Keys and values are at bytes 0 and 8 in both
layouts, so the slot reads are unchanged. Typed collections keep their
static step, and their IR is unchanged. scr_map.h asserts the key kind's
offset and SCR_MAP_KEY_F64's value.
set-checked-storage.ts iterates a number-key Set and a hashed number-key
Map through their checked views, with deleted entries.
- The scan for properties JavaScript adds to children now also records `Reflect.defineProperty`, `Object.defineProperty` and `Object.defineProperties` on a child, and element writes (`child["kill"] = ...`). A key that is not a literal sends every member through the child's box. A typed call to a method replaced through `Reflect.defineProperty` then calls the replacement instead of the native method.
- The "Cannot set property x of #<ChildProcess> which has only a getter" message is built at its exact length. Before, a name that overflowed the 160-byte buffer passed the untruncated length to the throw, which read past the buffer.
- Corpus 6611 adds a module that replaces kill through Reflect.defineProperty and writes to a getter-only property with a 300-character name; 6610 and 6611 get their cost files.
- Date toString and toTimeString now decide the zone name's daylight flag at the instant itself, as V8's LocalTimezone does. The offset cache's segments hold offsets only, as V8's do. Before, a merged segment kept the DST flag of the read that created it, so a zone that left daylight time without changing its offset could print the summer name after the change: Africa/Algiers on 21 October 1977, at +0100 on both sides.
- Corpus date-zone-intervals.ts reads Algiers across that day in three orders; the names match Node.
Node creates monitorEventLoopDelay's histogram with a lowest discernible
value of 1000 ns, so an empty histogram's percentile() returns 511, the
highest value its first bucket holds. The runtime returned 0.
6397-event-loop-metrics.ts now skips a sampling window that has no
recorded delta. A delta needs two samples after reset(), and a loop that
wakes late runs only one sample in a window, so on a loaded machine the
program printed an empty window under Node and compiled alike. The
program also prints percentile(50) after each reset.
- A getter or setter that JavaScript defines on a child process now runs with the child as `this`, instead of the hidden object that holds the child's added properties, for reads, writes and member calls. A write to a getter-only accessor throws "Cannot set property x of #<ChildProcess> which has only a getter".
- The scan for properties JavaScript writes onto children now covers every JavaScript file of a program that loads child_process, so a module that patches a child it received counts even when it never names child_process.
- The scan's cache and the program's "writes onto children" flag live on the Lowerer instead of a WeakMap and a WeakSet.
- Corpus 6611 covers a module that adds a replaced method, an accessor pair and a getter-only accessor to a child it did not spawn.
- The zone-offset cache now follows Node's V8 date cache (DateCache::LocalOffsetInMs): 32 segments of instants with one offset each, the 19-day lookahead, merges of segments with the same offset, a binary search of up to five probes for a change point, and least-recently-used replacement. Local Date getters then answer as Node's do in every order of reads, including the order-dependent answers around Tucumán's two offset changes in June 2004. The previous single-interval cache differed from Node in about half of the seeded orders tried.
- Scattered reads hit a cached segment more often: a getter benchmark over consecutive and scattered instants runs in 169 ms instead of 314 ms (America/New_York; Node 567 ms).
- Corpus date-zone-intervals.ts adds seeded orders of reads around the Tucumán changes and across three years in four zones.