Skip to content

Island handle operations, native subclasses of island classes, arrays and collections by reference (NI-5b/5c/5d) - #1903

Merged
ctate merged 24 commits into
mainfrom
ni/i6-subclasses
Oct 28, 2026
Merged

ctate merged 24 commits into
mainfrom
ni/i6-subclasses

Conversation

@cramforce

@cramforce cramforce commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1902. Sixth of nine Node islands PRs.

What it does

Handle operations.

  • Island objects run their own Symbol.toPrimitive/toString/valueOf for String(), templates, + and numeric conversion. This uses a new to_primitive handle hook in scr_dyn.h; only handle classes that set it see a change.
  • Island modules load with no CommonJS parent, as Node's ESM loader loads CommonJS, so lazy-traps and node24-commonjs-metadata now match.
  • $ORIGIN/ load paths resolve against the executable's directory.
  • Arrays, tuples, Maps and Sets that the island does not declare stay handles wherever the program uses them through island operations, including const bindings. Any other use is SC8010 at that use.
  • Compound assignment, optional chains (?.) and spread now work on island values.

Native subclasses (NI-5b, 5d). class X extends IslandBase at module top level is created in Node with native methods, accessors, field initializers, super(...) and super.m(). When the base is a deferred island class, the subclass is created lazily on first use, so it does not start Node at boot. SC8011 now also covers classes of compiler-managed island packages, for the forms that are still not supported.

By reference (NI-5b).

  • Native arrays and tuples cross as Proxies over the native array, so Array.isArray is true.
  • Typed arrays and Buffers share memory.
  • Maps and Sets cross as real V8 Maps and Sets whose methods call export_op on the native collection. This is bridge ABI 8: the host struct gains NI-6's fields and then export_op.
  • util.promisify(islandFn) runs Node's promisify in the island.

NI-5c. Native x instanceof Error (and the builtin error subclasses) asks Node for island objects. util.inspect in the island shows the contents of array, Map and Set exports.

The island helpers module is released at exit, which fixes the sanitized-lane RC audit for 6862 and 6864.

Two changes from int/islands to keep good code untouched:

  • An island object's ToPrimitive is a direct scr_ni_to_primitive call under SCR_NODE_ISLANDS, not a new ScrDynHandleOps member. On int/islands the member grew every handle class's ops table, and the zero-cost gate flagged about 590 executables.
  • The generic error-first util.promisify overloads in the fallback types match only functions that return nothing. On int/islands they captured execFile/exec in TypeScript's subtype pass, and corpus 1473, 1573, 5626 and 5636 failed preflight.

The island lowering keeps its caches on the Lowerer, not in module-level weak collections (compiler-weak-collections.test.ts).

Evidence

  • Corpus programs, byte-identical with Node:
    • 6848-node-island-handle-ops: undeclared arrays and Maps as handles, identity, spread both ways, ToPrimitive, compound assignment, optional chains.
    • 6862-node-island-native-subclass: an error subclass and a subclass of it, super.describe(), a getter, instanceof on both sides, throw and catch, and a semaphore subclass with an async override.
    • 6863: native arrays. 6864: promisify. 6865: collections.
    • 6866/6867: a deferred class subclassed at top level. Node is never mapped when the class is unused, and starts at the first throw when it is used.

Shortcuts

  • x.m?.() reads the method twice. Handle bindings are const only, and x++ on island members is not lowered.
  • Tuple and plain dyn array writes from the island go through Reflect.set, which materializes and commits: O(n) per write.
  • Static members, decorators, parameter properties, private or computed names, and classes not at top level are still SC8011.
  • A local (not top-level) promisify(islandFn) is not handled.
  • An inspect of a native mutation shows it only after the value crosses again.

Gate results

  • Island set in a Vercel Sandbox, with the libnode dev pack rebuilt from this branch's bridge (node-island*.test.ts, npm.test.ts, packages/compiler/src/node-islands, differential 68xx): plain 223/223 + diff 64/64; sanitized 223/223 + 64/64.
  • pnpm lint, pnpm format:check and pnpm test:ts7 --baselines-only pass.
  • The self-hosted compiler (packages/compiler/src/native/cli.ts --emit=llvm) builds.
  • Zero-cost gate: 2838 programs identical (benchmarks, self-hosted compiler, TypeScript corpus) against the base branch. The only DIFFERS is corpus/fs-extra-package (%m0.settleVoid). It differs in every gate of the stack, including Bound V8's stack scan by Node's own stack in Node islands #1898, which does not touch the compiler, so it is nondeterministic on the base. both-fail covers only the island programs, because the gate host has no libnode pack. Compile time stays within tolerance.

CI on the first push found failures outside the focused island set: the ABI in bridge.test.ts, the runtime ABI header declarations, the effect gate on the ts7 allowlist, weak collections in the compiler, the pipeline scout and the Node version line. The fixes are folded into the PRs that introduce each piece, and they are re-checked in a Sandbox on the final tree.

Stack

#1898
#1899
#1900
#1901
#1902
→ #1903
#1904
#1905
#1906
#1907
#1908

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 23, 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 Oct 28, 2026 6:57pm UTC

… and collections by reference (NI-5b/5c/5d)

