Main moved the per-program report lines into zero-cost-compare.mjs and
added sharding and a baseline cache. The runtime-only and
nondeterministic verdicts now print through programLines, count as
compared in merged shard reports, and the runtime pack diff is skipped
when only recording a baseline cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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).
AbortController and AbortSignal lowered to fetch.abort* entry points, so
a program that only used them linked the fetch unit with its socket,
HTTP, TLS and zlib dependencies. They now lower to the abort.* entry
points on every target, a new abortSignal feature links scr_abort.c (the
fetch unit's signal implementation built without its transport, which
WASI already ships) and the EventTarget unit, and programs that also
fetch keep the fetch unit's copy.
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.
A unit's record holds its IR in the compact encoding, the entries it added
to the helper, lifted-function and other appendable lowerer tables, and the
reachability, generic demand, statement and flag events it made. A unit has
no record when it grew a table no record carries, changed a field, replaced
an entry it did not add, or emitted a diagnostic. Every lowerer collection
and scalar field is classified, and a test fails on one that is not.
SCRIPTC_LOWER_JOURNAL=serial records every unit and replaces its results
with the ones its record carries. scripts/recorded-lowering.mjs
(pnpm test:recorded-lowering) compares corpus programs' LLVM text with and
without the mode. SCRIPTC_LOWER_JOURNAL_DUMP=<dir> also turns the mode on
and appends each unit's record to <dir>/records.jsonl. Without either,
lowering is unchanged.
The NSS check scanned the linked executable for the `libnss_` literal, so
a program with that text in a string of its own was refused. A --static
glibc link now writes a link map, and the executable is refused when the
map places code from libc.a's nss_module.o or nsswitch.o in it (lld's
kept input sections, or GNU ld's memory map with a nonzero size).
- `scriptc build --static` (and the `staticLink` compile option) links glibc
executables as static PIE on the build host's own glibc target; musl
executables are already static and accept the flag.
- An executable that keeps glibc's NSS module loader (host, user and group
name lookups) is refused with SC3002, since a static glibc executable
loads those modules from the run host.
- Other targets, cross-compiled glibc targets, --sanitize and non-executable
output are refused; static and dynamic builds use separate cache entries.
When --split codegen keeps the module whole, the extra outputs are new
empty modules, and their source file name defaulted to the module
identifier: the path of the private copy of the LLVM input, which has a
random name. That name reached each part's STT_FILE symbol, so two
release builds of one program differed in .strtab. The empty parts now
take the module's source file name, as the first part has it.
The LLVM backend can emit scr_census_write, which a census program
defines and the runtime calls through a weak reference, so the header
declares it as it does scr_detached_exit.
Every native build started the LLVM helper once for `version` before
emitting, and hashed every file the frontend read, including the supplied
declaration files that do not change between builds.
- verifyNativeHelper takes an optional record of helpers that already
answered `version` as expected. The native compiler keeps it in its cache
(family "helper"), keyed by the helper's and the compiler's file identity,
the compiler version and the target. The probe runs again when the helper
file changes. The package name and version are still checked every time.
- FrontendInputTracker takes the snapshot of the cache entry a build
replaces. A file whose settled identity has not changed since that
snapshot keeps the digest recorded there, which is the trust
frontendInputsStillMatch already gives an unchanged identity.
Builds produce the same executables.
An arena-aware constructor of a record shape or class with reference
fields tests whether the current arena is closed on every allocation, and
appends a dispose entry when it is not. When every root and call region in
the program opens a closed arena, the current arena is closed or absent,
so planArenas sets ArenaPlan.allClosed and those constructors leave out
the test and the dispose list.
Libraries never build the census (heapCensusOf), but the runtime's weak
scr_census_write reference made library archives list an external
symbol outside their declared set. Declare and call it only in programs.
`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.
- The heap census also counts instances a class value constructs: the
construct thunk that `newValue` calls counts them after its allocation,
as `new` does at its own.
- A build with SCRIPTC_HEAP_CENSUS set is its own executable cache entry
(`heapCensus` in the early cache's key and route key), and one with
SCRIPTC_REPR_REPORT set does not read the early cache, since a hit
emits no module and so writes no report. The CLI's startup route defers
both to the full compiler.
- Test: a warm cache still builds the census executable, counting
`new` and class-value constructions, and still writes the report.
fork() copies only the calling thread, so a child forked while the
allocator's prefault helper held scr_sa_pf_lock waited on that lock forever
at its first fault-in request, and a child that released the range the helper
was faulting in waited forever for the helper to finish it. Native builds'
emit workers hung this way, with the parent waiting in waitpid.
The helper's first start now registers pthread_atfork handlers. The prepare
handler takes the lock across the fork and the parent handler releases it.
The child handler resets the ring, the busy range, the waiter state and the
heap polling state, reinitializes the lock and both conditions, and marks the
helper not started, so the child's first request starts a helper of its own.
check_fork_while_prefaulting in scr_alloc.test.c forks while the helper
faults in 64 MiB, over 64 rounds; each child releases that range, faults in a
range of its own and must exit within five seconds. ThreadSanitizer builds
skip it.
`[...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.