Skip to content

Follow the value in island method calls and serialize island objects in JSON.stringify - #3124

Open
cramforce wants to merge 28 commits into
mainfrom
fix/island-runtime-bugs
Open

cramforce wants to merge 28 commits into
mainfrom
fix/island-runtime-bugs

Conversation

@cramforce

@cramforce cramforce commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #3029 (ctate/islands-value-island-a1a34a): this branch merges that PR's head and builds its JSON.stringify part on #3029's walk, so it must merge after #3029. Until then the diff here shows #3029's commit as well; review that one there.

Two valid programs compiled and then threw at the Node island boundary. Both now match Node. Both fixes are island-only: the lowering runs only in a program with islands, and the runtime code is in island units or under SCR_NODE_ISLANDS in the existing node-island build of scr_json.c.

1. A method call on a native value typed by an island's declarations

import { send, type Input } from "pkg";          // type Input = Array<InputItem>, declared by the island
function convert<T extends string>(model: T, n: number): string {
  const messages: Input = [];                     // a native array: the island's alias only annotates it
  for (let i = 0; i < n; i++) messages.push({ type: "out", call_id: model + i, output: "o" + i });
  return send(messages);
}

Before: Uncaught TypeError: scriptc: a call needs a Node island value, exit 1. Node prints the JSON.

