The ICU unit's WTF-8 to UTF-16 conversion counted units from lead bytes
alone. A 4-byte-shaped encoding of a BMP value counted two units and wrote
one, so ICU received a length past the units written, and word segments
built from those ranges could return prior heap contents. The conversion now
decodes each maximal ill-formed subsequence to U+FFFD, as the WHATWG UTF-8
decoder does, keeps WTF-8's 3-byte lone surrogates as their unit, and
reports the units it wrote. The decoder lives in scr_intl_utf16.h.
Directory entry names (readdir, opendir) and process.argv entries now decode
as UTF-8 with U+FFFD for malformed bytes, as Node decodes them, instead of
entering strings as raw bytes.
test_intl_utf16.c checks the decoder against Node's TextDecoder over
malformed sequences. intl-malformed-utf8.test.ts compares a program that
segments malformed directory names and an argument against Node.
Adding scr_stats_of_path and scr_stats_zero to scr_lib.c moved every
program's executable: scr_lib.o is linked everywhere, so its new symbols
and strings shifted addresses even for programs that never watch a file.
The two entry points now live in an SCR_FS_WATCH_FILE build of scr_lib.c,
the matrix's variant for programs whose feature set has watchFile, and the
poll unit calls the renamed scr_stats_poll_path. Programs that do not poll
link the default object unchanged: compiled without the define, scr_lib.o
is byte-identical to the base (verified by comparing the two objects).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Every new runtime entry this wave added landed in a unit that baseline
programs link (scr_lib.c's write/copyFile flags, scr_bytes.c's
string_decoder dyn forms, scr_events.c's prepend registries and
stdin.isRaw, scr_async_dyn.c's uncaught prepend, scr_net.c's server.off).
A changed unit changes its object's build-id and symbol metadata even when
every retained function is byte-identical, so 1517 corpus programs
reported a runtime change for code they never call.
Each new entry now lives where only a program that uses it can reach it:
- writeFile's `flag` option and the fs/promises twins move to
scr_fs_flags.c (feature fsFlags); copyFile's mode form reuses
scr_fs_cp.c's reflink walk, so it is that unit's SCR_FS_FLAGS build.
- string_decoder's checked-dynamic chunk forms move to
scr_bytes_strdec_dyn.c (feature strdecDyn).
- the prepend registrations, stdin.isRaw and server.off are the
SCR_PROCESS_EXTRAS builds of scr_events.c and scr_async_dyn.c and the
SCR_NET_SERVER_OFF build of scr_net.c (feature processExtras).
The prepend entry points reuse the append registration and move the new
entry to the front, so no existing function changes and every default
object stays byte-identical. The Windows copyFile arm stays beside the
Win32 path helpers in scr_lib.c, inside its _WIN32 block.
The source build (the -O1 -fsanitize=address lane) compiles these units
from their sources rather than from the prebuilt pack, so RUNTIME_DEFINES
names the same three builds the pack matrix selects.
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>
Builds that compile the runtime from source (sanitized builds, and links
that do not start from a program object) need the same ICU code the pack
ships, so ensureIcuArchive compiles it once per flavor and caches it like
the mbedTLS archive. Its sources are the pinned release tarball, fetched
(verified against its sha256) into the same per-user build cache and
reused from there, so an offline build works from a populated cache.
The recipe is Zig's C++ driver at the packs' glibc target: host C++ would
turn on the C23 strtol names (__isoc23_*) that the artifact policy
forbids, and libstdc++ binds them too. Zig's bundled libc++ and libc++abi
sources compile beside ICU's and are merged into the archive, so the
final link stays a plain C link with the pack's -lm. ICU requires RTTI
(typeid, dynamic_cast), so the design's -fno-rtti is not available;
-fno-exceptions is, and cxa_personality.cpp (the exception personality
routine) is the one libc++abi unit left out.
Symbol renaming stays on (the _78 suffixes), which is what makes ICU
linkage machine-checkable. The unit args add the fetched include paths,
and scr_intl.c joins the optional units only when intl is selected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node answers a BigIntStats when the stat call is passed `bigint: true`: the
same snapshot, read through a bigint member table. Every number member of
Stats reads as a bigint — the integers, the `*Ms` millisecond members (the
truncated nanosecond quotient, as the binding computes them) and the
BigIntStats-only `*Ns` nanosecond members (sec * 1e9 + nsec, exactly) —
while only the four time members stay Dates. scriptc refused the option
outright.
The representation is static, chosen by the option literal: the lowerer
validates `bigint` against the call's result type (so
`{ bigint: true, throwIfNoEntry: false }` still narrows
`BigIntStats | undefined`), refuses a runtime-valued option and an option
that disagrees with the destination, and keeps every other option key
fenced. fstatSync joins statSync/lstatSync on the same path, with the
descriptor evaluated before the options.
The IR gains a `bigintStats` handle kind beside `stats`: the same RC
family, the same non-traced leaf and non-dynamic handle treatment, so no
shape, tracing or dyn-check behavior moves for any program that never asks
for one. Its member table is a `bigstats.*` lib-call family whose Date
members reuse the existing stat reads.
The runtime support is its own unit, scr_stats_bigint.c: the snapshot
layout moved into scr_stats_private.h so the bigint getters can read the
same fields without scr_lib.c carrying their code. The unit is gated on the
existing `bigint` feature in both the runtime-pack matrix and the
compiler's from-source unit table (the sanitized lane links from that one),
and `-ffunction-sections` keeps a bigint program that never reads a member
from linking any of the getters. No typed program's IR or LLVM changes.
tests/corpus/7018-bigint-stats.ts pins the member table, the Date/bigint
split, ms/ns agreement, arithmetic on a member, lstat over a symlink,
fstatSync over a descriptor, the throwIfNoEntry union, and the plain Stats
answers of the un-optioned and `bigint: false` forms against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
path.posix, path.win32 and the bare path module are one module object
each, the handle process.getBuiltinModule answers, so require('path/posix'),
path.posix and the target platform's bare path compare as Node's do, a
namespace beside other values in one slot is untyped, and path[family]
reads that object. Functions read through them take checked arguments:
each string parameter passes Node's validateString under its own name
(basename's suffix first), resolve validates paths[i] only as far as its
last-first walk reads them, parse answers an untyped { root, dir, base,
ext, name }, format follows _format over any object, and
toNamespacedPath answers a non-string as it is. A JavaScript call whose
arguments are not strings, or omit one, goes through the same checks.
Typed slots keep the typed function values.
path.win32.relative lowercases both resolved paths with
String.prototype.toLowerCase and walks them by UTF-16 unit, comparing
segments instead when lowercasing changed a length, as Node does. It
folded ASCII only, so a path with İ or Ⱥ in a different case gave a
longer relative path. The function moves into its own runtime unit,
scr_path_win32_relative.c, which links with the regex unit whose case
mapping it uses. scr_path.c stays free of it, so library and
cross-target builds that link the path functions without the regex unit
still link.
A JavaScript binding whose inferred type holds a union of functions and
arrays (a test table of [fn, cases] rows, or one destructured row) takes
the checked-dynamic kind that dynFallbackType gives the unmappable
spellings of such tables. Its rows keep their functions' names, and
for-of, forEach and destructuring read the items as untyped values.
TypeScript bindings keep their typed layout.
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>
Dev executables compiled their program shards in fixed batches of up
to eight, each batch waiting for its slowest shard before the next one
started. Keep up to eight clangs busy instead, each taking the next
shard when it finishes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A JavaScript ReadableStream given to Response, Request or fetch as a body
was converted to the string "[object ReadableStream]". The fetch unit now
reads such a stream through a reader of its own. A locked or disturbed
stream gives Node's TypeError, and the chunks must be Uint8Array
when the body is consumed ("Received non-Uint8Array chunk").
instanceof against a web stream class held in a variable or passed as an
argument walked no prototype for handles and answered false. It now starts
at the class prototype of the handle. The fetch unit's own streams, readers
and controllers, such as Response.body, are instances of the JavaScript
classes, so `response.body instanceof ReadableStream` is true again.
These paths, and the fetch reader functions' hand-off to web stream
readers, are in SCR_HANDLE_CLASSES variants of scr_json.c and scr_fetch.c,
which only programs with JavaScript web streams link; other programs that
use fetch build to the same size as on main.
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.