Repository navigation
Train islands 2: structuredClone clones an Error as an Error, a warning where an island object is copied, leaf records an island held are released, and dynamic programs get back part of what islands 1 cost - #3382
Conversation
A record or class instance whose fields cannot form a cycle has no cycle
header, so its capsule's trace does not visit it. The walk before a major
GC never counted the capsule's reference to it, so every such record an
island was handed looked held natively and stayed a root for the rest of
the run: 90,000 { attempt: number } records left 90,000 exports held, and
two major GCs freed none. The walk now counts that reference without
entering the record.
Co-Authored-By: Claude Code <noreply@anthropic.com>
…SC6007) Since #3052 a checked conversion accepts an object or array a Node island made and builds a native value from its members; before, it threw "expected array at $, got Object from a Node island". The copy is silent: under Node the value is the island's own object, so the island's later changes show through it, the program's writes reach the island and it is === to the island's object. The island value matrix has 299 such facts in 166 entries (identity lost, a write on either side not seen by the other, members the target type does not declare dropped). The divergence scan now reports each conversion the compiler can see as SC6007, "island value crosses as a copy" (the code PR #3252 uses for island arrays; the title is the general one the code registry names): - an assertion whose operand is an island read or call, awaited or not, or a const of one, typed any, unknown or by the island (`make() as Item[]`); - a typed binding of such a value (`const xs: number[] = anyList()`); - a typed parameter of a callback passed to an island call, when the callback writes through the parameter or compares it; - an island call declared to return a Map or Set whose key, value or element type is a native array or record (the collection stays the island's; each member read from it is converted). A target that stays checked-dynamic (unknown, a record of unknown members, a live view) keeps the island's object and reports nothing, as does a copy the program writes (`structuredClone(value) as T`) and a value no island made. The scan reads the syntax tree after lowering and adds no lowerer state; no generated code changes. Of the 299 facts, 298 are in a program that now gets the warning at the conversion they sit behind; the one left is the identity of a nested element read inside a callback. 44 sites in 20 of the matrix's 239 programs; none in a control program. Co-Authored-By: Claude Code <noreply@anthropic.com>
structuredClone had no arm for errors: it copied an error's checked-dynamic node property by property, the hidden `%error` marker included, so the result was a plain object with no kind, no name and no stack, which kept a subclass's fields and an AggregateError's errors. The copy is now what Node's serializer makes: a new error of the one of Error's six native subclasses the source's name spells (else Error), with the source's own message, its stack when that is a string, and a clone of its own cause; nothing else travels. A DOMException copies its name and message. An error the input holds twice clones once, and a cause may lead back to its error. The copy is an ordinary error with its ordinary node, built the way a decoded message builds one. A class's `name` getter is read through the reader the program installs, which scr_error.c now exports as scr_error_read_name; only the clone links it. A cycle through a plain object or array still raises the existing refusal. Typed call sites are unchanged: the copy checks back into the static type. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com> # Conflicts: # docs/content/docs/limitations.mdx
Co-Authored-By: Claude Code <noreply@anthropic.com>
With structuredClone of a native Error fixed, sixteen pinned observations leave the table and one loses its clone part: the four error controls (U.clone), the nine n2i2n/U.clone cells the islands-1 train pinned as wrong, and the clone part of four clo-n2i/N.callback entries. Nothing is new and nothing else changes. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…the island table The five island paths the islands-1 train put behind ScrNiDynOps (error_get, error_has, promise_get, promise_has, promise_set) still tested each entry for NULL, and the two error sites looked the error up in the error-view cache before they called. The table is installed whole by one initializer, so the entry tests decided nothing, and the lookup is only needed in a program that links the island units. A site now tests the table and calls; error_get and error_has take the view and find its error in the island unit. The table's slots keep their places. In the release runtime object, against the tree before islands-1: scr_dyn_in +106 bytes (was +234), scr_dyn_key_set +71 (+213), scr_dyn_obj_read_receiver +5 (+111), scr_dyn_promise_key_get +79 (+89), and scr_errdyn_find is inlined again (it had become a function of 163 bytes). Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…a property Review finding on #3382: an error whose own cause is an accessor (Object.defineProperty(e, "cause", { get })) cloned to an error with an own cause holding undefined, where Node's copy has no own cause. The error's cause slot follows its node's entry, and for an accessor that entry holds a placeholder, not a value. The clone now copies the cause as it copies the message: only an own data property. Pinned in 9918-structured-clone-errors-js.mjs for an accessor on an error made without a cause and on one made with a cause. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
The two results the description promised. Gate identity on the pushed head ( Sanitized island suites on Main is red in one unit test since #2567 ( |
|
Pushed The commit changes one function, |
Train cli-5 (#3386) and this train each add a divergence warning beside SC6004: both lines are kept in codes.ts (SC6007, SC6008) and both mentions in the header of diagnostic.ts. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…rtup-1 By the test's update mode. One line moves: the fixture's statements in the dynamic tier go from 12 to 10 on the merge with main 851b59b. The warnings the snapshot is there for are unchanged. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Pushed The merges.
The generated island files, each by its script on the merged tree (32 vCPU, libnode pack from the tree, bridge ABI 19):
Checks on the merged tree. Build, runtime pack, generated-file checks, Native bootstraps. CI runs them only for pull requests that touch the bootstrap paths, and no round of this train ran both on a tree that differs from the head only by main: the plain and the sanitized native bootstrap ( |
|
The results the last comment said were running, all on the pushed head's tree ( Both native bootstraps pass.
Sanitized island suites (32 vCPU, libnode pack from the tree): island harness suites 423 of 423, island corpus ( |
…nto the restack of vercel-labs#3284 Co-Authored-By: Claude Code <noreply@anthropic.com>
…e restack of vercel-labs#3284 Co-Authored-By: Claude Code <noreply@anthropic.com>
Conflict: packages/compiler/src/frontend/lowering/divergences.ts. The lang train moved the width-copy scans of variable declarations out of this visitor (vercel-labs#3221); main (islands-2, vercel-labs#3382) added one line to the same branch, the list of annotated declarations a program with islands checks. The branch keeps main's line and nothing of the scans the lang train moved. Co-Authored-By: Claude Code <noreply@anthropic.com>
…vercel-labs#3284) into train/landing-1 Byte views and symbols keep one identity across the Node island boundary. The head sits on vercel-labs#3382's head, so the merge brings only its own diff. No conflict. (The branch's later head 6eb3826821 is its own merge of main 3dc1f4a and adds nothing to this tree.) Co-Authored-By: Claude Code <noreply@anthropic.com>
Main gained vercel-labs#3394 (the Lowerer is handed over only as itself, with a guard test), vercel-labs#3382 (islands: structuredClone of errors, the collector's leaf records, island member sites behind the ops table), vercel-labs#3390, vercel-labs#3389, vercel-labs#3393 and vercel-labs#2295. Two conflicts, both with vercel-labs#3394: - frontend/specifier-bounds.ts: main's two calls pass lowerer.bindingScope(); the bounder has no Lowerer any more and keeps passing its context, which is a record on both roads (Lowerer.specifierBoundContext() and specifierBoundContextOf(program)). - frontend/lowering/lowerer.ts: the import list, and main's bindingScope() beside this branch's specifierBoundContext(); both stay. One edit outside the conflicts, where a clean merge left a comment that is no longer true: runtime/src/scr_node_island_handle.c, nh_error_view_ref (new on main with vercel-labs#3382). Its comment said the ref it returns is borrowed from the pin. Under the worker routing a pinned lookup returns a counted reference, taken under the table's lock, and both callers of the helper (nh_error_get, nh_error_has) release it, as the routing already made them do. The code merged right by itself; the comment now says +1. Main's guard test passes on the merged tree: no call passes a Lowerer to the bounder. Co-Authored-By: Claude Code <noreply@anthropic.com>
One rollup of three branches about the Node island boundary and one cleanup (landing train
islands-2, first cut), merged together and validated as one tree. None of the three branches had a pull request of its own.It removes the one answer #3377 (train
islands-1) landed worse, and pays back part of what #3377 cost every dynamic program. #3377's first paragraph named it:structuredCloneof a program error that went native code, island, native code gavefresh:{"%error":true,"message":"plain"}where Node givesfresh:{}, becausestructuredCloneof a native error was already wrong with no island in the program. With this trainstructuredCloneof an error gives an Error, as Node does, and the nine pinnedn2i2n/U.clone = wrongcells and the four error control cells leave the island value matrix. #3377 also made 3,709 dynamic programs 320 to 1,064 bytes larger; 3,681 of them get 256 to 744 bytes back here (see "The island member sites" below).What lands
The member branches are merged, not rebased, so their signed commits are in this branch.
fix/structured-clone-error(531b694b5c):structuredCloneof an error in compiled code gives a new Error of the class Node picks (the one of Error's six native subclasses the source'snamespells, else Error), with the source's ownmessage, itsstack, a clone of itscauseand nothing else; a DOMException clones to a DOMException; one copy per source error within a call, so an error held twice clones once and a cause may lead back to its error. Before, the clone was a plain object carrying a hidden%errormarker, a subclass's fields and an AggregateError'serrors, without a class, anameor astack. Two corpus programs (9917-structured-clone-errors.ts, 43 lines of output;9918-structured-clone-errors-js.mjs, 19). Runtime only: the built compiler is byte-identical to main's. Loud as on main, and listed in the owner's notes: a static type that is a built-in or user Error subclass (SC1100 at build time, or the failed cast at run time), a cycle through a plain object, a cause that does not clone.fix/island-matrix-wrong(6050e43a2b): a build-time warning, SC6007 "divergence: island value crosses as a copy", where the compiler sees a checked conversion copy an object or array a Node island made: an assertionislandCall() as T(also through aconstor anawait), a typed binding of such a value, a typed parameter of a callback passed to an island call when the callback writes through it or compares it, and an island call declared to return aMaporSetof native arrays or records. Since Node parity: compile the apps the island scorecard refused #3052 such a conversion copies where it used to throw, so identity is lost and writes are not shared, silently: 299 facts in 166 entries of the value matrix. The entries stay pinnedwrong(the fix is a live view); the warning says where. Over the matrix's 239 programs: 44 sites in 20 programs, none in a control program; 298 of the 299 facts are behind a warned conversion. TypeScript and docs only: it changes no observation (the regenerated matrix is byte-identical with and without it). New limitations entry "Objects converted from Node islands", coverage fixturetests/coverage-fixtures/island-copies/.fix/island-collector-leaf-records(cd09b5c6f9): the cross-heap collector releases a record or class instance without a cycle header that an island was handed. Such a record's capsule never had its reference counted by the walk before a major collection, so the record stayed a root for the rest of the run: 90,000{ attempt: number }records handed to an island left 90,000 exports held and two major collections freed none. 35 lines inscr_node_island_gc.c, fixturetests/fixtures/node-island/gc-records.{ts,cjs}, one test innode-island-gc.test.ts.What the train changed beyond the members
docs/content/docs/limitations.mdx: main (Deliver a child's stdout and stderr, and its children's events, in Node's order #3343, child process event order) andfix/island-matrix-wrongeach added an include after the same line; both are kept.clone=wrongpart, nothing is new and nothing else changes. Leaving:U.clone = wrongin the four error control cells (error.control.-,error-cause.control.-,error-class.control.-,error-aggregate.control.-);n2i2n/U.clone = wronginerror.p,error.w,error.u,error-cause.w,error-cause.u,error-class.p,error-class.w,error-class.u,error-aggregate.u;clo-n2i/N.callback = clone=wronginerror.u,error-cause.u,error-aggregate.u.island-error-class.uclo-n2i/N.callbackgoes fromclone=wrong,forin=wrong,json=wrong,keys=wrongtoforin=wrong,json=wrong,keys=wrong. A full update of all 239 programs on the members' tree writes a file byte-identical to the committed one, and the matrix passes in check mode on the tree with main's islands: a value an island made passes the checked conversions #3029 and the cleanup.ScrNiDynOpsentry, no change to the embedded host script or to the conformance list, so the train does not meet the start-up train (Train startup 1: start Node for island packages only when the program needs it, and answer island module resolution at build time #3350) in a generated file.14960424a1(Run the island conformance lane on pushes to main #3374, islands: a value an island made passes the checked conversions #3029) before the last gate round, and up to793159ec8a(trains perf-2 and cli-2, Judge nursery bypass statistics per closure body #2719, Order a Node island lifecycle program by its steps, not by milliseconds (main is intermittently red in it) #3380, Bound computed import() specifiers and load bounded builtins natively; require in ES modules throws #2567, Fix main: remove an import binding-analysis.ts no longer uses #3381) for the push. The second merge brings 167 files; none is a file this train changes and none is an island runtime file.The island member sites of
scr_json.cdo less (commit1bda0dd984)#3377 put five island paths of
scr_json.cbehind the island table (error_get,error_has,promise_get,promise_has,promise_set). Each site still tested its entry for NULL, and the two error sites looked the error up in the error-view cache before they called. That unit is in every dynamic program, so the tests and the lookup were too: #3377's gate showed 3,709 programs 320 to 1,064 bytes larger, and the lookup (scr_errdyn_find) had stopped being inlined.The table is installed whole by one initializer, so an entry test decides nothing, and finding the error of a view is only needed in a program that links the island units. A site now tests the table and calls;
error_getanderror_hastake the view and find its error in the island unit. The table's slots keep their places (only the first parameter of those two entries changes type).scr_dyn_inscr_dyn_key_setscr_dyn_obj_read_receiverscr_dyn_promise_key_getscr_errdyn_findas a function of its ownEach site is proven to still run by switching it off alone in one runtime build of that commit's tree; a named corpus program then stops matching Node, and with none switched off all pass:
error_getanderror_has:node-island-lifecycle-error-properties;promise_get:node-island-crossing-promise-subclass,node-island-crossing-request-promiseand its typed twin;promise_has:node-island-crossing-promise-subclass,node-island-crossing-request-promise;promise_set:node-island-crossing-request-promise.Validation
All in Vercel Sandboxes, Node 24.22.1, libnode pack built from the tree under test (bridge ABI 19). Three layers: one full round on the three members; then, for main's #3374 and #3029 and the cleanup commit, the gate with bench-runtime, the island suites and the mutation proof on that tree (
1bda0dd984); then the short list on the pushed head, which adds only a merge of main.Full round on the members (
b55c3a4fb7against main3dda5d9c41's tree;bin/validate --gate-corpus=allwith the allow lists below; 43.5 minutes, 93 lane jobs):packages/cliandpackages/compileron a fresh tree).structuredClonerows below), none differing or runtime-only outside the lists. Compile time within tolerance: bench geomean +0.54% [-0.87%, +1.54%], fixtures -0.83% [-1.19%, -0.48%], self +0.03% [-4.08%, +3.37%] (inconclusive at the default run count).llvm-differential(dev),node-island-lifecycle-first-use-in-handlers/main.mjs: the lineisland timer after uncaughtcame afterunhandled second. The program (Node islands: fix error properties, process.argv, thenable awaits and work after 'exit'; pin lifecycle behaviour with 40 corpus programs #3123's, on main) starts Node inside anuncaughtExceptionlistener at 1 ms; the island's timer is due 1 ms after that and a native rejection at 15 ms, so a slow Node start flips the two. Alone, 20 runs of both differentials in one Sandbox: this train 20 passed; main's own tree 19 passed, 1 failed with the same output. Main has re-ordered the program since (Order a Node island lifecycle program by its steps, not by milliseconds (main is intermittently red in it) #3380, in this branch through the merge of main).server.test.tshttps-request-fn: stopped at its 120 s limit. Alone, sanitized, 5 of 5 pass (63 s for the first build, under a second after).pnpm node-compat:checkcurrent (4,996 source rows, 3,886 public rows);scripts/checker-identity.mjs --filter 991: 11 programs agree; island value matrix, full update: 239 of 239, byte-identical to the committed table; island conformance lane, check mode: 424 passed, 70 skipped, 0 failed; docs gate (NEXT_DIST_DIR=.next-check pnpm check): 25 of 25.On the train's last own commit (
1bda0dd984: the members, main14960424a1, the cleanup commit; 32 vCPU, Intel Xeon 2.90 GHz):node-island-host.mjs, IR codec, IR fields): pass.pnpm lint: 0 warnings, 0 errors, the runtime refusal baseline included.pnpm format:check: clean.pnpm test:ts7 --baselines-only: complete.tests/harness/*island*.test.tsexcept the conformance lane; the value matrix in check mode and the new collector test among them): 383 of 383. Island corpus, plain (differentialandllvm-differential, every island program): 470 of 470. Island diagnostics: 12 passed.1bda0dd984are running beside CI, and I will add their result as a comment.--gate-corpus=all --gate-complete, against main14960424a1: rows below.On the pushed head (
98b69349ad=1bda0dd984merged with main793159ec8a; 16 vCPU, Intel Xeon 2.50 GHz): build, runtime pack, generated-file checks,pnpm lint(0 warnings, 0 errors),pnpm format:check, ts7 baselines,pnpm node-compat:check: pass. Guard tests: 227 passed, 1 failed, and that one is main's since #2567 (551639137b):outer-expressions.test.ts"only outer-expressions.ts defines a function that peels wrappers off an expression" namesfrontend/binding-writes.ts#stripValueWrappers, a file this train does not touch. Focused corpus (the two new programs, main's re-ordered lifecycle program,island-value-conversions, the mutation proof's programs; both differentials): 16 passed. The island tests ofcoverage.test.ts: 7 passed. Docs gate: 25 of 25. The gate's identity on the pushed head runs beside CI and goes into a comment.Gate rows
Against main
14960424a1, 5,735 programs: 1,769 byte-identical, 229 fail to build on both sides, 56 allowed, 3,681 runtime-only, none differing. Compile time within tolerance (bench +0.57% [-0.87%, +1.82%], self -0.62% [-2.14%, +0.40%], fixtures +2.00% [+1.46%, +2.54%], which the tool calls inconclusive at the default run count; the members' own round read -0.83% for the same compiler). bench-runtime: 69 workloads, geomean +0.0%, 67 neutral, 1 faster (stream-buffered-drain-1.8%), 1 slower (csv-numbers+2.8% [+0.2%, +17.5%], whose executable is byte-identical to main's, so that reading is of one binary against itself).scr_dyn_key_set(3,681 programs),scr_dyn_obj_read_receiver(3,680),scr_dyn_promise_key_get(2,024),scr_dyn_in(1,665), andscr_errdyn_findstops being a symbol in 1,664. 3,657 corpus programs and the 24 dynamic benchmarks. This is recorded as a one-time change in the fleet's gate exceptions, beside Train islands 1: errors, promises, process events and process.stdout.write keep Node's behaviour across the Node island boundary #3377's row, which it pays back in part.fix/structured-clone-error's, the programs that link the clone: 34 by the source patternstructuredClone(the two new corpus programs among them) and 22 by program id:performance.mark/measureclone theirdetail(6), JavaScript that reads builtin globals as values links the table that holdsstructuredCloneas a function value (11), npm packages in the program call it (5). In the members' round, where they are the only rows: IR and LLVM identical on every one, the only resized functionsscr_sc_cloneandscr_structured_clone; 53 programs grow 2,768 to 3,000 bytes (median 2,936) and three that had no error-node code before grow 17,096 to 17,120 bytes (structured-clone-collections.ts+3.2%,7614-key-order-copies.ts+3.1%,unknown-array-holes.ts+4.2%: the error node builder bringsscr_dyn_prototype_unsupportedandscr_dyn_prototype_valid), under the fleet's rule that a program using a rare feature may grow (--gate-max-size-growth=5).fix/island-matrix-wrongandfix/island-collector-leaf-recordsadd no row: the first changes no emitted code, the second only an island unit.Not run
fix/structured-clone-errorleaves the built compiler byte-identical;fix/island-matrix-wrongadds a scan after lowering, and its own round on main5b64e49735ran both bootstraps and Test262 with nothing regressed). The plain native bootstrap is among the lane jobs.Sequencing
The start-up train #3350 conflicts with island changes through two generated files (the embedded host header and the conformance list). This train touches neither, but by the fleet's sequencing it merges after #3350; if #3350 moves either list under it, main is merged and the generated island files are regenerated by their scripts before it lands.
🤖 Generated with Claude Code