A record view's slot lookup answers NULL for a field a partial view lacks
(record-views.ts), but only a program that asserts a record literal to a
wider type ever asks for such a slot. Testing for the sentinel inside the
default scr_record_view_slot put a branch on a hot accessor of every
program that links a view: the zero-cost gate found fifteen typed
programs whose executables changed with byte-identical IR and LLVM.
Build the unit twice instead, as the fs.watchFile work does for its unit.
The default object is again byte-for-byte the one it was, and the
SCR_RECORD_VIEW_PARTIAL build — the one whose slot answers NULL — is
selected by the program's link features. `recordViewPartial` joins the
runtime features, set by a `partialView` dynCheck and carried through the
link features, the executable options and the early (cache-key) features,
so a cached executable can never link the other build. The runtime pack
gains the eight builds that pair the flag with the unit's existing
dynTier, webStreams and nodeIsland combinations; every program without a
partial assertion keeps the default object.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scr_js_wrapper.c constructs `new String/Number/Boolean` wrappers and
ToObject of a primitive as ordinary dictionary-mode objects whose
prototype is the unit's String, Number or Boolean prototype, with the
[[PrimitiveValue]] slot on the object's source identity. The unit also
answers instanceof, Object.prototype.toString, the util.types brands,
the String method receiver, and the guard of the graph-walking consumers
that cannot read the slot yet. A new `primitiveWrapper` runtime feature
(any `wrapper.` libCall) links it, so a program that never creates a
wrapper links no byte of the unit and the zero-cost gate keeps fully
typed programs clean. The `wrapper.*` signatures, symbols and dispatch
case are registered here; no frontend emits them yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The typed write slot, the drain probes and the run-time-typed chunk write all
live in scr_events.c, a unit every program links. Unconditionally they changed
the default object, so executables of programs that never touch the write slot
changed byte for byte (three corpus programs differed runtime-only). Typed
programs must stay byte-identical.
Keep the default object exactly as it was and put the new code behind
SCR_STDIO_WRITE_SLOT, the way SCR_STREAM_WRITE_ERRORS gates the stream unit:
the shared write body, the run-time-typed chunk entry point and Node's
anonymous write function exist only in that build, and scr_stdio_method keeps
its own body everywhere else.
The lowering sites that reach the slot (the write method as a value, its
replacement, a patched program's typed writes) now emit a distinct marker lib
call, process.stdioSlot, which is the same scr_process_stdio at run time. The
feature scan turns it, together with process.stdioWriteDyn, into the
stdioWriteSlot runtime feature, which selects the matching runtime pack variant
and defines SCR_STDIO_WRITE_SLOT for the executable. The pack matrix enumerates
the combinations with stdioExtended and nodeIsland explicitly, so each existing
variant keeps its defines and its object.
Both new lib fns join USES_TIMERS_LIB_FNS: the write callbacks they queue run
from a nextTick, and a program whose only stdio use is the write slot would
otherwise drop the callback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`writable.write(chunk, cb)` with `cb` passed as a function VALUE lost the
completion's error: `lowerWriteCb` checked only the callback's syntax and
lowered every other callback through `lowerExprExpecting(cbArg,
funcOf([], VOID))`, which adapts a one-parameter function down to zero
parameters. The native binary therefore printed `undefined` where Node
prints `null`, or the write's own error (`ERR_STREAM_WRITE_AFTER_END` /
`ERR_STREAM_DESTROYED` for late and destroyed writes).
Lower such callbacks through a new `writable.writeEcb` libCall
`(recv, mode, chunk, encoding, cb)`: a zero-parameter callback keeps the
existing synchronous path, and a callback that declares the error (a dyn
value, or `null | Error`) is wrapped by `scr_stream_ecb_wrap` with the
compiler-built adapter that delivers the error the write completion
carries. The stream unit publishes that error only in its
`SCR_STREAM_WRITE_ERRORS` build, selected by the new `streamWriteErrors`
runtime feature (rule: any `writable.writeEcb`). Every other build
compiles `SCR_WCB_CALL` to the same plain call, so their objects are
unchanged.
The destruction path also matched Node: `afterWrite` calls `cb(null)`
even for a stream destroyed after the write was queued (Node 24
`internal/streams/writable`), so the destroyed `SCR_ST_AFTER_WRITE` tick
passes NULL rather than the stream's stored error.
Corpus: `tests/corpus/writable-error-callbacks.ts` covers success, an
erroring write, a write after end, and a write on a destroyed stream.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- scr_intl.c resolves the default locale as V8 does: ICU's POSIX locale
(LC_ALL=C, C.UTF-8, POSIX) becomes en-US, so numbers keep their grouping
separators, and other defaults round-trip through their BCP 47 tag. The
number formatter, the date pattern generator, the date formatter and the
default word segmenter use that locale.
- The date formatter moves the Gregorian calendar's Julian cutover to the
start of ECMAScript time, so dates before October 1582 print their
proleptic Gregorian fields, as in Node.
- icu.numFormatDefault, icu.dateLocaleString and icu.dateLocaleTimeString
are in the may-throw set: a missing ICU data file throws an Error into the
surrounding catch.
- `x.toLocaleString(undefined)` reads the receiver before the argument runs.
- The linux-x64-gnu runtime pack ships the ICU headers scr_intl.c includes
beside its ICU archive (manifest `headers`, verified like the other
artifacts). The native compiler's sanitized build compiles scr_intl.c
against them and refuses with SC3003 when they are missing or changed.
The C-source path (sanitized builds) links the pack's ICU archive and
uses those headers, instead of building ICU with Zig's C++ driver.
- The macOS pack omits scr_intl.c, scr_intl.h declares ScrDyn, and the
library-mode teachings test uses a locale argument, which still refuses
with SC2020.
Corpus programs pin the C and C.UTF-8 locales, pre-1582 dates and the
receiver order against Node. intl-missing-data.test.ts covers the throw.
sanitizer.test.ts covers the header lookup.
fs.watchFile(path[, options], listener) polls the resolved path every
`interval` milliseconds (5007 by default, `persistent` by default) and
calls its listener with (current, previous) Stats when a poll sees a
difference. The first poll only records the baseline, a path that cannot
be stat'd polls as all-zero Stats, and a stat failure that repeats for
the same errno is not re-reported. One poller per RESOLVED path is
shared by every listener on it, and fs.unwatchFile(path[, listener])
removes the last listener registered with that callback, or every
listener when called without one. The call is a statement, since Node's
StatWatcher has no lowering, and bigint: true is refused.
The implementation is scr_watch_file.c, a new gated runtime unit
switched by moduleUsesFsWatchFile, so programs that do not poll link
none of it. The feature travels the usual chain (runtime-features,
executable-features, the native link info, the executable cache keys,
the runtime-pack matrix) and the validator learns that
fs.unwatchFile's trailing listener slot may be omitted.
Evidence:
- tests/corpus/fs-watch-file-names.ts and fs-watch-file-polling.ts match
Node on stdout, stderr and exit status in the plain and sanitized lanes
- 1564-fs-watch.ts, 1751-options-record-stance.ts and the `fs` corpus stay
green; the new order baselines and corpus cost files are recorded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pack builds the ICU archive with the same Zig C++ recipe, adds
scr_intl.c as an optional unit under the intl predicate, and publishes
the archive under the id "icu". Its data file (icudt78l.dat) is not a
link input: the pack lists it in a new manifest "data" array, and every
Intl build copies it next to the executable it links (verifyArtifact
checked, atomic copy, size-checked before reuse). A pack that omits
scr_intl.c refuses an Intl program with SC3002 instead of linking
undefined scr_intl_ references.
The pack fetches both ICU inputs from the pinned release assets: the
source tarball (verified, cached, unpacked by the system tar) for the
archive, and the data zip (sha256 and size pinned twice) for the data,
unpacked by a small stored/deflate reader. Nothing is embedded in a
binary. The runtime fingerprint keeps covering the vendored trees, the
pinned ICU version keys the archive path, and the pack matrix gains the
icu archive with the intl predicate.
The isolated native host (native/compiler.ts) links outside the in-process
linker and writes its executables synchronously, so it installs the same
data artifacts itself (installNativeRuntimeData); the stored pack
selection gains a schema version so a selection cached before the data
list existed is discarded rather than trusted. Nothing selects a data
artifact for a program without the intl feature.
The sanitized lane links the pack's ICU archive as shipped. ICU is C++
and compiles with Zig's C++ driver for the packs' glibc floor, so it cannot
ride the sanitizer lane's per-TU C rebuild, and instrumenting ~600 C++ TUs
would add a multi-minute build to the first Intl program in every sanitized
lane. scr_intl.c itself still compiles instrumented with every other runtime
unit, and AddressSanitizer's interceptors cover ICU's allocations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With --exceptions=rare, recursive functions and functions up to three
calls above a throw also propagate exceptions by unwinding on Linux, so
calls to them need no pending-flag test. Functions with a throw of their
own keep the flag. The default, common, keeps the current rules, and
builds without the flag emit the same code as before.
The option is part of the executable cache key; the bootstrap's cached
route leaves builds with it to the full compiler, and coverage and
library builds refuse it.
Adding the six new entry points (scr_child_off_spawn/_exit/_close/_error,
scr_child_has_signal_code, scr_child_signal_code) to scr_child.c changed
the executable of every program that links the unit, although none of
them calls the new functions and IR and LLVM stayed identical: the extra
functions grow the unit's `.eh_frame` contribution, and the linker's
frame bookkeeping for the retained sections changes with it (the first
visible difference is the phdr sizes at byte offset 208). Appending them
after the unit's own functions does not help — the unit's frame section
still differs, so all 89 corpus programs that link scr_child.o got a
different binary.
Move the entries into scr_child_lifecycle.c, a unit of their own
selected only by programs that use them (feature childLifecycle, the
scr_dyn_handle_props.c pattern: the matrix's optional source list, the
RUNTIME_FEATURE_RULES rule for child.offSpawn/offExit/offClose/offError/
signalCode, and the link-feature plumbing through the executable cache,
CcOptions and RUNTIME_UNITS). scr_child.c keeps its original content and
layout; the new unit reads its registries through scr_child_private.h,
which holds the per-platform ScrChild layout both units compile against.
A program that does not call the new entries links exactly the units it
did before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `file:` module namespace handed to typed code now boxes as Node's
namespace object instead of the "converting typed values to 'unknown'"
fence: `const u: unknown = ns`, `take(ns)`, a `Promise<typeof
import("./m")>` widened to `Promise<unknown>` (or `Promise<unknown> |
undefined`), and `await import("./m")` in an `unknown` slot all see one
non-extensible null-prototype object per module whose own properties are
the module's exports in Node's sorted order. They are enumerable
read-only accessors whose reads call the compiled namespace getter, so
they stay live, and the object carries Symbol.toStringTag "Module",
Node's strict-mode write TypeErrors, the not-extensible add error and
the ToPrimitive throw. `node:` builtins have no compiled export table
and keep their SC1101 fence.
The object is built by a new gated runtime unit,
packages/runtime/src/scr_module_namespace_dyn.c, selected by the
`moduleNamespaceDyn` runtime feature (`module.namespaceDyn` in the IR).
It adds no field to ScrDynHandleOps and no branch to a shared unit: the
core reaches it through the ordinary SCR_DYN_OBJ paths, so key listing,
for-in, `in`, JSON, identity, property reads and assignment all reuse
the existing dispatch. The compiler reuses the getter lifted for
module.namespaceValue (existing programs' IR is unchanged) and adds
moduleNamespacePromiseDynValue, one lifted async mapper per module for
the promise slots.
Zero cost: a program that never boxes a namespace links exactly what it
did. The zero-cost gate reports every program identical, with
byte-identical IR and LLVM; the runtime pack's only difference is the
new unit (0 changed runtime functions), and among the namespace corpus
programs only 8412 links it.
Evidence: tests/corpus/8412-module-namespace-untyped is a differential
program against Node in both the plain and sanitized lanes, and the
diagnostics snapshots and TypeScript 7 baselines stay unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A profile annotates a function only while its control flow hashes as it did
when the profile was collected, so compiler and source changes leave the
changed functions without counts. A --profile-use build now reads a
block-count file beside the profile (app.blocks beside app.profdata). Right
after the annotation before inlining, the helper aligns each uncounted
function's blocks with its recorded blocks by label kind and successor
count. Paired blocks take their recorded counts and their edges the recorded
edge counts, by target or, between targets whose labels changed, by
position. LLVM's flow inference fills in the rest, avoiding the edges the
function's own llvm.expect weights make unlikely. The helper then sets the
entry count, branch weights and hot or cold attribute as the profile
annotation does.
SCRIPTC_RECORD_PROFILE_BLOCKS names the file a --profile-use build writes
for the functions its profile annotates; such a build reuses no cached
artifacts. The block file keys every cache, the build report counts the
functions it annotated, and the staleness warning counts only the functions
left without counts. A single-module build with a context-sensitive profile
no longer counts annotated, mismatched and missing functions a second time
after inlining. scripts/refresh-cli-profile.mjs records the file in the
second phase and build-native-cli.mts reads it beside the profile.
A dev executable was one partition up to 1 MiB of LLVM text, so every edit
compiled the whole program again. Unoptimized partitions are compiled on
their own, with nothing imported across them, so dividing a program does
not change its code's quality, and the helper reuses each partition an edit
does not change from the partition cache.
nativeProgramPartitions now takes whether the program is optimized. Dev
executables are divided from 64 KiB of LLVM text, with at least 32 KiB per
partition: four partitions up to 128 KiB, then at least 8 as before. Release
and speed executables keep their partition and code generation counts.
A Linux build that links with lld records the driver's ld.lld job with the
identities of the files and directories it names, and later links run that
job directly while those identities match. When one of them changed, for
example when a system update replaced libc or the linker, the link went back
to the driver, but the entry stayed: record() kept the existing entry, so
every later link of that shape went through the driver until the entry was
pruned.
An entry whose job names a changed path now counts as missing. A listed link
records the job again with the driver's -v -### and runs it; a traced link
through the driver records it from its -v output. Entries without a job,
for links that cannot run as one ld.lld job, are kept as before.
Builds on Linux and macOS now connect to a shared `tsc-ts --api --listen`
server by default, which keeps the files it parsed and bound, so a later
build in the same working directory does not parse the TypeScript libs and
scriptc's declaration files again. SCRIPTC_CHECKER_SERVER=0 turns it off.
When CI is set to a value other than 0 or false, builds use their own tsc-ts
process unless SCRIPTC_CHECKER_SERVER=1.
- The server exits after 600 seconds without a build (was 300).
- Its name also digests the compiler executable's identity, so each scriptc
installation and build connects only to servers it started.
- The server process runs in the root directory, so it does not keep the
first build's directory in use; its --cwd argument is unchanged.
- When the connection to the server fails after it answered, through a
failed transfer or an ended stream, the native CLI runs the command again
in a new process with SCRIPTC_CHECKER_SERVER=0 and exits with its status.
- SCRIPTC_CHECKER_SERVER no longer changes the frontend cache key: the
server's answers are the build's own checker's.
The CLI docs describe the background process, its socket directory, when it
exits and how to stop it.
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.
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.
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.
Optimized executables with at most 1 MiB of LLVM text were compiled as one
module on one thread. With more than 128 KiB of LLVM text they now pass
several outputs and `--split codegen` to the LLVM helper. The helper
optimizes the module whole, as for a single output, then divides its
functions in module order into runs of about the same instruction count and
generates code for each run on its own thread.
- The output count is 4 or 8 and depends only on the module's size
(nativeCodegenParts), so artifacts never vary with the host. Node and
native compiler builds pass the same outputs.
- The helper writes the optimized module to bitcode once with its use-list
order. Every output after the first parses it and turns the other
outputs' definitions into declarations; the first output does the same to
the optimized module in place. Each function's machine code matches a
single-output build except for jumps to a definition that another output
also uses.
- Only local definitions that another output uses become hidden. Dead
constant users are dropped before unused declarations are removed, so no
output names a thread-local variable it does not use.
- When one run would hold more than three quarters of the instructions, the
first output is generated from the optimized module as for a single
output and the others are empty objects.
- Dev builds, partitioned builds and program-object validation are
unchanged.
A large program's hot code spans hundreds of 4 KiB pages, more than the
instruction TLB holds. tsc-ts has 12 MB of code, and 90% of its
main-thread samples at --checkers 1 fall in 274 distinct code pages.
`scriptc build --large-pages` links the new runtime unit
scr_text_pages.c, whose constructor moves the executable's code onto
2 MiB transparent huge pages before main. It copies the executable
segment, from its first to its last 2 MiB boundary, into anonymous memory
advised MADV_HUGEPAGE, makes the copy read-only and executable, and moves
it over the original range with one mremap, so addresses and instructions
stay the same. The move runs only on Linux, only when THP is "always" or
"madvise", and only in executables. If a step fails, the code stays
file-mapped. Like Node's --use-largepages, it is off by default: without
the flag the unit is not linked, and executables are unchanged.
The move costs about 3 ms per 10 MiB of code at startup, and all of the
moved code stays resident as private memory. The flag is part of the
executable cache key and the runtime-pack selection, and is refused for
non-executable output, like --detached-exit.
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>