Skip to content

Train islands 6: withdrawn, the head is main's tree (the member goes back to its owner) - #3449

Closed
cramforce wants to merge 12 commits into
mainfrom
train/islands-6
Closed

cramforce wants to merge 12 commits into
mainfrom
train/islands-6

Conversation

@cramforce

@cramforce cramforce commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Withdrawn: this train carries no change

The head b72aa656d2 has the same tree as main a57eaacffd. The train's one member, fix/islands-values-batch-2 (d8db4fb85d), is reverted in d3ea93cbc8 together with the value matrix re-pin and the conformance list entry that depended on it. Merging this PR changes no file. It can be closed.

Why

The review bot's finding on packages/compiler/src/backend/llvm/dyn.ts:2991-2995 is real, and it is a regression of the member's iterator commit 38942dee84 ("An iterator that crosses a Node island keeps its identity and its position").

A program holds an island iterator with a generator type and as unknown. The typed holder goes away, the program enters the island again, and the untyped value is cast back to the generator type and stepped. Program: an eight-case repro over the tunes package of tests/corpus/node-island-iterator-identity; it is in the review thread.

CaseNodemainhead 482ca941df
not stepped, cast at onceintrointrointro
not stepped, one island call, castintrointrothrows
stepped once, one island call, castverseverseverse
not stepped, a turn of the loop and an island call, castintrointrothrows
8 stepped once and kept untyped, cast8 verse8 verse8 verse
70 stepped once and kept untyped, cast70 verse70 versethrows
200 stepped once and kept untyped, cast200 verse200 versethrows
typed holder kept, one island call, castintrointrointro

The throw is expected Generator at $, got Object from a Node island. The plain and the sanitized builds print the same.

The cause is the lifetime of the pin that pairs an island iterator with its native generator. nh_pin_dead in packages/runtime/src/scr_node_island_handle.c calls an NH_PIN_ITER pin dead once the table's capsule is the generator's last holder. An unknown holder holds the island handle, not the capsule, so the sweep at the next island entry (the young pins) or at 64 pins (every pin) drops the pair, and the checked cast finds no generator for the handle.

The fix is not a small one: the runtime cannot read how many references the generator itself holds on the island value. Both commits of the member go back to their owner; the for-in commit d8db4fb85d was proven only stacked on the iterator commit.

What had passed on the withdrawn head

For the record: the gate-only round, the island conformance lane, the value matrix, the weak-key tests, the island harness, 3,000 stress runs and 600 ThreadSanitizer runs all passed on 482ca941df or on its parent 78c93f0d1b (the two differ only in the value matrix's expected file). None of them holds an island iterator only as an untyped value across an island entry.

🤖 Generated with Claude Code

cramforce and others added 10 commits November 8, 2026 06:21
…ition

A checked-dynamic value that flows into a generator-typed slot (Generator,
IterableIterator, Iterator) goes through the iterator adapter, a new native
generator that steps its source. Beside a Node island that made three silent
wrong answers:

- An island iterator was another object in the program each time it crossed
  (`kept() === made` was false, a Map keyed by it missed), and the adapter
  went back into the island as a native export, a third object there
  (`same(made)` was false).
- A generator the program handed to the island came back wrapped in an
  adapter (`keep(own) === own` was false).
- The adapter read every value out of an island iterable before its first
  step, so the program's first `next()` ended the island's generator, and an
  island generator that never ends could not be read at all.

Now an island iterator the program gives a generator type is one native
generator for each such type. The island unit keeps the pair in its pin
table (NH_PIN_ITER, beside promises, errors, byte views and symbols) until
the table is the generator's last holder. In a program that uses an island:

- every conversion of a dynamic value into a generator-typed slot goes
  through `%iter.exact`, the helper arrays of generators already use: the
  value itself when it is the native generator of the slot's type (a
  generator back from the island), the generator already paired with an
  island iterator, else a new adapter, which is paired;
- a checked cast into a generator type, and the argument check of a native
  callback, take a paired island iterator as its generator;
- a generator's dynamic value is the island iterator it is paired with, as
  its handle (the backend boxes generators through
  scr_node_island_iter_ref), so it goes back into the island as the island's
  own object, compares equal to a handle of it and is the same key of a
  dynamic collection;
