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>
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".
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.
A nullable class field (Tag | null) stores the class pointer itself, whatever form the union takes elsewhere. When the same union was also a Set element or a Map key, it was a boxed union outside class fields, and the frame's cleanup of a frame class released the field's raw pointer with scr_union_release. classReferenceFields now gives each field its storage type (NullableRefFields.storageType), as the heap teardown does, so the scope exit calls the class's release.
A class instance could live in its function's frame only when its class
had scalar fields only and CallLifetimes proved the binding was only read.
Per-call state objects whose fields own arrays, maps and other objects,
and whose methods and helpers store into them, stayed on the heap.
frame-escapes.ts proves which locals never leave their frame: every use
keeps the value in the frame (region-ir.ts keepsInFrame), a const binding
of it in the same function shares its fate, and a closure that captures it
stays in the frame and keeps the captured binding there. Parameter and
capture facts are solved optimistically over the call graph with a
worklist; async functions, generators and module code get none.
A frame class is a standalone class whose constructor does not let this
escape. Its instance now lives in frame storage when it is bound to such a
local, when it is passed in place to a parameter that stays in the
callee's frame, or when closures that stay in the frame capture its
binding. The frame owns the reference fields: they start empty, union and
dyn fields start at undefined, and the binding's scope (or the call's
statement frame) releases them on every exit. A cycle-capable class keeps
its collector header in front of the object, never a candidate.
setTimeout and setInterval passed their delay to the F64 timer slot
unconverted, so a delay typed number | undefined failed with SC2003. Such
a delay is an optional `ms?: number` passed on, or a number[] element
read, whose storage keeps the out-of-range undefined. Node converts the
delay with ToNumber, so undefined is NaN, the omitted delay. A union
delay now goes through lowerOptionalNumber, as Math arguments already
do. Other unions still reach the lib boundary's refusal, and number
delays are unchanged.
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.
The serial mode's wave-end check threw when a unit changed a held function in place, even when the unit had no record and its change was its own lowering's: two functions that define computed-key properties on one class both rebuild the class's descriptor guard. Each unit's in-place changes are now checked against the names its IR and new entries quote: a unit with a record that changed such an entry has no record, and a unit without one keeps its changes. The wave-end check throws only for a change to an entry no unit names.
A checked conversion of a capsule of record A into record type B, when A
has all of B's fields with the same types in any order, now answers a view:
a B object whose two trailing words hold A and A's offsets for B's fields.
Every field address of a B value tests the first word (recordViewFieldPtr,
the record clone and the JSON walker), so reads and writes through either
name reach the same storage. The view's record-source entry owns A's
capsule: boxing the view answers that capsule, so identity holds, B's
reflective surfaces and spreads see the whole object, and the entry dies
with the view, which releases A's capsule. The capsule finds its views
again through borrowed cast-list entries.
Only the target types of such conversions carry the words and the test.
They are planned from the IR before emission (record-views.ts): record
types a checked conversion can produce for which another record type in
the module has all their fields. Programs without checked conversions are
unchanged. This replaces the prefix pointer pass, whose narrower release
could not free the source's extra fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Module globals of type number join the whole-program int32 fixpoint as
`global:` candidates, starting non-negative. A candidate needs its module
initializer to assign it at the top level (its declaration), and no
write by `++`/`--`, for-of or catch; every assignment must evaluate to
an int32 under the current facts (an integer literal counts in any
function), and a write that may be negative keeps only the int32 fact.
- Reads take the proven range through IntegerSlotFacts.locals, so a
generator state reduced by a remainder (`seed = (seed * a + c) % m`)
takes unsigned integer remainders without the -0 select.
- Loop-entry versioning covers innermost for loops as well as while loops,
and a typed array index that the assumed entry ranges prove qualifies a
loop as a remainder does. In `for (let j = 0; j < line; j++)
out[o++] = f(src[i++])` the test checks `line`, `o` and `i` once per
entry, and the fast copy bounds both positions by the loop's passes.
- Proven integer loops step their shadowed bindings in expression position
too (`out[o++] = v`), so those positions run as i64 counters in the
fast copy and the typed array accesses use them without a round trip.
- An increment of a binding the enclosing for loop bounds takes the loop
range where a surrounding statement has discarded its fact.
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.
`[...text]` lowered to an array literal that spread the array of text's
code points: the literal's array copied the code point strings, retaining
each one, and the code point array then released them. An array literal
whose only element is a spread of a string is now the code points array
itself, as `Array.from(text)` already was. Both are fresh arrays.
charCodeAt on a non-ASCII string mapped the UTF-16 index to a byte offset
through a string index entry, which the first read claimed and the
string's release purged. For a string of one character, such as each
character of a for-of loop that codePointAt(0) classifies, the entry cost
more than the read.
A non-ASCII receiver of up to 16 bytes is now decoded from its start on
each read, as scr_str_utf16_len already counts short receivers. Longer
receivers keep the index entry and its cursor.
An index `base + c` converted `base` and took a fractional base as an
invalid index. A fraction smaller than the sum's precision is lost in the
sum, as in `1e-20 + 1`, which is the index 1. A base that is not an
integer now goes to the exact check of the sum itself.
The conversion also read the base local again when the access was
emitted. A write evaluates its right-hand side between the index and the
store, so `values[at + 1] = ((at = 5), 31)` wrote at the new base. The
sum now records the base value it was computed from, and the index
converts that value.
The corpus program covers both cases, and the emission test checks that
a write converts the base its sum read.
A typed array read or write at `base + c`, `c + base` or `base - c`,
where `base` is a number local and `c` an integer literal of magnitude
below 2^31, now converts `base` to an integer and adds `c` as an
integer, instead of converting the double sum. Accesses at several
offsets from one base, such as `x[at]` through `x[at + 7]` with a number
parameter `at`, then share one conversion and integer test. Below a
length of at most 2^52 the sum is an integer index exactly when `base`
is an integer, so the accesses read and write the same elements as
before.
A member read through a non-null asserted element read evaluates the
element's presence and then reads it, so a missing element throws Node's
TypeError. An index other than a local or a field read was held in a
hidden local first, which turned the receiver into a sequence: the
element was then read with a reference (retain and release) instead of
borrowed, and the fallback `genes[genes.length - 1]!.sc` of a search
made fasta's lookup loop 12% slower.
An index computed only from locals, field reads, number literals and the
lengths of arrays read through them (`xs.length - 1`, `i + 1`, `-i`)
has no effect and yields the same value when evaluated again with
nothing in between, so it is evaluated for the presence test and again
for the read, and the element stays a borrowed read. Other indexes keep
their hidden local.
A while loop whose remainder reads number locals that enter it without
range facts, such as copies of module globals, parameters or fields
(`const base = column;` before `(count + base - breaks) % 60`), now runs
as two copies. On entry, a test checks that each such local holds an
integer in [-2^48, 2^48] other than -0. The fast copy uses the facts the
integer range analysis proves when the function is analyzed again with
those entry ranges assumed: the loop's counters become bounded, and the
sums and remainders run as i64 arithmetic. When the test fails, the loop
runs as it did before.
- ir/loop-entry-ranges.ts picks innermost while loops with a remainder
that the assumed ranges prove and the function's own analysis does
not. The test covers only the locals the remainder depends on, directly,
through the loop's writes or through its condition.
- ir/integer-ranges.ts takes the assumed entry ranges per loop, and keeps
no fact about the assumed locals or the loop's writes after it.
- backend/llvm/loop-entry-versions.ts emits the test and both copies.
The test is marked likely. A function called only from the loop gets
`alwaysinline` on the fast copy's call, because the second copy would
otherwise cost it the inlining of a single call site.
reverse-complement (25M bases): 2.08 s -> 1.19 s; Node 3.97 s.
Object.freeze of a fresh record or array literal now sets the native
frozen bit instead of compiling to the literal, so an untyped reference to
the value sees it frozen: Object.isFrozen answers true and strict-mode
writes throw Node's TypeErrors.
Untyped property writes in sloppy-mode code (IrModule.sloppyDynWrites,
decided per write by isStrictCode) call scr_dyn_key_set_sloppy and its
computed form, which drop exactly the TypeError a refused assignment raises
(a read-only or frozen property, a non-extensible object, a frozen array
element) while a setter's exception still propagates. Computed-write
dispatch helpers are shared only by writes of the same mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inside a class constructor, Error.captureStackTrace(this, this.constructor) now reads this.constructor before the capture when the class or a base class declares a constructor getter, as V8 evaluates the argument: the getter runs and its exception propagates.
An instance of a compiled class reaches untyped code as a capsule, which
a property site sent to its helper on every read: the helper normalizes
the capsule, tests each class's key in turn, and loads the field.
A read site now emits the helper's plain field branches inline. A
capsule whose key pointer is one of those classes' keys loads the field
from its instance and shares the inline hits' conversion. A branch is
emitted only when its field holds a plain dyn cell and every subclass
the helper's normalization can rebox the key to reads the same field
from the same slot. Getters, bag properties, unset fields and other
capsules keep the helper.
An Array subclass's type maps to its element array type, so a field such
as TreeNode.kids holds a Kids instance in a slot typed as an array of
TreeNode. The collector visits such a slot as an array, which has a
collector header when its element ops trace, but the class only got a
header when its own class unit was on a cycle, and the collector read a
header the instance did not have. An Array subclass now carries a header
whenever an array of its element type does, and its collector descriptor
visits its elements instead of naming no children.
An element read of an array of Array subclass instances is a union with
undefined, and 'instanceof' against the class answered false for it. A
union now tests its array arms' class at run time.
A typed-array loop version runs a checked copy and one or two fast copies
of the loop, and each copy repeats every nested loop together with the
nested loop's own version. A loop whose guard proves only accesses outside
its nested loops, which run once per pass, now runs as written when a
nested loop accesses typed arrays; the nested loops still version their own
accesses. The loop versions as before when the guard proves the bounds of
an access inside a nested loop, or, in a worker program, the ownership of
an access inside a nested while loop, which never versions for ownership
alone.
Spectral-norm's paired row sums no longer copy their column loop for the
row loop's two stores. The worker program's au and atu functions are
102 KB of LLVM text instead of 236 KB each, and the program 519 KB instead
of 786 KB.
Corpus typed-array-nested-loop-versions covers row loops that store past
the end or at negative, fractional and NaN rows, views the nested loop
writes, and a nested while loop.