structuredClone in an island of a value holding native records, class
instances or objects (interceptor objects V8's serializer cannot clone)
clones a plain copy of that data, as Node clones the objects themselves.
An island array of primitives or strings (or arrays of those) returned
directly or through a promise crosses as a copy, which the checked cast
then meets, instead of failing the cast as an object handle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plain object a program passes into an island is an object-template
instance, which inherits from its implicit constructor's prototype object;
island code that checks for plain objects (jose's isObject:
Object.getPrototypeOf(o) === Object.prototype) rejected it. The instance now
inherits Object.prototype directly. Corpus 6826 checks it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A gap fence inside a function body may never run, so it no longer makes its
package a Node island under --node-islands=auto: only fences in top-level
module code and fences that need the JavaScript runtime do. The package's
island class still decides after a failed build. Typed packages like jose
stay native (DECISIONS.md, 2026-10-21), so the corpus pins of jose and
run-node are gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
g01 (twenty fs.stat round trips against a 2 ms timer), p10 (two 0 ms
timers armed on either side of an island call) and s06 (fs callbacks and
timers pending when Node loads) each failed one CI run in a different
lane: their order depends on how long the island call or the I/O takes,
which differs between the sides and grows under ASan. Their native runs
now have to produce the lines of one of Node's outputs, like the other
race programs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sanitized seed compiles the compiler's 700 MB LLVM module with one clang
process under AddressSanitizer. The compile peaks near 14 GB while the Node
process that emitted the module still holds about 6 GB, which exceeds the
runner's 16 GB of memory. The job now enlarges swap to 24 GB before it builds
the seed, as the sanitized emitter job does.
A number parameter that every call site proves an int32 reaches the
function's i32 entry. One that some caller passes as an unproven number,
such as a value read from a worker message or parsed from text, kept its
double, so every loop it bounds compared an integer counter with a double
limit and the values derived from it stayed doubles.
When such a parameter bounds a numeric loop (arithmetic, element and
field reads and writes, Math calls, no allocation or other calls), the
function now gets a copy of its body. Its own body first tests
each such parameter for an int32 that is not -0 and, when all pass, calls
the copy with `p | 0`, the same double. The copy's only caller proves
those parameters int32s, so it gets the i32 entry and integer loops.
Other values run the body as before.
Only plain synchronous functions of at most 2048 IR nodes are versioned:
not class members or generated helpers, not capturing, not the entry,
not dynamic-tier, not calling themselves, and at most 16 per module. A
function holding the only call site of another function is kept whole
unless that callee only computes a number or boolean from scalar reads:
a second call site can stop LLVM from inlining the callee and leave the
records it builds in memory. Library builds and modules that observe
call stacks are left as they are.
A typed record encoded from its field hooks queued a plain-object field on
the visit stack and went on to the next field, so getters on later fields
ran before the properties of earlier ones were read. Node's serializer
finishes each field, nested properties included, before the next.
The record path now drains the visit stack down to where it started after
each field, so properties are read in Node's order. Corpus 7139 posts a
record whose fields hold getter-bearing objects and prints the read order.
A function expression stored in a function type with more parameters
folds `arguments.length` to that type's parameter count, the count every
call through the slot passes. A named expression that calls itself
through its own name passes only its declared parameters there, so no
constant answers both calls. Such a read now reports SC1090 with the
reason, and expressions that do not refer to their own name keep the
fold.
- Once a 'message' listener without a typed entry has decoded the
message, the listeners after it read that decoded value, so a typed
listener sees what an earlier untyped listener changed, as in Node.
- A union with a literal discriminator gets no typed message entry: its
checked conversion also tests the discriminator, which the message
readers do not.
Corpus 7361 checks a typed listener after an untyped one that changes
the message, and a listener on a discriminated union.
Typed-array loop versions applied only to for loops, so a scanner written
with while loops, such as `while (pos < end && bytes[pos] === 32) pos++`,
kept one bounds test per access. A while loop counter is now a number
local that the loop changes only by positive integer increments (`c = c +
k`, `c++`, `c = (c + k) | 0`) written as statements of its body, blocks,
branches and nested while loops. An access at `counter + d` after a
condition compared the counter below a loop-invariant bound, with no
increment since, is in bounds for the whole loop when the guard finds the
counter's entry value an exact integer of at least -d and the bound at
most the length minus the access's extent. A counter with a `| 0`
increment also needs its loops' bounds at most 2^31 minus its increments,
so it never wraps.
A fixed check (a statically ranged index) now needs an end below 2^31.
The counter of a loop to a number[]'s length whose length the loop may
change reaches 2^32 - 2, and its typed-array accesses got a guard no
typed array passes and a second copy of the loop.
`u === 0`, `u !== true` and `Object.is(u, x)` against a scalar arm tested
the union's tag inline but read its payload through scr_union_get_f64 or
scr_union_get_bool. The call passed the union's stack box to the runtime,
so LLVM could not promote the box to registers, and an element read of a
`number[]` compared with a number kept its value, tag and box in memory.
The comparison now loads the payload slot directly, as other union
extractions already do, and the box no longer escapes.
`for (let i = 0, p = 0; i < count; i++, p += 4)` kept both counters as
doubles: the counted-loop match takes a single declaration and a single
update. A counted loop now also accepts further counters declared in its
head and stepped by a constant in its update, when the condition does not
read them and the body does not write them. On body entry k such a counter
holds `start + step * k`, so its range follows from the driver's: k is at
most the driver's span over its step.
When that range can pass 2^53, the versioned loop tests
`|start| + |step| * span / |driver step| <= 2^52` at run time, and an
unproven start is tested for an exact safe integer, as the driver's is.
Nested update blocks (`i++, j += 2, k--`) are flattened. Byte windows and
dense loops still take only single-counter loops.