- the adapter steps an island value's own iterator in the island, one value
  at a time (scr_node_island_iterator).

The bridge protocol and the crossing paths do not change (the bridge ABI
stays 20). A program without islands compiles as before: the helper, the
boxing entry and the cast's question exist only where the program links an
island.

Arrays of generators beside an island stay refused. An island iterator
handed to a native callback whose parameter has a generator type still
throws the TypeError it threw unless the program already holds that
iterator with that type.

tests/corpus/node-island-iterator-identity pins both directions, the
untyped views, two generator types of one iterator, the shared position and
an endless island generator.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Regenerated by the matrix's update mode. In the generator, lazy generator,
endless generator, array iterator and iterator object programs with a
declared type, 187 observations that were pinned as wrong (77), unsupported
(79), throwing (25) or crashing (6) now match Node: identity through a
parameter, a field, a Map key and a round trip in both directions, reads in
order on both sides, and reads of a generator that never ends.

Eight observations that matched are pinned as unsupported: `for (k in v)`
over a generator-typed island value held as `unknown` (`i2n/U.forin` and
`clo-i2n/U.forin` of four programs). The value is the island's iterator
again where it is dynamic, and `for…in` over an island value is not
implemented yet: it throws by name where it used to loop zero times over
the adapter.

Co-Authored-By: Claude Code <noreply@anthropic.com>
`for (k in v)` over an object that lives in a Node island (an object, an
array, a function, an iterator, a Proxy, a package's exports) ended the
program with "for-in over this checked-dynamic kind is not supported
yet". It now visits the enumerable string keys of the object and of its
prototype chain, in Node's order. A key that is gone when the loop
reaches it is skipped and a key added during the loop is not visited, as
in Node.

The loop over a dynamic receiver keeps its form: a key snapshot when it
starts and a test of each key before its visit. In a program that uses an
island a final pass (island-for-in.ts) routes both to the island unit,
which asks Node for an island value and the native entries for every
other value. Programs without islands keep their IR.

Node answers through two helpers of the island host, loaded like its
other intrinsics, so the bridge and its ABI are unchanged. The key list
crosses as one JSON text. A chain of ordinary objects is walked by V8's
own for-in and tested with `in`. A Proxy is asked what for-in asks it,
when for-in asks it: `ownKeys` and `getPrototypeOf` when the loop starts,
the key's descriptor when the loop reaches the key, so its traps run in
Node's order between the loop's passes.

Kept loud by name: for-in over the namespace that require() answers for
an ES module with a default export. That object is Node's facade of the
module and carries an `__esModule` key the module's own namespace does
not have.

Corpus: node-island-for-in (kinds of value, views, changes during the
loop, Proxy traps, a callback, both directions) and
node-island-for-in-exports (a package's exports from a CommonJS program).

Co-Authored-By: Claude Code <noreply@anthropic.com>
Values of a Node island keep what they are: an iterator that crosses an
island keeps its identity and its position, and for-in over an island
value visits what Node visits.

The branch's two regenerations after this commit (the value matrix
re-pin and one line of the conformance list) are not merged: both files
are main's here and are regenerated on the train by their update modes.

Conflict: builtin-never-throw.ts (main's dyn.nativeSearchParamsIs and
the branch's two iterator entries, both kept). The island value matrix
is main's file.

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

Regenerated by the matrix's update mode on this tree. 438 pins leave
(3,476 to 3,038), every one because the cell matches Node now; no pin is
added and no pin changes its kind.

- 251 pins in 76 programs: for-in over an island value (`forin` and its
  `forin#…` observations), which was `unsupported`.
- 187 pins in the five iterator programs (generator, generator-lazy,
  generator-infinite, iter-array, iter-object): an island iterator keeps
  its identity and its position, so observations that were `wrong`,
  `unsupported`, `throws` or `crash` match.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Regenerated by the lane's update mode on this tree (453 passed, 68
skipped); one signature moves. The program now gets past its for-in over
an island object and stops three lines later at
`Object.getOwnPropertyNames(islandObject)`, which is refused by name
(SC2020). Loud before, loud after.

Co-Authored-By: Claude Code <noreply@anthropic.com>
No conflict; nine files from main, none the train changes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
One conflict, the island value matrix: main's file is taken here and the
matrix is regenerated by its update mode in the next commit. The island
host script, its generated header, the lowerer and the conformance list
merged without one.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Regenerated by the matrix's update mode after the merge of main, from
main's file: the same 438 pins leave (3,450 to 3,012), every one because
the cell matches Node now; no pin is added and no pin changes its kind.
251 are for-in observations over island values, 187 are in the five
iterator programs.

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

vercel Bot commented Nov 8, 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 8, 2026 9:02pm UTC

@cramforce

Copy link
Copy Markdown
Contributor Author

HOLD: do not merge yet. The review thread on dyn.ts:2993 (an island iterator cast back from unknown after its last generator-typed holder is gone) is being reproduced under Node, on main and on this head. This comment is replaced by the result, and by a fix or the removal of the unit if the finding is real.

@cramforce

Copy link
Copy Markdown
Contributor Author

HOLD, confirmed: do not merge head 482ca941df. The review thread on dyn.ts:2991 is a real defect and a regression of this train.

An island iterator that the program holds with a generator type and as unknown can no longer be cast back to the generator type once the typed holder is gone and the program has entered the island again. Node steps it, main steps it, this head throws expected Generator at $, got Object from a Node island. The simplest case is an iterator that was never stepped, followed by one island call. The outputs are the same in the plain and the sanitized build.

The pair of the island iterator and its native generator is dropped while an untyped holder of the value is alive. The details and the three outputs are in the thread. This comment is followed by a fix on this branch or by the removal of the unit.

🤖 Generated with Claude Code

cramforce and others added 2 commits November 8, 2026 13:01
Reverts the member's merge 0206a8e and the train's own commits that
depend on it: the island value matrix re-pin 482ca94 (1c65865 had
already given way to main's file in the merge 78c93f0) and the
conformance list 2ceab61. The tree is now the tree of main d3f840e,
the last main this train merged.

The member's iterator commit 38942de drops the pair of an island
iterator and its native generator while an untyped holder of the value is
alive: nh_pin_dead calls an NH_PIN_ITER pin dead once the table's capsule
is the generator's last holder, and an `unknown` holder holds the island
handle, not the capsule. A cast back to the generator type after the next
island entry then throws "expected Generator at $, got Object from a Node
island" where Node and main step the iterator. The for-in commit
d8db4fb was proven only stacked on the iterator commit, so both go
back to their owner.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The train's tree was main d3f840e's after the member was withdrawn, so
this merge has no conflict and its tree is main a57eaac's.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@cramforce cramforce changed the title Train islands 6: an island iterator keeps its identity, and for-in over an island value visits what Node visits Train islands 6: withdrawn, the head is main's tree (the member goes back to its owner) Nov 8, 2026
@cramforce

Copy link
Copy Markdown
Contributor Author

The head is now main's tree: b72aa656d2 = the member reverted (d3ea93cbc8) + a merge of main a57eaacffd. git diff origin/main is empty, so there is nothing to merge and nothing a merge could break. The two hold comments above were about head 482ca941df, which is no longer on this branch.

The finding in the review thread was real (Node and main step the iterator, 482ca941df throws); the member fix/islands-values-batch-2 goes back to its owner for a rework of the iterator pin's lifetime. The numbers of the jobs that were still running on 482ca941df when it was pushed, for the record: value matrix check mode 239 passed; weak keys 3 and 3 (plain, sanitized); compiler unit tests 1,915 in 150 files; diagnostics 420; island harness 229 in 16 files, handles and GC sanitized 22; stress 17 child programs and 13 worker programs, 50 runs each, plain and sanitized, all pass (3,000 runs); ThreadSanitizer 17 and 13 programs, 20 runs each, all clean (600 runs). None of these holds an island iterator only as an untyped value across an island entry, which is why they did not see the defect.

🤖 Generated with Claude Code

@cramforce

Copy link
Copy Markdown
Contributor Author

Closing: this train is withdrawn. The review finding was a real regression (an island iterator held as unknown threw on a checked cast after its typed holder was released; main steps through the same program), the member is reverted, and the branch now carries main's tree with no changed files. The member returns in a later islands train after rework; the reproduction and the three outputs are in the resolved thread above.

🤖 Generated with Claude Code

@cramforce cramforce closed this Nov 8, 2026

This branch was successfully deployed

1 active deployment
Preview — b72aa656 Deployed Nov 8, 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.

1 participant