NODE_ISLAND and the twelve WS_* stream handles lived in
SCR_DYNH_NEW_TAGS, whose rows are numbered positionally from the
largest pinned value. Adding SEGMENTER_WORD and SEGMENTS_WORD ahead of
them renumbered every WS_* tag by two, and those numbers are immediate
operands in the link-gated stream units (scr_webstreams.c,
scr_stream.c, scr_stream_from_web.c, scr_fetch.c): eight runtime
objects changed and 35 non-Intl corpus programs stopped being
byte-identical in the zero-cost gate.
Move the thirteen older rows to SCR_DYNH_TAGS with the values they
already had (84 through 96). Their numbers, the enum, and every unit's
object code return to the base build, and the two Intl tags number from
97.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ICU builds with symbol renaming on, so its C API symbols carry the _78
version suffix, its data symbol is icudt78_dat, and its C++ symbols are
Itanium manglings that name namespace icu_78. icuSymbols() classifies
exactly those, and the gate's dynamic-tier check now fails a candidate
that links any of them without the pattern being allowed: a program that
does not lower to Intl links no ICU object at all.
The engine-free Intl functions (scr_intl_num_format_en_us in scr_lib.c,
scr_intl_segmenter_new in scr_grapheme.c) share the scr_intl_ prefix and
are deliberately not part of the test; any scr_intl.c function that does
link brings an ICU symbol with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three corpus programs execute the new forms under both Node and the
compiled binary: default-locale number formatting (grouping, CLDR
fraction digits, infinities, -0, the capability probe), default-locale
Date formatting and time formatting, and word segmentation (segments,
isWordLike, re-iteration, containing). Their ts7 order/preflight
baselines record each as one module with no diagnostics.
The harness learns two directives: `@env: NAME=VALUE` pins TZ/LC_ALL for
BOTH sides (the directive is part of the program bytes the oracle cache
key hashes), and `@requires-icu` skips programs on hosts whose packs do
not carry ICU in this slice.
The npm-static word-segmenter pilot no longer expects the package's
load-time segmenter to fence into a Node island: word segmentation
lowers now, so the package compiles statically (checked on the linux-x64
pack; other machines check the analysis and stop before the link). The
coverage snapshots and the mixed/tier fixtures move with the narrowed
refusal text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The forms the first slice answers now lower: x.toLocaleString() (and an
explicit undefined locale) to icu.numFormatDefault, the argument-free
Date#toLocaleString/toLocaleTimeString to the ICU date formatters, and
Intl.Segmenter({ granularity: "word" }) to the ICU word iterator (a
literal language tag is the locale; the empty string is the default).
Sentence segmentation keeps its refusing construction in JavaScript
dependencies, the grapheme segmenter stays the engine-free fast path,
and the code notes the deletion I4 schedules.
The refusals that named these forms follow: the four stdlib hints, the
Intl identifier hint, the Date method hint and the Segmenter option
messages list what lowers and what still does not (locale negotiation,
options bags, toLocaleDateString, formatter values). The library mode
refuses the icu.* libCalls, whose ambient locale state and data file a
library archive cannot carry, and the ambient process.config comment says
what a compiled binary can honestly answer.
An argument that IS undefined (exactly undefined, or void) means the
absent form and lowers; one whose type merely includes undefined may hold
a locale or an options bag at run time, and the constructor cannot read
either, so it is refused rather than silently treated as absent.
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>
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>
scr_intl.c is the ICU-backed unit: Intl.Segmenter word granularity over
ICU's UAX #29 iterator, and Number/Date toLocaleString and
toLocaleTimeString at the DEFAULT locale (uloc_getDefault, the same
answer V8 gets). The engine-free fast paths keep their own units: the
table-driven grapheme segmenter in scr_grapheme.c and the embedded en-US
number format in scr_lib.c.
ICU's data file is looked up beside the executable first ($ORIGIN-style,
via /proc/self/exe and its peers) and left to ICU's own search after that
(ICU_DATA). A program that uses Intl without the file throws a named
Error instead of formatting from nothing.
process.config.variables.v8_enable_i18n_support answers 1, as Node does:
the read lowers to this unit, so the flag is true of the binary that
reports it and no binary that reports it can lack ICU.
Two dynamic-tier tags (SEGMENTER_WORD, SEGMENTS_WORD) carry the handle
kinds, appended at the end of the list so pinned numbers do not move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node answers every locale-dependent Intl form from ICU, so a compiled
binary that must match it byte for byte has to run the same release over
the same data. vendor-inputs.ts pins ICU_VERSION = "78.3" beside the
other vendored versions and vendor-inputs.test.ts asserts it against
process.versions.icu, the oracle the differential corpus compares with.
ICU is not vendored. Its source tarball (icu4c-78.3-sources.tgz,
27,977,255 B) and its prebuilt data zip (icu4c-78.3-data-bin-l.zip,
12,836,659 B) come from unicode-org/icu's official release-78.3 assets,
each pinned by version and sha256 beside ICU_VERSION and mirrored in the
runtime packs' icu-inputs.mjs, with a unit test that keeps the two
identical. The tarball carries ICU's own sources.txt lists, which stay
the list of record: icuSourceList() reads them so the archive holds
exactly the units ICU's autoconf build compiles. ICU_CXX_TARGET names
the glibc floor the archive is built for.
The data (icudt78l.dat, 33,107,232 B) is shipped beside the executable,
never embedded: 970 source files and 33 MiB of compiled CLDR tables are
a download, not a reviewable tree in this repository.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A borrowed element read of a reference array falls back to
scr_arr_borrow_ref when the element is outside the dense fast path. The
call carried no memory attributes, so LLVM assumed it could write any
memory: a loop holding such a read reloaded the array's header on every
iteration, after inlining too. The fallback only reads the array (a hole
or an out-of-range index traps), so it now carries the attributes of the
other array read fallbacks, and the header loads stay hoisted.
- A BigIntStats Date member is built from its truncated millisecond member,
as Node's getters do, where the Stats getters round the fractional one: a
timestamp at least 0.5 ms past a millisecond no longer reads one
millisecond later than Node.
- statSync, lstatSync and fstatSync evaluate a bigint option whose value can
have an effect (`bigint: (count++, true)`), in member order after the path
and before the stat; an exception it throws is the call's.
- An object spread of a node:path namespace in JavaScript copies the module's
members, read through the module value so the copy holds the same
functions: an ES module namespace with its keys sorted and `default`, and
posix, win32 and the default import in Node's member order.
- scr_lib.h declares the BigIntStats readers, so every runtime name the
backend can emit has a prototype in scr_runtime.h.
A scanner reading a mostly ASCII text front to back (slices and
positioned startsWith at increasing positions) walked from the hot
cursor on every read within 64 units of it, since the near-cursor walk
came before the ASCII-run growth. A forward read past the run now grows
the run first. When a non-ASCII character ends the run, a nearby read
walks from the run's end or the cursor and starts a new run where it
lands.
The helper's program target options left UseInitArray off, so a program
constructor went to .ctors. The C startup files and lld run only
.init_array entries, so such a constructor never ran. Runtime units
already set the option (runtime-unit.cpp); program code now does too.
No program emits a constructor today, so executables are unchanged.
native-constructors.test.ts emits a constructor through a single object,
split code generation and program partitions, checks that it lands in
.init_array on ELF, and runs it.
The tag test and the walk share one evaluation: a call in the loop head
appears in the IR exactly once, and the loop walks the value the test saw.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- lowerForOfEmptyArrayArm binds the `Set | Map | string | never[]` source
to a hidden local before the tag test, so a call or getter in the loop
head runs once and the loop walks the value the test saw.
- markTarget marks a structural view as a collection view when its source
is already one, and reruns the fixpoint when a view marked earlier
becomes a collection view, so a Map or Set handed through two views keeps
answering through the collection.
- Promise.all in a JavaScript source whose call types as Promise<any[]>
(an unannotated parameter) runs the engine's Promise.all over the entries
as checked-dynamic values when they are typed promises. A merged
`Promise<number | string>[]` argument reaches that call since union
promise arms merge.
Corpus 8419 and 6956 and the js-own-tostring npm fixture cover each case.
`const proc = globalThis.process` lowers to alias plumbing: the declaration
emits no storage and no code, because member and call reads resolve through
the global it names. A read that needs the VALUE therefore failed at the use
with the generic "binding form with no lowering", which names neither the
declaration nor the cause.
Register every stdlib-global snapshot declaration (TypeScript sources only,
matching the elision) and report the cause at the declaration (SC2020),
naming the global and, when it has a short spelling, the type the program
asked the compiler to hold. The use site keeps the inherited-blocker cascade
(SC2004) instead of the generic text.
An unread snapshot stays elided: reading a global has no side effects, so
dropping an unread `const p = process` is observationally identical to Node,
and refusing it would break programs that compile harmlessly today.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An object literal whose get/set accessor satisfies a member the target type
declares as DATA was refused with SC1090: a record member is one storage
slot, so the accessor could only be snapshotted at construction. Node keeps
the accessor live, and the pi sites that hit this (agent.ts:90,
runner.ts:889, codemode/execution.ts:562) are all live getters — one throws
until the object observes later state — so a snapshot is disqualifying.
One static record layout cannot hold both member kinds at one name, so the
analysis marks the TARGET type instead of inventing a literal-only variant:
when a literal's member kind disagrees with the member the contextual type
declares at the same name, that type keeps checked identity, exactly as a
structural type that carries a class instance's identity does. The literal
then lowers through the checked-dynamic literal path, which defines accessor
members as live get/set properties and data members as plain properties, and
reads, writes, Object.keys, `in` and JSON.stringify on the declared type
dispatch dynamically — Node's own model.
The mirrored direction is closed too: a data property satisfying a declared
accessor member (`interface Live { get size(): number }`) used to be an
SC2002 missing-`%get:`-slot refusal; it now keeps the declared type's
identity as well.
Cost, the accepted trade: every value of a MARKED type becomes a checked
dynamic view — a property table and closure dispatch instead of a static
struct slot, so reads and writes on those types are slower (never wrong).
The cost is scoped to the marked type: a program with no such literal marks
nothing, and the frontend analysis itself adds only one array push per object
literal and one Set lookup per data member, with no checker query unless a
member name is an accessor somewhere in the program. IR is byte-identical
for 64 existing corpus programs compiled before and after. The whole-type
accessor-shape form (static reads, closure slots) remains available as the
faster representation if profiles ever show this matters; it needs the
accessor enumeration surfaces (keys, JSON, inspect, for-in, spread, rest)
that this route gets for free.
Only a single object type can be the literal's target: a multi-arm union
picks its arm from the value itself and an intersection merges members, so
neither is marked and both keep today's behavior. A value that reaches the
slot through a variable rather than a literal keeps today's refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An exhaustive switch's `default:` branch declares `const unreachable: never =
event`, and the warning reporter reads a member off that value through an
erasure (`(event as any).type`, `(event as unknown as { type: string }).type`) —
the shape pi's proxy warning takes. The union shared-field read refused it
because the checker types the receiver `never` (or the erasure), not the
union the value still holds.
`lowerUnionProperty` now proceeds when the receiver expression is a `never`
value, directly or through the parenthesized/parenthesized-assertion chain
that escapes it, and reads the field off what the lowered value actually is:
the shared-field read for the union it holds, the record read for a record,
the keyed read for checked-dynamic. `never` admits no reachable call site, so
no reachable program changes shape.
Corpus: tests/corpus/erased-never-member-read.ts (`as any`, `as unknown as`,
and a helper function taking the same shape). Node-identical stdout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A method declaring `ctx?: Ctx` completes to `(id, Ctx | undefined) => string`;
a slot that requires the narrower `(id, ToolCtx) => string` was refused with
SC1090 at the value's birth, because the gate only compared the two
signatures for equality. The slot coercion (funcCoerceAdapter) already
converts every parameter through coercibleValue, the record width conversion
(which also selects a width-liftable arm of a union destination — the
`ToolCtx` into `Ctx | undefined` shape), the partial checked-dynamic
extraction, and the union arm preservation. `funcSlotAdaptable` now asks the
same question through `cleanParamPair`, so the gate admits exactly the pairs
the adapter converts without a trap-only path, and both call sites (the
object-literal method slot and the mapped value target) use it.
The gate also learns the two slot shapes it could not name: an object
literal contextualized by an INDEX SIGNATURE (`Record<string, Handler>`)
takes its member slot from the index signature's value type, and a literal
with no contextual type takes the member's own completed type — the type
every later read of the member spells.
Nested function DECLARATIONS are no longer judged at birth: lowering a
declaration builds the body, not a value, and each value use re-checks the
completed signature against that use's own target. An overloaded nested
declaration with an optional parameter, returned as a value (`return write`
into a one-parameter `Writer`), is now accepted instead of refused.
Corpus: tests/corpus/optional-param-function-values.ts (a required slot over
an optional parameter, an index-signature member with an extra trailing
optional parameter, an annotated value with a defaulted parameter, the nested
overloaded declaration returned as a value, direct-call controls, and a
union-typed slot). Node-identical stdout.
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>
A spread merge like `{ ...base, ...override }` whose sources are union
types has one field set per combination of source arms, so the merged value
can carry fields from several arms at once. No single arm of the
destination union can hold it — naming one would drop the other arms'
fields, which Node keeps — and no runtime tag would be honest. The merge
was refused with the generic "cannot be compiled yet" (SC2001), which says
nothing about the shape the program is building.
The new SC2003 names the destination union and lists the candidate arms,
and its hint gives the two repairs: merge ONE arm per path (narrow every
spread source to the same arm first), or declare one open record type whose
fields are all optional instead of a union of all-optional arms. The
diagnostic fires only for a union-typed destination; other non-record
destinations keep badType.
`NonNullable<T>` over a union already maps to the union value itself, so
tests/corpus/7017-nonnullable-union.ts pins the mapped-type path
(assertions, generic instantiations, narrowing, serialization) against
Node. tests/diagnostics/union-spread-merge.ts re-pins the refusal, and the
union-clone-override-width snapshot moves from SC2001's generic text to
the arm-naming SC2003.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A union of records whose every arm declares the same field optional has a
definite answer for `delete u.k`: the field becomes absent, exactly as the
monomorphic optional-field delete writes it. The delete statement used to
fall through to the receiver-agnostic path and refuse the union.
lowerUnionFieldDelete dispatches over the arms (one tag test per arm, the
last arm as the fall-through else) and writes each slot's absent value
through a recordSet on the narrowed arm; the union value is retained once,
so every arm deletes the object the program passed rather than a
projection. Sloppy-mode writes keep their frozen-record semantics.
Everything else keeps its fence: unit arms, class arms, required fields
(a slot with no absence to represent), accessors, index signatures,
optional chains and tuples all return null and reach the existing
diagnostics.
tests/corpus/7016-union-field-delete.ts pins the delete, the absence
afterwards (`in`, optional reads, JSON.stringify), the retained identity
the caller's alias sees, and deleting an already-absent field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
path.posix and path.win32 are module objects, not declarations: they are
the same handle `process.getBuiltinModule("node:path")` answers, so
`posix === path.posix` holds and a mutation through one spelling shows
through the other. Reading either namespace as a value mapped to a module
namespace type with no value lowering, and `import { posix } from
"node:path"` was refused outright.
The platform namespaces now lower to the checked-dynamic module value, as
a `path.posix` property read, as a named import, and as a receiver
(`win32.join(...)`); pathModuleValue answers the module handle instead of
building a fresh object literal, which is what makes the identity and the
cached members hold. Type mapping treats a path platform namespace and
@types/node's PlatformPath interface as the dynamic type they are, so a
union with a plain record arm collapses onto it instead of refusing.
tests/corpus/7015-path-platform-namespace-values.ts pins the identity,
cached members, joins and the import forms against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The socket registry, the http client registry, an agent's entry list and
fetch's list of transfers in flight were singly linked: each append walked
to the tail and each removal walked to the element. A client request on a
pooled connection registers and unregisters a socket and a client and adds
and removes an agent entry, so every request walked lists as long as the
open connections. These lists now link both ways with a last pointer, in
the same order as before. A client finds its agent entry through a
back-pointer, and an agent counts its queued entries, so a finished
request scans for a queued one only when there is one.
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.
Adds the technical-writer skill from vercel/chat to .agents/skills, with a
.claude symlink to .agents so Claude Code finds it. AGENTS.md now asks agents
to use the skill for prose in this repo.
CONTRIBUTING.md gets a matching "Writing with AI coding agents" section.
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
A loop of two to eight iterations with a constant count, whose body is an
inner loop that exits once an accumulator reaches the value its ORs
saturate at (mandelbrot's pixel pairs: `b |= 2`, `b |= 1`,
`if (b === 3) break`), now runs its iterations in one inner loop: every
iteration's declarations, then one loop running each iteration's body in
turn that exits once every accumulator is full, then every iteration's
trailing assignments in order.
Once an accumulator is full its ORs leave it full, so an iteration that
runs past its own exit leaves its accumulator as its own loop did, and
every other value it computes is dead. The bodies only compute numbers,
read typed-array elements that answer NaN out of range and write their
own locals; the declarations and the inner loop read nothing the trailing
assignments write. The independent iterations then overlap their
dependence chains.
scr_str_case_conv scans ASCII text for the first letter to convert eight
bytes at a time, then converts the rest eight bytes at a time; the tail
and big-endian targets take the byte loop. With a refreshed profile, the
byte-at-a-time scan compiled worse for short strings with nothing to
convert, which made validate-errors slower.
The runtime profile is collected again on this change with
scripts/refresh-runtime-profile.mjs (326 raw profiles). Changed
functions hold 0.0% of its counts, against 3.7% for the previous one.
tests/corpus/string-case-ascii-words.ts covers ASCII strings of every
length from 0 to 40, with letters at block boundaries and next to the
bytes just outside A-Z and a-z.
An uncork() flushed a head writeHead stored even when nothing was
written while corked. Node keeps the stored head until write(), end() or
flushHeaders(), so the flush now sends it only when a write() or
flushHeaders() ran while corked. The http-held-head fixture covers the
three cases.
The call-lifetime and string-and-array lifetime tests kept their optional element union a box with a `Set<Item | undefined>`, which no longer boxes a union of identity arms. They now emit with keepSetElementsBoxed, which keeps the unions the module's Sets hold boxed.
An unchecked element read of a tuple array (`rows[i].length` with
`rows: [number, string][]`) answers the element's optional value, and the
computed `.length` path discarded it, so a read past the end printed the
tuple's arity. It now holds the receiver and throws Node's TypeError,
"Cannot read properties of undefined (reading 'length')", when the element
is missing, before answering the arity.
The corpus program tuple-length-computed.ts covers a missing element read
directly and through a field holding the array.
#2468 shipped child.off()/removeListener() and child.signalCode in the gated
scr_child_lifecycle.c unit, but it also restructured the always-linked
scr_child.c terminal dispatch loops to detach their listener arrays before
emitting. scr_child.c is compiled once per program and linked into all of
them, so its object moved and every child-process executable grew: the
zero-cost gate reported 55 runtime-only corpus programs.
Restore scr_child.c byte for byte and move the emit-snapshot discipline into
the gated unit instead. The base loops already bracket each terminal emit
with a state flag that only the emitter writes -- `settled` for 'exit' and
'error' (set after the stream drain, immediately before the terminal branch)
and `close_done` for 'close' (set at the top of the close emit) -- and run no
user code in between. A removal inside that window is therefore a no-op: the
entry stays, the in-flight emit still delivers it, which is exactly Node's
answer (emit copies the listener array), and the emitter's own drop releases
it. Outside the window no emit of that event is in flight, so removal in
place is Node-exact, and the base already releases late registrations once
the window has closed. 'spawn' detaches its array before invoking its
listeners, so removal during it finds an empty registry.
scr_child_private.h repeats the structure layout for the gated unit only;
scr_child.c keeps its own definitions untouched.
Validation: zero-cost gate against 68980c241 reports no runtime-only program
and no scr_child symbol in any IR, LLVM or executable difference; scr_child.o
is byte-identical to the base in all six shipped flavors, and the
scr_child_off_*/scr_child_signal_code entries live in scr_child_lifecycle.o.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The BigInt reciprocal divided a two-limb value by one limb, which compiles
to a __udivti3 call. zig provides that builtin from its compiler-rt, a
single object that also defines memset, memcpy, memmove, memcmp, bcmp,
strlen and the _chk variants. Linking it for __udivti3 made those
definitions take precedence over libc's in every glibc executable built
with SCRIPTC_CC=zigcc, and zig's bcmp and memcmp compare one byte per
iteration and its memset stores one byte at a time.
The reciprocal now divides in 32-bit halves with one-limb operations, so
the runtime references no compiler-rt builtin and glibc executables import
the four functions from libc.
The sweep and the dispatch loop's pending check walked every registered
socket on each loop pass, so a server with many keep-alive connections
paid for all of them on every request. A socket now joins a work queue
when it may have deferred work (bytes buffered for a consumer, write
callbacks waiting for the drain, 'finish', an error or 'close') and
leaves it when a sweep visit leaves it none. The queue is ordered by
registration, so the sweep visits sockets in registry order as before.
With SCR_RC_AUDIT, the dispatch loop checks that no registered socket
outside the queue has deferred work.
res.writeHead() builds the head and keeps it until the first write(),
end() or flushHeaders(), which sends it with that call's data in one
socket write, as Node's _storeHeader does. A response with an Expect
header sends the head at once, as in Node. The http-held-head server
fixture compares the wire timing with Node's.
A union that is a Set element or a Map key was a tagged box in the whole
program, because the C map hashes and compares union keys by tag and
payload. One `Set<Node | undefined>` made every `Node | undefined` value
of the program a box, at every read.
A union whose arms all compare by identity (records, class instances,
arrays, functions), beside at most one undefined and one null arm, now
keeps its pointer representation in collections too. The map keys such a
union by reference: the pointer carries the identity, and NULL and the
null sentinel are two more distinct keys. Unions with a string or another
value arm keep their boxes, and library builds keep the earlier rule.
The tests of the box paths keep their unions boxed through a new
`boxedUnions` emission option instead of a Set of each union.