Under ASan, self-hosting-llvm-emitter, three-library, npm-static and npm
each take 20 to 40 minutes as one Vitest file, so a sharded run cannot
finish before them.
- self-hosting-llvm-emitter splits into its emitter-cases-and-programs
and fixed-point halves; a shard builds the seed once for the halves it
owns. three-library skips the optimization its shard does not own.
Both put their tests in a describe block, which skipUnownedTest
partitions.
- CI runs both in every shard's case-sharded side queue
(scripts/ci-test-plan.mjs); pnpm test:sandbox case-shards them with npm
and npm-static.
- npm.test.ts keeps a skipped placeholder in a shard that owns no
program, so an empty shard does not fail the file.
The sanitized native-driver bootstrap ran its instrumented compiler with
the default 8 MiB stack. Its frontend phase overflows it (SC3004) on main
as well, outside CI's ulimit for the emitter phase. The test now raises
the soft stack limit to the hard one for every sanitized compiler process
it starts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each thread keeps up to 8 MB of arena chunks and up to 64 dropped arenas
for reuse (scr_arena.c). Nothing freed them when a worker thread finished,
so every worker that used a group arena left its pool behind: 40 workers
run one after another, each dropping a 12 MB arena, kept 328 MB resident.
scr_arena_thread_exit frees the calling thread's pooled arenas and chunks.
The worker thread calls it after its context cleanup, before it posts the
exit event. The new harness test runs 24 such workers in sequence and
checks that the resident set grows by less than 32 MiB over the last 20
(160 MiB before this change, 1 MiB after).
In JavaScript sources, ReadableStream, ReadableStreamDefaultReader,
ReadableStreamDefaultController, CountQueuingStrategy and
ByteLengthQueuingStrategy (globals and node:stream/web exports) are class
values over a new runtime unit, scr_webstreams.c, which follows Node 24's
webstreams step by step: underlying sources, queuing strategies, default
readers, tee, ReadableStream.from and async iteration settle their promises
in Node's order. scr_dyn_handle_class.c gives a dyn handle tag a
constructor value with a prototype, brand checks and Node's constructor
errors. Stream state carries cycle headers, so the collector reclaims
streams whose callbacks close over their controllers.
Promise steps run as native promise reactions, a variant of scr_async.c
that only programs with web streams link; every other program links the
default object. TypeScript sources keep their static web stream lowerings.
Byte streams, BYOB readers, pipeTo and pipeThrough throw SC2020.
node24-web-stream-readers and node24-web-stream-readable-lifecycle now
match Node.
`toUpperCase` and `toLowerCase` now record that a result of their ASCII
path is ASCII. `padStart` and `padEnd` take the UTF-16 lengths of the
receiver and the fill the way `length` does: a proven-ASCII or short
string is counted directly instead of claiming a string-index entry. A
one-unit fill repeats without a division, a one-byte fill with memset,
and a result padded from an ASCII receiver and fill is recorded as ASCII.
`word.slice(0, 3).toUpperCase().padStart(4, "-")` previously built an
index entry for the three-byte receiver on every call.
The constructor copies the array it is passed into the instance, and
other reads of the field give a new array over the elements, while under
Node.js the field holds that array itself. The contract check now proves
the two agree, and reports SC7009 where it cannot:
- The arrays fields hold are never written. A whole-program graph of the
places arrays are held (locals, parameters, results, class and record
fields, and the elements of arrays, Maps and Sets by where those are
held) follows every array a field read may give, and reports a write to
one, or one that reaches a dynamic value, a call through a function
value or a library call other than printing and JSON.stringify without
a replacer. Functions that return their argument give each caller its
own argument back. The report names the field read.
- The array passed to the constructor is one nothing else keeps: a new
array (a literal, a copy, or the result of a function that returns only
such arrays or its argument), a local array at its last use (after calls
that only read it or write it in place, or in a loop that then assigns
the local a new array), a parameter whose every caller passes such an
array, an array a field already holds, or a const array that is never
written, which is then followed like a field's.
The review fixes from before stay: `new` evaluates its arguments into
locals unless they are all locals and constants, so that the allocation
reads the length after every argument has run; the constructor uses the
array parameter only to store it; and the dynamic view of an instance
reads the field as an array of its elements.
Since the transfer drops its socket at the response's 'end', resuming a
read no longer reached the decoder: sf_response_resume_if_ready returned
before sf_inflate_resume once the transfer held no paused socket. A
compressed response whose whole body arrived before it was read then kept
its remaining decoded output buffered, and later reads stayed pending. The
buffered input and decoder output now drain on demand after 'end' as well
as while the socket is paused.
`[...text]` lowered to an array literal that spread the array of text's
code points: the literal's array copied the code point strings, retaining
each one, and the code point array then released them. An array literal
whose only element is a spread of a string is now the code points array
itself, as `Array.from(text)` already was. Both are fresh arrays.
charCodeAt on a non-ASCII string mapped the UTF-16 index to a byte offset
through a string index entry, which the first read claimed and the
string's release purged. For a string of one character, such as each
character of a for-of loop that codePointAt(0) classifies, the entry cost
more than the read.
A non-ASCII receiver of up to 16 bytes is now decoded from its start on
each read, as scr_str_utf16_len already counts short receivers. Longer
receivers keep the index entry and its cursor.
An index `base + c` converted `base` and took a fractional base as an
invalid index. A fraction smaller than the sum's precision is lost in the
sum, as in `1e-20 + 1`, which is the index 1. A base that is not an
integer now goes to the exact check of the sum itself.
The conversion also read the base local again when the access was
emitted. A write evaluates its right-hand side between the index and the
store, so `values[at + 1] = ((at = 5), 31)` wrote at the new base. The
sum now records the base value it was computed from, and the index
converts that value.
The corpus program covers both cases, and the emission test checks that
a write converts the base its sum read.
A typed array read or write at `base + c`, `c + base` or `base - c`,
where `base` is a number local and `c` an integer literal of magnitude
below 2^31, now converts `base` to an integer and adds `c` as an
integer, instead of converting the double sum. Accesses at several
offsets from one base, such as `x[at]` through `x[at + 7]` with a number
parameter `at`, then share one conversion and integer test. Below a
length of at most 2^52 the sum is an integer index exactly when `base`
is an integer, so the accesses read and write the same elements as
before.
The lowering of the complete frontend holds about 3 GB of heap. Node sizes
its default heap limit from the host's memory, and on hosts with less
memory the limit is about 2 GB, where the child runs out of heap.
A member read through a non-null asserted element read evaluates the
element's presence and then reads it, so a missing element throws Node's
TypeError. An index other than a local or a field read was held in a
hidden local first, which turned the receiver into a sequence: the
element was then read with a reference (retain and release) instead of
borrowed, and the fallback `genes[genes.length - 1]!.sc` of a search
made fasta's lookup loop 12% slower.
An index computed only from locals, field reads, number literals and the
lengths of arrays read through them (`xs.length - 1`, `i + 1`, `-i`)
has no effect and yields the same value when evaluated again with
nothing in between, so it is evaluated for the presence test and again
for the read, and the element stays a borrowed read. Other indexes keep
their hidden local.
A while loop whose remainder reads number locals that enter it without
range facts, such as copies of module globals, parameters or fields
(`const base = column;` before `(count + base - breaks) % 60`), now runs
as two copies. On entry, a test checks that each such local holds an
integer in [-2^48, 2^48] other than -0. The fast copy uses the facts the
integer range analysis proves when the function is analyzed again with
those entry ranges assumed: the loop's counters become bounded, and the
sums and remainders run as i64 arithmetic. When the test fails, the loop
runs as it did before.
- ir/loop-entry-ranges.ts picks innermost while loops with a remainder
that the assumed ranges prove and the function's own analysis does
not. The test covers only the locals the remainder depends on, directly,
through the loop's writes or through its condition.
- ir/integer-ranges.ts takes the assumed entry ranges per loop, and keeps
no fact about the assumed locals or the loop's writes after it.
- backend/llvm/loop-entry-versions.ts emits the test and both copies.
The test is marked likely. A function called only from the loop gets
`alwaysinline` on the fast copy's call, because the second copy would
otherwise cost it the inlining of a single call site.
reverse-complement (25M bases): 2.08 s -> 1.19 s; Node 3.97 s.
The native fetch dialed a new connection for every request and closed it
after the response, so a loop of requests to one origin paid a TCP
connect, and for https a TLS handshake, per request. Node's fetch keeps
connections alive through undici's dispatcher. A direct request now goes
through a keep-alive agent private to fetch (scr_http_fetch_request): an
idle connection for the same host, port and bare hostname serves the next
request, there is no limit on connections per origin, and an idle
connection closes after 4 s or the server's Keep-Alive hint less one
second, without keeping the process alive. Proxied requests still dial
their own connection.
When a response ends, the transfer resumes its socket if it had paused it
and stops pausing and resuming it, since the agent may hand the socket to
the next request. scr_http_request_agent_ex and http.Agent creation share
their agent logic with the new entry point.
Object.freeze of a fresh record or array literal now sets the native
frozen bit instead of compiling to the literal, so an untyped reference to
the value sees it frozen: Object.isFrozen answers true and strict-mode
writes throw Node's TypeErrors.
Untyped property writes in sloppy-mode code (IrModule.sloppyDynWrites,
decided per write by isStrictCode) call scr_dyn_key_set_sloppy and its
computed form, which drop exactly the TypeError a refused assignment raises
(a read-only or frozen property, a non-extensible object, a frozen array
element) while a setter's exception still propagates. Computed-write
dispatch helpers are shared only by writes of the same mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6809-node-island-stdio armed a 5 ms island timer and a 10 ms native timer
next to immediates. On a loaded host the main script can run past 5 ms
after arming, so Node runs the island timer before the immediates and the
output order depends on load. The timers are now 50 ms and 100 ms, well
after the script and the immediates finish; the expected order is
unchanged.
CI's pinned LLVM ships ld.lld, and the compiler links Linux executables
with it whenever clang finds it. The Sandbox image and the managed-image
bootstrap installed clang-22 without lld-22, so Sandbox builds linked
with GNU ld: static hello came out at 99 KB instead of 65 KB, and
runtime-tree-shaking failed only in Sandboxes. Install lld-22 in both
and check that clang finds it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inside a class constructor, Error.captureStackTrace(this, this.constructor) now reads this.constructor before the capture when the class or a base class declares a constructor getter, as V8 evaluates the argument: the getter runs and its exception propagates.
An instance of a compiled class reaches untyped code as a capsule, which
a property site sent to its helper on every read: the helper normalizes
the capsule, tests each class's key in turn, and loads the field.
A read site now emits the helper's plain field branches inline. A
capsule whose key pointer is one of those classes' keys loads the field
from its instance and shares the inline hits' conversion. A branch is
emitted only when its field holds a plain dyn cell and every subclass
the helper's normalization can rebox the key to reads the same field
from the same slot. Getters, bag properties, unset fields and other
capsules keep the helper.
The static hello binary grew past the 96 KiB ceiling of the runtime
tree-shaking test on Linux hosts that link with GNU ld (99,040 B). CI does
not see it: its pinned LLVM ships ld.lld, which the compiler prefers, and
hello links to about 65 KB there. The Sandbox gate installs Ubuntu's
clang-22 without lld, so it links with GNU ld.
dd418b067 crossed the ceiling (98,032 -> 98,960 B). Its scr_glibc_math.c
defines floor, fmod, frexp and other glibc names as hidden functions so a
program can start without libm. With GNU ld that cannot work: ld decides
--as-needed before section GC, so the runtime's unreachable code keeps
libm needed, and it keeps every regular definition of a name a needed
shared library defines, hidden or not. Every executable carried the whole
unit (about 2.5 KB of code and unwind data) and still loaded libm. Links
without lld now leave the unit out; its functions answer what glibc's do.
192ea3247 installed scr_gen_tag_reader from a load-time constructor in
scr_async.c. A constructor is a link root on every format, so each
executable kept scr_gen_tag and, with GNU ld, all of scr_async.c's string
literals (about 1.1 KB). The generator runtime now installs the reader
when it creates a generator, the only time a reader can be needed.
Hello is now 93,928 B with GNU ld and 65,472 B with lld.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An Array subclass's type maps to its element array type, so a field such
as TreeNode.kids holds a Kids instance in a slot typed as an array of
TreeNode. The collector visits such a slot as an array, which has a
collector header when its element ops trace, but the class only got a
header when its own class unit was on a cycle, and the collector read a
header the instance did not have. An Array subclass now carries a header
whenever an array of its element type does, and its collector descriptor
visits its elements instead of naming no children.
An element read of an array of Array subclass instances is a union with
undefined, and 'instanceof' against the class answered false for it. A
union now tests its array arms' class at run time.
The generator listed a field as a scalar when its walk read nothing, so
a nested record holding only numbers and booleans got a ValuesNotRead
check on the record itself, which fails because the record is an object,
even after regenerating. A field is now listed as a scalar only when its
type holds no string and no object; such a record is checked through its
own fields, so a field added to it or a scalar in it that starts to hold a
string still fails type checking with its name.
The generator exports generateIrFields(irText) for the new test, which
generates the readers for an ir.ts with such a record and type-checks
them. The generated file for the current ir.ts is unchanged.
- forin-keys.test.ts matches the record-ops table with its eighth field, the
store hook, which is null for a record no untyped code writes.
- bench/record-untyped-writes writes typed records through `unknown` and
`Object.assign` views, so it links dynamic-tier symbols; DYNAMIC_WORKLOADS
lists it.
- The sanitized self-hosting emission test gets a 600 s timeout. It takes
about half of the default 300 s on an idle 16-core host, with or without
these changes, and exceeded the default on a loaded CI shard.
The sanitized run of "native LLVM layout and metadata emission (llvm)"
takes 170 to 300 seconds in its CI shard beside the shard's other tests,
against the suite's 300-second test timeout. On a quiet 8-vCPU machine it
takes 130 to 160 seconds.
A recorded baseline entry now holds a hash of its program's inputs: the
entry file and its relative imports (paths and contents) and the
optimization level. --baseline-cache uses an entry only when the program
still hashes the same, so a program or an import that changed since the
record builds its baseline, instead of a recorded build error or artifact
hash of the old sources settling the comparison as both-fail or identical.
Records take schema 2; older records are ignored.
The full gate takes 30 to 75 minutes on one Sandbox, which made it the
longest step of a validation round. It can now be split and merged:
- --shard=i/n splits the bench and corpus programs (the self-hosted
compiler runs in shard 1, or alone with --set=self), and
scripts/zero-cost-merge.mjs merges the shards' --json reports into one
verdict, recomputing the compile-time intervals over every shard.
- --record-baseline-cache records the baseline's artifact hashes once;
--baseline-cache skips the baseline build of every program whose
candidate artifacts hash the same. The record is tied to the built
baseline checkout, toolchain and paths.
- The candidate-source self build overlaps the comparison builds on
hosts with room for it.
pnpm test:sandbox can reuse a prepared snapshot's LLVM helper and runtime
pack (SCRIPTC_SANDBOX_NATIVE_REFERENCE) when their inputs are identical,
and retries rate-limited Sandbox creation with backoff.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>