The island lowering claims recv.m(args) whenever the receiver's declared type is island-declared, wherever the value lives. Every other operation follows the value at run time (scr_node_island_get, _set, _invoke, _apply and _construct take the checked-dynamic path for a non-handle). A call did not, in two cases:

  • An argument that runs code. JavaScript reads the member before the arguments, so the call lowers to nodeIsland.get and nodeIsland.applyTo, and scr_node_island_apply_to assumed the held member was a native closure or an island function. A builtin member of a native value (push, a Map's set) has no function value, so the read answered something not callable and the call reached the bridge. Nothing here is specific to generics or to push: any member, any native receiver, any argument that is not inert.
  • A native class instance, with any arguments. const sink: Sink = new Counter(); sink.take(5) threw take is not a function: the runtime's by-name call has no table of class methods, which only the class dispatch pass knows.

islandMethodCall lowers the call in two arms:

  • An island receiver takes the bridge exactly as before (nodeIsland.invoke, or the member read first and nodeIsland.applyTo after the arguments).
  • A native receiver takes the ordinary checked-dynamic method call, the dynInvoke node the rest of the compiler lowers value.m(args) to. So class methods, generator steps and iterator helpers are dispatched by the passes that rewrite that node, and the native arm is the same code a value typed any gets.

The arguments cross once, into hidden locals both arms read. A member that is a function value (a closure on a native object, an island function a native object holds) is still called as it was read (nodeIsland.calls). islandCaughtRead already routes a caught value with the same nodeIsland.isHandle test. A call that spreads an argument has no fixed argument list for dynInvoke and keeps the runtime's routing; nodeIsland.applyTo now carries the member name, so a native receiver's builtin member is called by name there too.

2. JSON.stringify reaching an island object: three things on top of #3029's walk

#3029 already serializes an island value inside the stringify walk (the object's kind from an is_array slot in ScrNiDynOps, keys and members through scr_dyn_obj_keys and scr_dyn_obj_read, an array's length read in Node, toJSON with the real key, omitted functions and symbols, cycles by the handle's referent). This PR used to carry a second walk for the same thing. It no longer does: the branch merges #3029 (through 3787c75b9), deletes its own walk (scr_json_island_write, its is_array slot, its member-read and toJSON branches, its symbol test and its entry in the compact walker) and keeps only what #3029's walk does not do.

I ran this PR's 37 JSON.stringify cases against #3029 merged with main, compiled and under Node 24.22.1. 28 match as they are. Three behaviours account for the other 9, and each is a small addition under SCR_NODE_ISLANDS on top of that walk:

casesprogramNode#3029 + mainhere
9 to 14JSON.stringify({ a: "x", n: 1, label: "l" }, islandFn) and (…, islandArray), on island and native values, with boxed String/Number keys{"a":"X","n":1,"label":"L"}, {"label":"l","n":1}the replacer is ignored: {"a":"x","n":1,"label":"l"} (the same on main, silently)matches
19, 20an island object holding new Number(1.5), new String("s"), new Boolean(true), JSON.rawJSON("12345678901234567890")1.5, "s", true, 12345678901234567890{}, {"0":"s"}, {}, {"rawJSON":"…"}matches
32a cycle through two instances of an island class Link… object with constructor 'Link'… object with constructor 'Object'matches
  • Replacers. scr_json_stringify_value tested the native kinds of the replacer only, so an island function or an island array was dropped. It is honoured now, for island and native values, and an island property list reads boxed String and Number keys with their ToString (the review fix already on this branch).
  • Internal slots. One host helper, scriptc:json, reached through a json_query slot, answers the JSON text of a boxed primitive or a raw JSON object; the walk asks before it enters an island object that is not an array.
  • Constructor names. The walk marks the entry it pushes for an island object, and the circular-structure TypeError asks the same helper for its constructor's name.

Zero cost: every line this PR adds to scr_json.c is under SCR_NODE_ISLANDS, the unit's node-island build, so against #3029 the default object is unchanged.

Evidence

  • tests/corpus/8885-node-island-native-receiver-calls/: the reported push; other array members with calls, operators and spreads as arguments; a native Map; a native object with a closure member; a class instance and a subclass through a binding and a parameter, with inert arguments and arguments that run code; the same calls on island objects; Node's TypeError for a member that is not a function, after the arguments ran.
  • tests/corpus/8886-node-island-json-stringify/: handle roots (object, any, array, index-like keys); gaps; native and island replacers; native and island property lists; a Date's toJSON; omitted undefined and function members; a getter; boxed primitives, a symbol and a function inside an array; a raw JSON object; toJSON keys as a root, a record member and an array element; handles inside a native record, array and dictionary; a native value held by an island object; four circular shapes; util.format's %j.
  • Order baselines and corpus cost records for both.
  • tests/harness/island-value-matrix/expected.json is regenerated for this tree. Merging islands: a value an island made passes the checked conversions #3029 with main changes how most kinds of value cross, and the matrix main gained after islands: a value an island made passes the checked conversions #3029 branched pins the old answers: 107 of its 239 programs fail on islands: a value an island made passes the checked conversions #3029 merged with main alone, and the same 107 here before the regeneration. Against main the table goes from 4213 entries to 3515. All but two of those changes are islands: a value an island made passes the checked conversions #3029's: islands: a value an island made passes the checked conversions #3029 merged with main regenerates to 3517 (857 entries gone, 161 new, 171 with another status). This PR's part is two entries gone and none new (map-obj.p, n2i2n/N.read.getByKey, with and without the island callee: threw, now matches Node). When islands: a value an island made passes the checked conversions #3029 records its own table this file will conflict and is regenerated again.

Since the merge with main (adb045cfe, main 326aef394)

Gates

Proved against #3029 merged with main (4775581e3 = main 716cb8626 + #3029 3787c75b9), so the comparison is this PR's own delta. Head 2ba11f120; the gate ran on 70d98a984, which differs from the head only in the regenerated matrix table. Sandboxes: Intel Xeon 2.50 and 2.90 GHz.

  • Zero-cost gate, --gate-corpus=all: PASS. 5247 identical, 103 both-fail (the island programs: the gate runs without a libnode pack, on both sides), 0 differing, 0 runtime-only rows, over the benchmarks, the whole corpus, the fixtures and the self-hosted compiler ("candidate compiler source builds with the candidate"). Bench geomean +0.21 % (95 % CI −0.94 %, +1.01 %). The runtime pack differs only in the node-island builds of scr_json.c and in scr_node_island.c / scr_node_island_handle.c. An earlier round on the first merge (b92abbd93 against main + islands: a value an island made passes the checked conversions #3029 2d07592af) gave the same counts.
  • Island tests, with a libnode pack built from this tree's bridge: every node-island and island-value corpus program in the differential and LLVM-differential suites, plain 174/174 and sanitized 174/174; the eight island suites and the value matrix, 344/344 (matrix 239/239 pinned); the ir, node-islands and host-header unit tests, 874/874; pnpm lint, pnpm format:check, pnpm test:ts7 --baselines-only.
  • The 37-case JSON.stringify table: 37/37 here, 28/37 on islands: a value an island made passes the checked conversions #3029 merged with main.
  • Earlier on this branch, before the restack (the compiler sources of the two-arm call are unchanged since): the full lanes and both native bootstraps at 5de1aa70b, the gate and both bootstraps again for the review fixes at 47e4b09e7, and a pi before/after pair with identical diagnostics (62 and 62). The stacked Give class instances their own class's methods in Node islands, and call them with spread arguments #3198 reruns the bootstraps on this tree plus its own commits.
  • Size of the two-arm call lowering, six island corpus programs built with and without it (release, linux-x64): four unchanged, 8810-node-island-sdk-request-values +88 B, 6834-node-island-package-callbacks +2,496 B (+0.27 %), 6848-node-island-handle-ops +23,800 B (+2.6 %). Programs without islands are byte-identical (the gate).

Still failing, found on the way (not in this PR)

🤖 Generated with Claude Code

ctate and others added 2 commits November 5, 2026 03:47
A checked conversion accepts an island handle. A record reads each field
through the handle, as Node reads a property; an array, tuple or
index-signature record takes a native view of the value, with the view
released at every exit. JSON.stringify, Object.keys and object spread and
rest read the island object's own enumerable properties in Node, so a
toJSON method, an accessor and a function-valued member keep their
meaning. A program without islands emits the same code as before.
…in JSON.stringify

Two valid programs compiled and then threw at the Node island boundary.

A method call on a native value typed by an island's declarations

The island lowering claims `recv.m(args)` whenever the receiver's
declared type is island-declared. The type does not say where the value
lives: an island alias or interface may annotate a native array, Map,
object or class instance (`const messages: Input = []`). Every other
operation already follows the value at run time, but a call did not in
two cases:

- When an argument runs code, the call lowers to a member read
  (`nodeIsland.get`, before the arguments, as JavaScript orders it) and
  `nodeIsland.applyTo`, which assumed the held member was a native
  closure or an island function. A builtin member of a native value
  (`push` on an array, `set` on a Map) has no function value, so the read
  answered something that is not callable and the call threw
  "scriptc: a call needs a Node island value". The reported shape is a
  generic instance pushing `{ ..., call_id: model + i }`; nothing in it is
  specific to generics or to push.
- A native class instance threw "take is not a function" for every call,
  inert arguments included: the runtime's by-name call has no table of
  class methods, which only the class dispatch pass knows.

`islandMethodCall` now lowers the call in two arms. An island receiver
takes the bridge exactly as before. A native receiver takes the ordinary
checked-dynamic method call, the `dynInvoke` node the rest of the
compiler lowers `value.m(args)` to, so class methods, generator steps
and iterator helpers are dispatched by the passes that rewrite that
node. The arguments cross once, into hidden locals both arms read, and a
member that is a function value is still called as it was read
(`nodeIsland.calls`). `islandCaughtRead` already routes a caught value
this way. A call that spreads an argument keeps the runtime's routing:
`nodeIsland.applyTo` carries the member name, so a native receiver's
builtin member is called by name there too.

JSON.stringify reaching an island object

The stringify walk knew native containers only, so an island handle,
as the root or anywhere inside a native value, threw "JSON.stringify of
a runtime handle is not supported yet". The walk now serializes an
island object as it does a native one, with V8 answering every question
about the object through the island ops slot: IsArray, the length, the
own enumerable string keys in V8's order, each member's Get, and its
toJSON, called with the real key. The replacer runs with the real
holder, a property list reads the listed keys, and a native value an
island object holds is walked as the native value it is. A replacer
that is an island function or an island array is honoured, for native
values too: scr_json_stringify_value tested the native kinds only and
dropped it silently, so `JSON.stringify(nativeValue, islandReplacer)`
printed the unreplaced text. One host helper, `scriptc:json`, answers
what V8 keeps in internal slots: the text of a boxed primitive or a raw
JSON object, and the constructor name the circular-structure TypeError
prints.

All of the walk is under SCR_NODE_ISLANDS, the existing node-island
build of scr_json.c, so the default object is unchanged; ScrNiDynOps
gains two slots at its end.

Tests

- tests/corpus/8885-node-island-native-receiver-calls: the reported
  push, other array members with calls, operators and spreads as
  arguments, a native Map, a native object with a closure member, a
  class instance and a subclass through a binding and a parameter, the
  same calls on island objects, and Node's TypeError for a member that
  is not a function.
- tests/corpus/8886-node-island-json-stringify: handle roots, gaps,
  native and island replacers, native and island property lists (on
  island and on native values), a Date's toJSON, omitted members, a
  getter, boxed primitives, a raw JSON object, toJSON keys, handles
  inside native records, arrays and dictionaries, a native value held by
  an island object, four circular shapes, and util.format's %j.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Nov 7, 2026 4:39pm UTC

Comment thread packages/runtime/src/scr_json.c Resolved
cramforce and others added 3 commits November 6, 2026 20:45
…all them with spread arguments

Two failures of a native class instance held in a binding typed by a Node
island's interface.

A class instance crossing into an island without its methods

An island reads a class instance's prototype methods from a table. The
table was registered by the crossing expression: an expression whose
static type is a class was wrapped with nodeIsland.classMethods, and the
runtime kept the first table it was given for each capsule type key. So:

- An instance that reached the island any other way (a binding typed by
  the island's interface, `unknown`, a member of a record, an array or a
  Map, the result of a program callback) had no methods unless its class
  had crossed directly earlier in the run. The island threw "s.take is not
  a function", or silently saw `typeof s.take === "undefined"`.
- The table was the static class's. A subclass instance held as its base
  class ran the base class's method in the island (7 where Node prints 14)
  and lacked the methods only the subclass declares.
- The export table keeps an object's first capsule, so the static type an
  object first crossed under decided its methods for the rest of the run.

The table now comes from the class the object was constructed as, and is
looked up when the island first reads a method of the object. A program
that holds island values has one function, %island.classMethods, from a
capsule to that table: it tests the capsule's key and then the instance's
class, most derived first, over every class a checked-dynamic value can
hold (the classes the class dispatch pass found boxed, with their
constructed subclasses). %main registers it once (nodeIsland.classTables)
and the export keeps the answer. Nothing is registered at crossings:
nodeIsland.classMethods and the per-key table are gone, and the
shared-module class registration keeps only its constructor helper
(nodeIsland.classConstructor).

A method call with a spread argument on a class instance

`sink.take(...values)` threw "take is not a function". The native arm of
an island method call is the dynInvoke node, which has a fixed argument
list, so a call with a spread kept the runtime's call by name, and the
runtime has no table of class methods. The spread form now lowers its
native arm the way the ordinary tier lowers such a call: the member read
(a dynKeyGet with a literal key, which the class dispatch pass answers
with the method value) before the arguments, and a dynCall that spreads
the argument pack. That serves the receivers whose members compiled code
dispatches (nodeIsland.isCapsule: a class instance, a generator). Other
native receivers keep the runtime's call by name, which takes a list of
any length and is what dynInvoke ends in for them; the ordinary tier has
no method values for a native Map or Set.

Programs without island values are unchanged: the function and its
registration exist only when the IR holds an island operation.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…nd interface

A generator is the other kind of native value whose members compiled code
dispatches. Typed by an island's interface and stepped with
`stepper.next(...steps)`, it takes the same native arm as a class
instance: the member read and a call that spreads the argument pack.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The class dispatch pass builds the island method tables only for a program
whose IR holds an operation on a Node island value. The calls that copy
JSON both ways (nodeIsland.callSync, nodeIsland.call) do not count: no
value crosses by reference through them.

Co-Authored-By: Claude Code <noreply@anthropic.com>
cramforce and others added 4 commits November 6, 2026 23:09
Main at 4fc60de. Conflicts, all of the "both sides added an entry at the same place" kind, resolved by keeping both: the `ScrNiDynOps` table and its initializer (main added `object_tag`, this branch `is_array` and `json_query`), the bootstrap's `load` (main added `scriptc:clone` and `scriptc:objectTag`, this branch `scriptc:json`), and the generated `scr_node_island_host.h`, regenerated with `scripts/node-island-host.mjs`.

Co-Authored-By: Claude Code <noreply@anthropic.com>
eb7546b made `scr_node_island_apply_to` refuse a native receiver's member that the arguments turned into a function: JavaScript read the member before them, so the call is the TypeError for that value. That covers a call that spreads its arguments. A call with a fixed argument list does not go through it: compiled code asks `nodeIsland.calls` and, for a native receiver whose member was not a function value, makes the by-name call itself, which reads the member again and called the function the arguments had stored (`late.take(swap(late, fn, 3))` returned `fn`'s result where Node throws `late.take is not a function`).

`nodeIsland.calls` now takes the member's name and the callee's text, makes the same second read after the arguments ran, and answers false with that TypeError pending; it joins the may-throw set so the pending check follows it. Members with no function value before or after (an array's push, a class method) and members that are still not functions take the by-name call as before.

Corpus 8885 pins the fixed-argument forms: a function replaced by another function (the first is called), a number replaced by a function and a missing member added (both throw, naming the callee), and calls whose arguments leave the member alone.

Found by the Vercel review bot on #3124.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Brings in #3124's new head: the fix for the bot's JSON thread and the spread call (eb7546b), main merged in at 4fc60de, and the same refusal for a call without a spread (47e4b09).

Three conflicts, all where main's island crossing changes (#3005, #3093, which #3124 now contains) meet this branch's class tables:

- `toIsland` (lower-node-island.ts): main's `crossByReference` wrapped a class instance with `withClassMethods`, which this branch deletes (an instance's methods come from its own class's table when the island first reads one). Kept main's structure, with `crossByReference` only boxing the value.
- `scr_ni_host_export_has` (scr_node_island_export.c): main's new branch for an inherited name calls `ni_class_method`, whose signature this branch changed. It now passes the export id and clears an exception the resolver left, like the branch beside it.
- `scr_node_island.h`: the two declarations side by side (`scr_node_island_calls` with its new arguments, `scr_node_island_is_capsule`).

Co-Authored-By: Claude Code <noreply@anthropic.com>
cramforce and others added 3 commits November 6, 2026 23:52
#3029 (ctate/islands-value-island-a1a34a) already serializes an island
value in JSON.stringify inside the existing protocol walk: the island
object's kind from an `is_array` slot in ScrNiDynOps, its keys and
members through scr_dyn_obj_keys and scr_dyn_obj_read, an array's
length read in Node, its toJSON with the real key, and cycles by the
handle's referent. This branch carried a second walk for the same thing
(scr_json_island_write, its own `is_array` slot with another signature,
a member-read branch, a toJSON branch and an entry in the compact
walker). Both together would leave two live paths and one struct slot
declared twice, so this merge deletes ours and builds on #3029's. This
PR is now stacked on #3029 and merges after it.

What stays is what #3029's walk does not do, each a small addition on
top of it under SCR_NODE_ISLANDS. Measured with this branch's 37
JSON.stringify cases against #3029 merged with main: 27 match, and
these four behaviours account for the other 10.

- A replacer that is an island function or an island array is honoured
  (scr_json_stringify_value tested the native kinds only and ignored
  it, for island and native values alike), with the boxed String and
  Number keys of an island property list.
- A boxed primitive or a raw JSON object an island made writes V8's
  text. Walked as an object it printed `{}`, `{"0":"s"}` or
  `{"rawJSON":"…"}`. The `scriptc:json` host helper answers it through
  the `json_query` slot, which is kept; the walk asks before it enters
  an island object that is not an array.
- An island symbol is omitted like a native one: `[Symbol()]` inside an
  island object printed `[{}]` where Node prints `[null]`.
- The circular-structure TypeError names an island object's constructor
  (`'Link'`, not `'Object'`): the walk marks the entry it pushes for an
  island object.

Corpus 8886 gains a line for the symbol, with a Map, a Set, a hole,
NaN and -0 beside it. The receiver two-arm call and its tests are
untouched by the merge.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
#3124 now merges #3029 and builds on its JSON.stringify walk for island
values, and has current main. This merge had no textual conflicts.

Co-Authored-By: Claude Code <noreply@anthropic.com>
cramforce and others added 3 commits November 7, 2026 01:11
Merging #3029 with main changes how most kinds of value cross an island
boundary, and the value matrix main gained after #3029 branched pins the
old answers: 107 of its 239 programs fail on #3029 merged with main, and
the same 107 on this branch.

The table is regenerated here for this tree (SCRIPTC_ISLAND_MATRIX=update,
then a pinned run: 239 of 239). Of its change against main, 4213 entries
to 3509, all but two entries are #3029's and are the same on #3029 merged
with main alone, whose own regenerated table has 3511 entries: 856 gone,
154 new, 172 with another status. This PR's part is two entries gone and
none new: reading a native Map-holding record back by key through an
island (map-obj.p, n2i2n/N.read.getByKey with and without the island
callee) threw and now matches Node.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Brings the island value matrix recorded for #3124's tree.

Co-Authored-By: Claude Code <noreply@anthropic.com>
#3029 gained a commit (3787c75) after this branch merged it: its
walk now omits an island symbol like a native one, a toJSON that is an
island function on a native object is called, a handle is tracked by the
value behind its box in the conversion visit, and Array.isArray follows
a proxy in the bridge.

The symbol was one of the four things this branch added on top of that
walk, so the addition here (scr_json_island_symbol and its test in
scr_json_omitted) is deleted and both conflicts resolve to #3029's
side. Three remain on top: island replacers, the text of a boxed
primitive or a raw JSON object, and the constructor name in the
circular-structure TypeError.

Co-Authored-By: Claude Code <noreply@anthropic.com>
ctate and others added 2 commits November 7, 2026 04:06
#3029's new commit changes a few more cells (a proxied array, symbols).
Regenerated for this tree and rerun pinned: 239 of 239. The table has
3515 entries; #3029 merged with main alone regenerates to 3517, and
the difference is the same two entries as before, both gone here
(map-obj.p, n2i2n/N.read.getByKey with and without the island callee).

Co-Authored-By: Claude Code <noreply@anthropic.com>
cramforce and others added 2 commits November 7, 2026 02:30
#3124 merged #3029's review fixes (3787c75) and recorded the island
value matrix for its tree.

Co-Authored-By: Claude Code <noreply@anthropic.com>
35384b2 recorded the island value matrix for this branch before
#3124 took #3029. Both sides rewrote the table; this merge keeps
#3124's, and the next commit records it for this tree.

Co-Authored-By: Claude Code <noreply@anthropic.com>
cramforce and others added 3 commits November 7, 2026 04:41
ScrNiDynOps (scr_node_island.h and its initializer): main appended
function_identity and function_of_identity (#3234), this branch
json_query. All three are kept, main's first.

scr_json_stringify_value: #3234 made an island function a replacer
function, and this branch had made an island function or an island array
one. After the merge the branch's function arm could not be reached, so
main's block takes the function and the branch's block the array alone.

The island value matrix is recorded again on the merged tree: 3,515
entries become 3,499. The 16 that are gone now match Node: the twelve
x.constructor reads main compiles since #3165 (pinned refused:SC2020;
main's own table still has them, #3298 removes them) and four identity
facts of an island function that #3234 fixes. None is new and none
changed status.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Main's #3293 refreshes the island conformance list. Nothing conflicted.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Regenerated on top of main's refreshed list (#3293), with a libnode pack
built from this tree's bridge: 85 entries become 67. Eighteen programs
now match Node, most of them through JSON.stringify of an island value
and a value an island made passing the checked conversions; six fail at
a later point than before. None is new.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Main's #3318 regenerates the island value matrix for main's tree. This
branch's table was recorded on this branch merged with the same compiler
and runtime sources (main has changed two test lists since), so it is
kept as recorded; it already has #3318's two changes (the x.constructor
reads, and async-iterable.p without the S.make lines that used to end
the program).

Co-Authored-By: Claude Code <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

Merged main into this branch, three times as main moved while the proof ran: 25914c21f (main 50bcfe1b3), ba2e57376 (97d372266, the conformance list) and adb045cfe (326aef394, the matrix re-pin), with one commit of its own, 90109f149 (the conformance list for this tree). Head: adb045cfe.

What conflicted.

What did not conflict and needed a decision. #3234 made an island function a replacer function in scr_json_stringify_value; this branch had made an island function or an island array one, a few lines below. After the merge the branch's function arm could not be reached. Main's block now takes the function and the branch's block the island array alone: one test per case, main's kept. The 37-case JSON.stringify table is 37 of 37 on the merged head, and 8886-node-island-json-stringify passes plain and sanitized.

Island conformance list. Main refreshed its list (#3293) while this was in proof. This tree moves entries of its own (most of them through #3029's conversions and this PR's JSON.stringify), so the list is regenerated on top of main's: 85 entries become 67, 18 programs now match Node, 6 fail at a later point than before, none is new. One of the 18 is main's own: packages/semver > island:semver is stale on main since semver runs in an island by default, so main's lane is red on that cell.

Re-run on the merged head (Linux x64 Sandboxes, Intel Xeon 2.90 GHz, libnode pack built from this tree's bridge; the compiler and runtime sources are those of 25914c21f, the later commits change three test lists):

  • Zero-cost gate, --gate-corpus=all, against main 50bcfe1b3 merged with islands: a value an island made passes the checked conversions #3029 (3787c75b9), as the description's gate was: PASS, 5,295 identical, 109 failing on both sides, none differ, no runtime-only row. Compile time: benchmarks +0.35% [−1.12%, +1.48%], the compiler's own build +0.87% [−0.54%, +1.75%], fixtures +3.06% [+1.20%, +4.92%] (the gate calls it inconclusive; the description's round had +2.42% [+0.28%, +4.54%] for the same row).
  • Island unit tests: 186 passed. The island suites and the value matrix: 354 passed, plain and sanitized (matrix 239 of 239 against the recorded table). Island diagnostics: 7 passed.
  • Every island corpus program, differential and LLVM-differential: 234 passed plain, 234 sanitized.
  • Island conformance lane against the regenerated list: 413 passed, none failed.
  • pnpm lint, pnpm format:check, pnpm test:ts7 --baselines-only, the generated island host header and IR codec checks: clean.

Not run on this head: the full lanes and the native bootstraps (the two-arm call lowering is untouched by the merge; both ran before the restack, as the description says).

What #3029 still needs (not touched here; this branch contains its head 3787c75b9):

cramforce and others added 3 commits November 7, 2026 07:04
lower-node-island.ts: main's #3254 replaced stripTypeWrappers by the
shared withoutParenthesesTypeWrappers and dropped its import, next to the
import this branch added. The branch's import stays, the removed one goes.

scr_node_island_export.c: both sides added fields to an export slot,
main the twin class state of #3271 (twin, attachment, adopts, adopted),
this branch a class instance's method table (methods_known, methods).
Both are kept; each side's initialization and release of its fields
merged without a conflict and is intact.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Main's #3331 drops the stale semver entry of the island conformance
list. The list recorded for this tree already lacks it (semver runs in
an island by default here too), so it is kept as recorded.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The island value matrix conflicted again: main's #3335 changed 43 of its
lines (a read of a missing member that the types decide now throws
where it was wrong). The table is recorded again on the merged tree:
3,499 entries, the same 43 facts change status here (wrong to throws),
none is new and none is gone.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

Main moved under this branch four more times after my comment above, and the maintainer merged #3198 into it (a55497183). Head now: 9cdb212dd, merged with main 23f1b176f. This is my last push here; I am not following main's matrix re-pins further until #3029 lands.

What the three later merges resolved.

Re-run (Linux x64 Sandboxes, Intel Xeon 2.90 GHz, libnode pack from this tree's bridge, ABI 13):

  • On 9cdb212dd itself: island unit tests 226 passed; the island suites and the value matrix 374 passed (matrix 239 of 239 against the table recorded on this tree); island diagnostics 7; island corpus, differential and LLVM-differential, 264 passed. pnpm lint, pnpm format:check, pnpm test:ts7 --baselines-only, generated header and codec checks: clean.
  • Sanitized, on 5b65065a1 (one merge of main earlier; the two trees differ by five main commits and the 43 table lines): island suites and matrix 374 passed, island corpus 264 passed. The conformance lane last ran to the end on b0d0a9c89 (two merges of main earlier): regenerated there, the list did not change, and 413 passed against it. A run on 5b65065a1 was cut off by the Sandbox's time limit and has no result.
  • The peel-helper guard and runtime-pack-matrix.test.ts pass on b0d0a9c89; on main 82475b871 and later the guard fails for member-call-receivers.ts#unwrap, which is main's.
  • Give class instances their own class's methods in Node islands, and call them with spread arguments #3198's own delta, measured before it was merged in (its tree against this branch's adb045cfe), zero-cost gate --gate-corpus=all: PASS, 5,295 identical, 111 failing on both sides, none differ, no runtime-only row. Island suites 354 and island corpus 242 passed on that tree, plain and sanitized; matrix and conformance regenerated there: unchanged.

Not run on this head: the zero-cost gate (it ran on ba2e57376 against main + #3029: PASS, 5,295 identical, and on #3198's delta, above; the three later merges resolve an import line, slot fields of an island-only unit and two test lists), the sanitized suites on the last merge, the full lanes and the native bootstraps.

What #3029 still needs is unchanged from the comment above: an answer for its 244 differing programs and 3,301 runtime-only rows against plain main, and its own matrix and conformance tables once it takes main.

This branch was successfully deployed

1 active deployment
Preview — 9cdb212d Deployed Nov 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants