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 program is nondeterministic only when every artifact that differs also
differs between two baseline builds; a deterministic difference in another
artifact still fails the gate. The runtime-only check now reads the symbols
of changed vendor archives as well as runtime objects.
Records checked from a proxy in a program that freezes untyped values no
longer fail on the proxy's integrity query: a proxy is frozen exactly when
its target is. A sloppy-mode function called with undefined or null
receives the global object that globalThis reads, so its builtin members
such as console answer before anything else reads the global object. The
new corpus programs are named instead of numbered and record their CI
costs.
A method value of a web stream class, such as a saved
WritableStream.prototype.close, called on an instance whose own property of
the same name wraps it, ran the own property again, so the wrapper recursed.
The classes' method values now call the class's method directly
(ScrDynHandleClass.method_invoke); member calls still look at own properties
first.
TransformStream resolved its start promise with a transformer start's
result without adopting a thenable, so both sides started at once and a
rejection was lost. The start promise now resolves as a promise's resolve
function does (scr_dyn_promise_resolve_with): a thenable adopts through its
resolve job, a throwing `then` read rejects, and the stream starts or errors
when it settles, in Node's order.
Lowerer.namesWebStreamClass, a cache made when first needed, is listed in
Lowerer.unjournaled.
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.
The test compiles the three npm package twice, dev and release, and both
cases shared one 600s budget through test.each. Release compiles the whole
library into one LLVM module that scriptc-llvm-codegen optimizes and
codegens in a single process on a single core: 426s of the 477s total run
in the native-archive phase alone. Across sandbox lanes release measured
331, 335, 339, 345, 348, 350, 358, 428, 450, 511, 521 and 581s, so whole-
box speed varies about 1.7x and the slowest lanes sat within 20s of the
budget; the sanitized dev case hit 600s twice. There is no regression, the
work is fixed and single-threaded.
Generate the two cases in a loop so each gets its own timeout: 1200_000ms
for release and for dev under SCRIPTC_SAN, and the unchanged 600_000ms else.
Test names are byte-identical to the .each %s expansion, so
skipUnownedTest keeps assigning each case to the same shard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scr_map unit test compiles 23 runtime translation units with
ASan/UBSan, and that build is essentially the whole test: 90s of clang
against a 2.1s binary run. scr_hash.c alone takes 29s at -O1 because the
sanitizer instruments its unrolled digest rounds; scr_json.c and
scr_lib.c add 28s more. The test then normally ran within 20s of its
120s timeout, and whenever a lane shared the box it repeatedly timed out
in both the plain and sanitized lanes.
The link set cannot shrink: scr_map.c's dyn ops need scr_dyn_* from
scr_json.c, scr_lib.c supplies the remaining date/math/number/perf
symbols, and scr_lib.c needs scr_hash.c. The test asserts Map semantics
under sanitizers, not codegen, and its cost checks are relative bounds,
so build it at -O0: every assertion and sanitizer check still runs and
the build drops to about 15s. Eight concurrent copies now take 20-22s
each instead of 90s, with the timeout unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without the node index, or for a function the index didn't record,
eachNodeOfKinds passes every node of the body, statements too. The
unwind ABI's virtual-call pass cast each node to an expression before
testing its kind, and the native compiler checks that cast: with
SCRIPTC_EMIT_FACTS_VERIFY=1, whose comparison walks every body, it
stopped on the first statement. The kind test now narrows the node.
Recording a unit copied the keys and values of every appendable table before the unit and compared them after it. Each appendable table now keeps one snapshot of its entries' content per recorded wave (JournalTable.contents): a unit is checked only on the entries its IR and new entries name, by key or by the helper name its value holds, and the wave's end checks the content and key order of every entry. The name scan keeps only the strings the tables are keyed by (names that start with % and union ids). ShapeRegistry.journalSizes counts its declared-order candidates and attached names as they are added. On tsc-ts the serial mode's overhead over plain lowering falls from 12.7 s to 10.4 s.
In JavaScript sources, WritableStream with its default writer and
controller, TransformStream with its controller, pipeTo and pipeThrough,
and TextEncoderStream and TextDecoderStream join the native web stream
classes, following Node 24's webstreams step by step: sinks and
transformers, backpressure and the writers' ready and closed promises,
abort with the controller's signal, the pipe's shared write requests and
its abort signal, and UTF-8 encoding and decoding across chunks settle in
Node's order. The streams take own properties, which a pipe does not use
for its own reads and writes, and the global object answers the classes
by computed name in programs that name them.
A JavaScript class that extends one of these classes keeps the SC1090
refusal, and TextDecoderStream refuses encodings other than UTF-8.
The node24-web-stream lifecycle, errors, queues, transforms, pipes,
pipe-errors, pipe-abort, tee and encoding cases now match Node.
exactInteger can answer a limit's ToUint32 view, which for a number
literal is a literal of its unsigned bits: -1 has the view 4294967295. The
counted loop took such a literal as its i64 endpoint as written, so a
negative limit became a large positive endpoint and the loop ran when it
should not. An i32 literal endpoint is now read as signed or unsigned, as
the integer says.
A counted for loop converts its limit to an i64 endpoint. A limit that is
not a literal went through the double every time: converted to double,
clamped to 2^53, rounded with llvm.floor or llvm.ceil and converted back.
At the default target CPU the rounding is a libm call, which a loop such as
`for (let i = values.length - 1; i > at; i--)` paid on every entry even
when `at` was an int32 local. A limit that is an exact integer (an int32
local, an integer view, or a proven range) is its own floor and ceiling and
within ±2^53, so it is now the endpoint directly; other limits keep the
double path.
A function value with more parameters than the slot it flows into is
assignable in TypeScript exactly when the extra trailing parameters are
optional, and a call through the slot passes no value for them, so the
callee sees undefined as it does in JavaScript. funcCoerceAdapter refused
every such pair, so a callback parameter forwarded into a narrower slot
fell through to requireExactShape and failed with SC2003 ("union
conversion has no unambiguous layout mapping") even though the checker
accepted the program.
The adapter now supplies each omitted extra parameter with omittedArgFor
(the undefined arm of the parameter's ABI union, or the checked-dynamic
undefined). A pair whose extra parameter cannot hold undefined stays
refused, and an extra required parameter remains a checker error.
Only pairs that previously returned null reach the new code, so programs
that compiled before are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A record now has a reads section: the entries of its function tables that the unit's IR and new entries name and that the tables held before the unit, each with a digest of its content (contentDigest). An applier checks that it holds each one with the same content. This catches helpers named in creation order, such as %dyn.class.protocol.<n>, which another process can number differently.
Two lowering effects had no table a record could check. A unit that collects a function-local class registers the class's members as reachable units, and Object.freeze of a class instance adds hidden fields to the class layout. The lowerer now counts both (unitRegistrations, classLayoutChanges), and a unit that changes either count has no record.
Response.clone and Request.clone tee the body. The tee branches of a body
read from a JavaScript ReadableStream did not keep that body's chunk check,
so consuming the original or the clone took string chunks as text. The
check is now a flag on the body's stream that its tee branches inherit, and
consuming either branch rejects with Node's TypeError "Received
non-Uint8Array chunk".
analyzeFrameEscapes walked every function's body on each visit of its parameter and capture fixpoint, and once more for each function's locals: 1.8 s on tsc-ts's IR, plus 0.5 s for the locals. Each function's body is now walked once into a summary: its value groups, the groups that escape whatever the facts say, and per group the calls and closures whose facts decide it. A query explores only the facts its answer depends on (a parameter's fact depends on the uses of its own group) and solves that part of the graph to its greatest fixpoint; solved facts are kept for later queries. The answers are unchanged: on tsc-ts every parameter fact and every function's frame locals are identical, and the emitter's queries take about 0.35 s.
When Error.captureStackTrace evaluates a JavaScript constructorOpt (a getter,
a call, or this.constructor with a constructor getter), V8 has already
evaluated the target. The lowering kept the target in a local only when its
expression was not a plain read, so a constructorOpt that assigned the target
variable made the capture install the stack on the new value. The target is
now always kept in a local before the constructorOpt runs.
Under Node.js, publish() deep-freezes everything it reaches, so
Object.isFrozen and Object.isSealed return true and Object.isExtensible
returns false for a published object, array, Map or Set. Native builds
already refused writes into published values, but answered these queries
as for an unpublished value: publication marks an object immortal and sets
no frozen bit.
- record.isFrozen, array.isFrozen and the extensibility test of Maps and
Sets also read an immortal rc word as published, the test of the
published write guards, for the types the program may publish
(PublishedTypes). Other types emit the code they did.
- scr_dyn_is_frozen, scr_dyn_is_sealed and scr_dyn_is_extensible answer
for a published object, array, Map or Set, or a checked view of one,
before materializing the view.
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>