Follows #3161 (the island value matrix), whose table pinned these three as silent wrong answers.
Three places where a program that uses a Node island compiled and then answered differently from Node without saying so. The matrix of #3161 pinned all three; its table loses 154 entries with this change.
1. A missing key through an index-signature view
A program type with a string index signature that receives an island value is a live view of it (#3093), and so is every other value of that type in the program. A keyed read of a view extracted the declared value type from the dynamic value, so:
const table: Record<string, number> = isl.lookup(); // the island's { a: 1 }
table[key] === undefined // Node true; native false, folded at compile time
const x = table[key]; // Node undefined; native TypeError: expected number at $, got undefinedThe same held for ??, typeof, a truthiness test, an optional chain and a destructuring without a default, and for the program's own Record<string, number> values once one island value made the type a view.
The native record of the same type reads V | undefined for a key it does not declare. A view's keyed read now means the same: maybeNarrow converts the dynamic value to V | undefined (the mechanism process.versions already uses), and the optional-read analysis that widens the bindings such a read flows into treats the view's read like the record's, in its two existing read predicates and in the object-pattern scan. Object.hasOwn on an island object asks Node (it threw "Own-property checks on this checked-dynamic kind are not supported yet").
This applies to a type that is a view because it receives island values, which ClassProtocols now remembers (receivesIslandValues). A dictionary view of the program's own records (a cast from unknown) has the same misreading for a literal key (view.nope === undefined is false), on main and without any island. The first version of this change fixed that too, and the zero-cost gate showed what it costs: 7 programs without islands change, one of them bench/record-untyped-writes. That is a change to the native dictionary view with a benchmark to measure, so it is left out here and recorded as a follow-up with the seven programs.
2. A program class instance inside an island
Island code saw an instance as an object with Object.prototype whose interceptor answered fields, methods and getters alike as own properties:
class Counter { n = 3; get double() { return this.n * 2 } set double(x) { this.n = x / 2 } inc() { return ++this.n } }
// in the island, with v the instance:
v.double = 80 // Node: n is 40. native: n stayed 3, silently
Object.hasOwn(v, "inc") // Node false. native true
Object.getPrototypeOf(v) === Object.prototype // Node false. native true
v.constructor.name // Node "Counter". native "Object"
JSON.stringify(v) // native ignored the class's toJSONNow the representative of an instance gets a prototype object, built once per class from the class's member table: methods as non-enumerable data properties, accessors as accessor properties, constructor a function with the class's name. The interceptor answers the instance's own properties only, and V8 finds everything the class declares the way it finds any class's members, with the instance as the receiver. So a setter runs, a write to a getter-only accessor throws in strict code, toJSON and toString are found, and in, Object.hasOwn, descriptors and Object.keys answer as Node does.
- Compiler: the member table (
nodeIsland.classMethods) also carries the accessor halves, under their lowered namesget:nameandset:name. - Runtime (
scr_node_island_export.c): an instance's export names its class's table and class name; a name the table holds is not an own property. - Bridge: the prototype comes from the host helper
classPrototype, cached per table.
A write or delete that the native object refuses is no longer reported as done: the host answers "refused", and the bridge throws Node's TypeError where the island code is strict and does nothing where it is sloppy. A frozen record used to keep its value silently under "use strict".
This changes the bridge ABI (one field in the export value, one status code). It is numbered 15: main is at 10, and four other open branches hold 11 to 14.
3. An island array of primitives that becomes a native copy: SC6007
An island array of numbers, strings, booleans or bigints (or arrays of those) stays the island's array while the program reads it through a const binding's members, an index, its methods or a spread, or passes it back. Used anywhere else it is copied into a native array, silently: an argument of a program function, a let, a return, a member of a program object, an awaited island promise, a for…of loop.
function total(xs: number[]) { xs.push(10); return xs.length }
total(isl.makeList()); isl.keptJoin() // Node "1,2,3,10"; native "1,2,3"Each such use is now an SC6007 divergence warning, recorded where the lowering emits the copy, so scriptc build prints it and coverage --fail-on=divergences fails on it. Not reported, because no program can tell the copy from the island's array: a spread, an array destructuring pattern, a template span; the argument of Array.from or structuredClone; a comparison with null/undefined, typeof, !, a condition. console.log(v) and JSON.stringify(v) are reported like any other argument (the compiler has no fact about which callees keep or write an argument); the hint is [...v].
The decision recorded for this (island-stress-values D1) is "warn, count, then refuse". The counts:
- pi, built with this compiler change on the fleet's integration tree (main plus the open pi PRs, not this branch): 8 sites in 4 files (
openai-completions.ts:982,:983;provider-composer.ts:187,:240;config-selector.ts:609,:726,:761;theme-schema.ts:118): two assignments into a program record, four values that flow on through??, one argument, onefor…of. Its lowering diagnostics are unchanged: 38 errors, the same 36 sites. - The island corpus: 13 sites in 2 of 102 programs (
6837-node-island-clone-and-arrays,6897-node-island-declared-arrays, both of which exist to pin the copy).
The refusal is not in this PR: it would add eight build errors to pi for reads of configuration arrays that never observe the copy. SCRIPTC_TRACE_ISLAND_COPIES=1 lists the sites on stderr, also for a build that fails before it reports warnings, which is how pi was counted.
Not changed, now documented
Recorded in the limitations page and pinned by the matrix:
- The prototype island code sees is flat: it inherits from
Object.prototypewhatever the class extends, and holds the inherited members itself. Itsconstructorcannot build an instance. Object.isFrozen,Object.isSealedandObject.isExtensibleanswer for the island's representative (V8 does not allow an object with interceptors to be non-extensible).delete v.fieldon a class instance is refused (a TypeError in strict island code,falsein sloppy code) where Node deletes the property. It answeredtrueand kept the field before.
Tests
tests/corpus/8939-node-island-index-view,8935-node-island-class-prototype,8931-node-island-array-crossing: each fixed observation and each use that is not a copy, against Node.tests/harness/node-island-array-copies.test.ts: one SC6007 warning per kind of copying use (snapshot), and none for the corpus program.tests/harness/island-value-matrix/expected.jsonregenerated: of its 5,420 pinned observations, 154 now match Node and leave the table (81 silent wrong answers, 39 native errors where Node answers, 30 run-time refusals, 4 early exits): an index-signature record 70, a class instance 43, a record with optional members 30 (Object.hasOwnof the island's object), a frozen record 6, an async iterable 5. No remaining entry changed status. Twelve observations ofasync-iterable.penter the table as native errors: that program used to exit where it made the value, so they were never reached; it now reads the value and fails later, atfor awaitand at reflective reads. Which of the three changes ended the early exit was not bisected.
Proof
All Sandbox rounds compare this change with #3161's head (172009316), the commit it was written on; the two commits were then rebased onto main (87 commits later, none in a file changed here).
- Zero cost.
bin/validate --base=172009316 --gate-corpus=all: PASS. 5,199 programs byte-identical (IR, LLVM and release executable), 101 fail to build on both sides, none differ, no runtime-only rows; the candidate compiler builds its own source. Compile time within tolerance: self -1.21 % [95 % CI -2.14 %, +0.01 %], fixtures +0.59 % [+0.13 %, +1.05 %], benchmarks -0.63 % [-2.26 %, +1.41 %]. - What the gate caught. The first round ran the view change for every checked-identity view and failed: 7 programs without islands differed (
bench/record-untyped-writesand six corpus programs, +0.10 to +0.58 % size). That is why section 1 is restricted to types that receive island values. - Lanes. both full lanes on the final commit, with the libnode pack: plain 21,837 passed, 0 failed; sanitized 18,607 passed, 0 failed. They include the value matrix against the regenerated table, the island suites and the island corpus. The first round, with the general form of section 1, failed one case in each lane:
dyncheck.test.ts"a record cast from unknown to a dictionary throws at its mismatched read", which pins the native dictionary view the restriction leaves alone. - Native bootstraps (the lowering changed): the plain and the sanitized bootstrap both pass on the final commit (they are among the lane jobs above), and passed in the first round too.
- On the rebased tree (8 vCPU Sandbox, Intel Xeon 2.50 GHz): the three corpus programs match Node plain and sanitized; the island corpus (224 differential and LLVM-differential cases),
node-island.test.ts, the new harness test and the matrix cells of every kind that moved pass;pnpm lint,pnpm format:check,pnpm test:ts7 --baselines-onlyandscripts/checker-identity.mjsfor the three programs are clean; the docs gate (NEXT_DIST_DIR=.next-check pnpm check) passed before the rebase.
🤖 Generated with Claude Code
Activity
12Brought up to date with main (d2bbe4d99): merge commit 13c323e62, then 800f5892d (bridge ABI) and 372f1dee5 (value matrix). Head 372f1dee5.
What conflicted. packages/runtime/src/scr_node_island.h and scr_node_island_handle.c: both sides appended to the ScrNiDynOps table (#3234 added function_identity and function_of_identity, this branch has_own). Both are kept, main's first. The table is internal to the runtime, so the order has no meaning outside it. Everything else merged without conflict, and the generated island host header is unchanged by regeneration (node scripts/node-island-host.mjs --check).
Bridge ABI: 15, not 13. The description says 13. That number and 14 are claimed by two other open branches, and main is at 10, so this change takes the next free number (scr_node_bridge.h, node-islands/libnode.ts, the expected text in node-island.test.ts). A tree that carries this with another bridge change bumps once more.
The island value matrix is regenerated, not merged. tests/harness/island-value-matrix/expected.json was written by SCRIPTC_ISLAND_MATRIX=update on the merged tree. Against the textual merge it differs by twelve entries: the refused:SC2020 observations of x.constructor (class.control, class.p, class.w, class.u, error-class.control), which compile on main since #3165. #3298 removes the same twelve on main. If #3298 lands first, this file is regenerated once more on top of it (no further change is expected).
Re-run on the merged tree (Linux x64 Sandboxes, 32 vCPU, Intel Xeon @ 2.90GHz, Node 24.22.1, a libnode pack built from this tree's bridge):
- Workspace build,
pnpm lint,pnpm format:check,pnpm test:ts7 --baselines-only, the IR codec and field table checks: pass. - Zero-cost gate (
--gate-corpus=all,800f5892dagainstd2bbe4d99): PASS. 5,288 programs byte-identical, 106 fail to build on both sides (they need a libnode pack, which the gate does not have), none differ, no runtime-only row. Compile time within tolerance: benchmarks -0.14% [-1.21%, +1.03%], self +0.65% [-0.76%, +1.02%], fixtures +0.22% [-1.08%, +1.52%]. The candidate compiler builds its own source. The commit after it changes only the matrix table. - Island unit tests (
packages/compiler/src/node-islands,packages/libnode-common, the runtime ABI and header tests,program-facts): 186 pass. - Island harness, all twelve
*island*.test.tsfiles (node-island-array-copiesand the value matrix against the regenerated table among them): 358 pass plain, 358 sanitized. - Island corpus (
differential.test.tsandllvm-differential.test.ts,-t island, with the three programs this PR adds): 230 pass plain, 230 sanitized. - Island diagnostics snapshots: 7 pass.
- Island conformance lane: 12 entries fail, the same twelve that fail on
mainatd2bbe4d99with nothing applied (its list is stale; #3293 refreshes it, andpackages/semverhas moved once more since #3293 was generated). None is from this branch.
Not re-run on the merged tree: the full test lanes, the two native bootstraps, the docs gate and the pi build. The resolution changed no lowering by hand.
🤖 Generated with Claude Code
The latest updates on your projects. Learn more about Vercel for GitHub.