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>
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.
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>
A typed-array loop version runs a checked copy and one or two fast copies
of the loop, and each copy repeats every nested loop together with the
nested loop's own version. A loop whose guard proves only accesses outside
its nested loops, which run once per pass, now runs as written when a
nested loop accesses typed arrays; the nested loops still version their own
accesses. The loop versions as before when the guard proves the bounds of
an access inside a nested loop, or, in a worker program, the ownership of
an access inside a nested while loop, which never versions for ownership
alone.
Spectral-norm's paired row sums no longer copy their column loop for the
row loop's two stores. The worker program's au and atu functions are
102 KB of LLVM text instead of 236 KB each, and the program 519 KB instead
of 786 KB.
Corpus typed-array-nested-loop-versions covers row loops that store past
the end or at negative, fractional and NaN rows, views the nested loop
writes, and a nested while loop.
String.prototype.indexOf took its byte-search fast path on a proven-ASCII
receiver only when the start index was not positive. A positive start went
through the general path: ToIntegerOrInfinity, the surrogate-needle check,
and UTF-16 to byte conversions in both directions, although byte offsets
are UTF-16 indexes in an ASCII string. The fast path now covers every start
index: a positive start truncates and clamps to the length, where only the
empty needle is found, and the search runs from that byte offset.
Every unbounded recursion repeats some cycle of the call graph, so a
stack check in one function of each cycle bounds it. stackCheckedFunctions
now picks a feedback vertex set of the stack-check graph greedily (the
function with the most caller-callee pairs first) instead of checking
every function on a cycle, and adds a check where a run of unchecked cycle
functions would exceed 32 calls, so the stack used between two checks
stays of the same order as before.
Worker entry polls follow: a synchronous function on a cycle that may
throw needs no entry poll, since a descent that never returns ends at a
checked function of its cycle. NoThrow contracts keep the model in which
every function on a cycle checks, a superset that does not depend on the
choice.
`skip || (flag ? warnA() : warnB())`, with void calls in both arms, as
React's development warnings write it, failed with SC9001 "ternary must
not be void": the statement lowered the logical as a value, and a
conditional of void calls has none. A logical whose result is discarded
now lowers as `if (!skip) ...` (or `if (skip) ...` for `&&`) when its
right operand needs no value: its type maps to void, or it is a logical
or comma expression whose last operand needs none. This holds in
statements and in a for loop's initializer and update, and for a
generic callback's conditional whose type parameter maps to void.
Closes#2322
Co-authored-by: MathurAditya724 <57684218+MathurAditya724@users.noreply.github.com>
The runtime vendored zlib 1.3.1 and archived it at -Os, and host C-lane
builds linked the system libz, so deflate, raw deflate and gzip output
differed from Node's at every level. The vendored copy is now Node
v24.22.0's deps/zlib (Chromium's zlib, 1.3.2.1-motley-285e94b), built at
-O3 with the units and defines Node's zlib.gyp selects per target: the
SSSE3 Adler-32 and SSE2 inflate and slide-hash code on x86-64, the NEON
and ARMv8 CRC-32 code on arm64, and the portable code on wasm32. The SIMD
units check the CPU at run time and give the same output, so compressed
bytes match Node's on every target.
zlibInputs (runtime-pack-common/scripts/zlib-inputs.mjs for the runtime
packs, vendor-inputs.ts for Zig cross builds and the sanitized runtime)
holds the per-target table, and a unit test keeps the two identical.
Every C-lane driver now links the vendored objects instead of -lz.
Chromium's BSD license ships beside zlib's in the runtime packs.
Corpus 8405 compares compressed bytes with Node across levels,
strategies, memLevel, windowBits, dictionaries and flushing streams;
8406 covers chunked inflation with small input pieces and output chunks.
An integer clone reads a number array's element as an 8-byte double. A
number array proven to hold only int32s keeps 4-byte int32 slots
(SCR_ELEM_I32), so the clone read such an array at the wrong width: it
returned wrong values, and near the array's end it read past the slots.
The clone now tests the array's element kind: int32 slots load as 4-byte
int32s, double slots load as doubles and keep the exact-int32 test, and
any other storage bails to the ordinary body.
New corpus program: integer-clone-int32-slots.ts.
In JavaScript, `counts.get(key) ?? 0` on a Map the compiler types as
Map<string, number> is typed `any` by the checker, so `??` was lowered
on checked-dynamic values: the `number | undefined` read converted to a
dynamic value, which allocated a heap union for every read, and the test
ran on that value.
When the left of such a `??` is a union of one number, boolean or string
arm with undefined or null, and the default has the same type, the test
and the default now run on the typed path, where the read needs no heap
union, and only the result converts to a dynamic value. Other defaults
keep the dynamic path.
New corpus program: untyped-nullish-scalar-defaults.mjs.
The native bootstrap's package checks copy the seed's distribution into a
package directory outside the workspace and, since the CLI packages depend
on their LLVM helper and runtime pack, drop the bundled copies. Nothing in
that directory resolved @scriptc/llvm-linux-x64-gnu, so
verify-native-cli.mjs failed with 'Cannot find module'. The test now links
both packages into the directory's node_modules, as pnpm links a workspace
package's dependencies.