Node islands stack, part 6 of 11 (scriptc-perf/results/islands-prs/PLAN.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/compiler/src/backend/llvm/lib-shared.ts
#	packages/compiler/src/ir/builtin-signatures.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

Review pass on this PR.

All Vercel Agent findings are already addressed on this branch, and I re-verified each against the head (76a60ff):

  • Self-referential array/collection mirror refresh (packages/libnode-common/bridge/scr_node_bridge.cc): RefreshMirror inserts the export id into g_refreshing before syncing and erases it with an RAII Done guard, so a cycle that reaches the same export again returns instead of overflowing the native stack. Added in 89f8b39.
  • Derived island-subclass constructor returning a replacement object (packages/runtime/src/scr_node_island_host.cjs): the subclass() wrapper returns an object/function result, throws TypeError: Derived constructors may only return object or undefined for any other primitive, and only then falls back to box.self. Added in 89f8b39. Verified against Node 24: same message, same behavior.

Vercel Agent's re-review of the current head reports 0 suggestions, and CI is green (run 40505952799).

Stack note: ni/i5-shared-modules (#1902) is still at 3a8aee7, which this branch already contains, so the merge check before this pass was "Already up to date".

🤖 Generated with Claude Code

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

Copy link
Copy Markdown
Contributor Author

Restacked onto the new signed ni/i5-shared-modules head (4a673558b) with a plain merge, no rebase. Merge commit: 86056af74.

Conflict — packages/runtime/src/scr_dyn.h: both sides appended an independent block at the same point (the end of ScrDynHandleOps, before the transform unit's declarations). Kept both:

  • i6 side: the ToPrimitive hint enum (SCR_DYN_HINT_DEFAULT/NUMBER/STRING), used by scr_json.c and scr_node_island.h.
  • i5 side: the WebIDL handle-class API (ScrDynHandleAccessor, ScrDynHandleStatic, ScrDynHandleClass, and the scr_dyn_handle_class_* declarations), implemented in scr_dyn_handle_class.c / scr_webstreams.c.

No semantic overlap: the enum is a value hint, the class block is the interface/prototype surface. Verified both remain the only definitions (no duplicate declarations).

node scripts/regen-generated.mjs regenerated the three generated files from the merged sources; its output matched, so nothing was hand-edited.

Verified in a Linux sandbox on the merged tree: node-island.test.ts, node-island-handles.test.ts, node-island-shared-sources.test.ts, node-island-order.test.ts — 63/63 passed; corpus 6864-node-island-promisify, 6865-node-island-collections, 6866-node-island-deferred-subclass — 3/3 passed. The same stack top (#1908) also passes 64 island-harness tests and 11 island corpus programs.

🤖 Generated with Claude Code

cramforce and others added 4 commits October 28, 2026 08:46
Resolve tests/harness/npm.test.ts by taking main's npm-suite split and
porting the island path normalisation and the two island KNOWN entries
into tests/harness/npm-suite.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lowerer journal test requires every collection to be journaled,
checked or left out by name. islandHandleBindings is a memo keyed by
symbol and islandInstanceofHandled is unwound scratch of the lowering in
progress, so both are unjournaled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stale i6 branch marked lazy-traps island-only, which held while the
whole package was refused and ran in a Node island. main compiles the
package statically with a throwing stub per blocked edge, so its require()
edges still match Node through an island while an import() whose literal
specifier resolves nowhere is a deferred SC2012 fence. Record that
remaining gap the way main does instead of claiming Node parity.

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

Copy link
Copy Markdown
Contributor Author

Stack maintenance round on this branch; head is now a989612ce.

CI root cause (the one failure in the whole stack). Every PR from here up fails test (Linux, plain|san, 10/20) in packages/compiler/src/frontend/lowering/unit-journal.test.ts with expected [ 'islandHandleBindings', …(1) ] to deeply equal []: the new island collections were never classified. Fixed in 126154edc — islandHandleBindings is a memo keyed by symbol (the answer depends only on the declaration) and islandInstanceofHandled is scratch of the lowering in progress, so both join Lowerer.unjournaled with that reason recorded beside them. The aggregate test job fails downstream of the lane, so it needs no separate fix.

Conflict. tests/harness/npm.test.ts conflicted because main split it into tests/harness/npm-suite.ts. Resolved by taking main's refactor (3b86b83c4, which merged origin/main and ported this branch's island cases into npm-suite.ts) and then merging the newest ni/i5-shared-modules head a484390e0 (6d74cbb10). No source files were rewritten by hand.

Sandbox validation with a libnode pack built from the top branch's bridge sources (bridgeAbi 10, so the pack matches the stack): unit-journal, the eight node-island*.test.ts files (100 tests), the 64 island corpus programs, and the npm suite. That surfaced one stale expectation this branch carried: lazy-traps claimed Node parity through an island. main compiles that package statically with a throwing stub per blocked edge, so its require() edges still match Node through an island, but an import() whose literal specifier resolves nowhere is a deferred SC2012 fence instead of Node's ERR_MODULE_NOT_FOUND rejection. a989612ce records exactly that as a divergence (the same shape main records), instead of asserting parity. It is main's existing import() gap, not behavior this stack changed.

Zero-cost gate. The only compiler edit adds field names to Lowerer.unjournaled, which is read exclusively by unit-journal.test.ts; it is inert at codegen, so the gate cannot differ. Everything else here is test harness and merges.

🤖 Generated with Claude Code

@vercel-internal-security-reviewer

Copy link
Copy Markdown

Security review details

Base automatically changed from ni/i5-shared-modules to main October 28, 2026 17:41
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

Restacked after main moved. #1902 was merged at 17:41Z (merge commit 2f3b926de), so this branch now merges origin/main and the merge was clean (no conflicts, no hand edits; pnpm manifest reproduces the merged surface manifest byte for byte). Head is now 81580f44a and a fresh CI run is in progress on it.

🤖 Generated with Claude Code

@ctate
ctate merged commit 94e9217 into main Oct 28, 2026
136 of 138 checks passed
@ctate
ctate deleted the ni/i6-subclasses branch October 28, 2026 23:10

This branch was successfully deployed

1 active deployment
Preview — 81580f44 Deployed Oct 28, 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