Repository navigation
Conversation
…ross the boundary An island promise that only island code handles was reported as an unhandled rejection by the runtime, and awaiting one took a microtask turn more than in Node. The program's mirror of an island promise subscribed to the V8 promise when it crossed. It now subscribes when native code first reacts to it, settles inside V8's reaction job and runs the native reactions there; a typed conversion of a mirror waits with it. V8 decides whether an island promise is unhandled. A native error thrown through island frames came back as another object, without its class and fields, and island code saw none of its fields. A native error now crosses by reference: one V8 Error per native error (an export, bridge ABI 12), which is the native error again when it crosses back. Island code reads its own enumerable fields (copied at each crossing) and everything else (cause, an AggregateError's errors) through the export. An island object with a then method was adopted only by await. The Node island build of the dynamic promise unit reads then in V8, so every resolution adopts it: Promise.resolve, combinator entries, an async function's return, an executor's resolve, a reaction's result. Members an island class adds to Promise (an SDK's request promise) were lost on the program's promise: typed reads, writes and calls through the package's types, and dynamic reads, now reach the island object. Co-Authored-By: Claude Code <noreply@anthropic.com>
…awaited A conversion of an island promise that nothing reacted to keeps the promise it converts in its payload words. The free path cleared the payload before the source was released, so the mirror leaked (the sanitized lane caught it in node-island-crossing-combinators-typed). Every free and every settlement now drops the lazy state first, which also keeps a view that something else settles from treating its payload as a source. The value a V8 reaction settles a mirror with converts before the settlement's jobs are collected. Co-Authored-By: Claude Code <noreply@anthropic.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ronise what a V8 Error holds Two review findings on #3190, both reproduced. An executor that resolved its promise with an island thenable left its resolving functions active: a later reject, resolve or throw in the executor settled the promise first (Node: the first resolution stands). The promise is now marked resolved until the thenable's job settles it, and the executor's resolve, reject and throw leave a resolved promise alone. The V8 Error of a native error kept the message, name, stack and code it was made with. Island code that held the error read the old values after native code changed them. They are now brought up to date at every crossing, like the enumerable fields. Co-Authored-By: Claude Code <noreply@anthropic.com>
… an explicit value) Co-Authored-By: Claude Code <noreply@anthropic.com>
cramforce
marked this pull request as draft
November 7, 2026 06:34
The review thread on #3123 (then read twice during adoption): a getter that answers a function on its first read only. The resolution reads it once and calls what it read. Co-Authored-By: Claude Code <noreply@anthropic.com>
…the error A program error crosses into a Node island by reference: the export table holds the error's view and the bridge keeps one V8 Error for it. The runtime's error-view cache also holds the view and the error, until nothing else does. The cross-heap analysis counted the cache's reference to the view as a holder in the program, so it reported every exported error as a root: V8 never collected the V8 Error, the export table never released the view, and the cache never swept the pair. Every error that crossed stayed alive for the rest of the run. The analysis now explains the cache's two references for an exported error view, and keeps the export a root only while something else in the program holds the error. Peak resident memory for 100,000 iterations of one pattern, before: an error passed as an argument 361 MB, thrown through an island call 437 to 489 MB, as a cause 734 MB, against 108 MB for a callback that crosses no error. tests/fixtures/node-island/gc-native-errors.mjs sends four errors per iteration; the test asserts that 300,000 iterations stay within a factor of 1.5 (plus 32 MiB) of 100,000 and that the collector frees the exports. Co-Authored-By: Claude Code <noreply@anthropic.com>
A method call or a write on a member an island class adds to Promise (a request promise's asResponse() and withResponse()) threw in untyped code: `r.asResponse is not a function`, `Cannot create property`. The Node island builds of the invoke unit and of the dynamic write ask the island object, as reads and the typed entry points already do. then, catch and finally stay the program's promise's own. Co-Authored-By: Claude Code <noreply@anthropic.com>
Awaiting an island promise that is an instance of a Promise subclass whose constructor does not take an executor (the request promise of generated API clients, which also starts its request in an overridden `then`) rejected with `TypeError: Promise resolve or reject function is not callable`. The bridge took every V8 promise as itself and called the intrinsic `then` on it, which constructs the result through the species constructor, here the subclass, with an executor it ignores; the subclass's own `then` never ran. `await` takes a promise as itself only when its `constructor` is the intrinsic Promise. Anything else is resolved like a thenable: its own `then` runs from a promise job. The bridge now follows that rule. Co-Authored-By: Claude Code <noreply@anthropic.com>
…-promises-errors The island operations table takes main's object_tag before this branch's promise_get and promise_has. Co-Authored-By: Claude Code <noreply@anthropic.com>
…romise-subclass Co-Authored-By: Claude Code <noreply@anthropic.com>
cramforce
added a commit
that referenced
this pull request
Nov 7, 2026
#3190 gained a re-synchronisation of the name, message, stack and code a V8 Error holds at every crossing. Here those are properties whose values live in the native error, so there is nothing to bring up to date: the arm keeps this branch's form. #3190 also took this branch's collector fix by cherry-pick; the test file keeps both branches' tests. Co-Authored-By: Claude Code <noreply@anthropic.com>
cramforce
added a commit
that referenced
this pull request
Nov 7, 2026
…te through the dynamic set #3190's merged head changes an error's message, name and code natively after island code kept the error (node-island-crossing-error-identity), and the island read the old message and name. A native error's view holds those three as of the last time it was built or written through; typed code writes the error's own slots. The export now answers message, name and cause, and whether each is an own property, from the error itself, for every native error and not only for a class instance, and leaves an accessor of the view (a message computed on demand) to the view. An island write went through Reflect.set followed by a commit of the whole view, which would have overwritten a slot typed code had written since the view was built. It now goes through the dynamic property write, which stores name, message, stack, cause and code in the slots itself. Co-Authored-By: Claude Code <noreply@anthropic.com>
This was referenced Nov 7, 2026
The matrix pins every observation that is not a match with Node. With a native error crossing by reference, 57 pinned differences are gone (its identity in island code and after a round trip, cause, an AggregateError's errors, code, and the programs that used to exit in the round-trip section) and 26 cells list fewer differences. 32 cells are new, all in sections that used to end in `exit` before they were reached (the round trip of a typed error, an island async iterable); they are named refusals, the island-write cells that already exist for the other direction, and the native tier's own answers. Four cells change kind: `structuredClone` of an untyped native error after a round trip was `unsupported` for the island view it used to come back as and is now the native tier's own answer for an Error, which the control rows pin as `wrong` without any island. Co-Authored-By: Claude Code <noreply@anthropic.com>
The island-thenable pass replaced an intrinsic node in place by deleting its keys through a `Record<string, unknown>` view and assigning the new node's. In the compiler compiled by itself that view made every IR node type a dynamic record: the native compiler's LLVM input doubled (2.06 GB against 1.03 GB), 18,000 more of its functions lost their profile, and the bootstrap's seed build took 1,926 s against the base's 767 s on one 32 vCPU machine, past the test's 1,800 s limit. The zero-cost gate does not see it; the native bootstrap lanes did, as timeouts. The pass now uses the typed rewrite (transformStmtList) on the functions that have such a promise and leaves every other body alone. Same machine: 751 s, 1.03 GB, the base's profile match. Co-Authored-By: Claude Code <noreply@anthropic.com>
…ite) into fix/island-promise-subclass Co-Authored-By: Claude Code <noreply@anthropic.com>
cramforce
marked this pull request as ready for review
November 7, 2026 12:16
This was referenced Nov 7, 2026
Contributor
Author
|
Superseded by #3377 (landing train islands-1), which carries this branch's commits merged with the other island boundary changes and current main, validated as one tree. Review of this change continues there; if #3377 does not land, this can be reopened. 🤖 Generated with Claude Code |
espetro
pushed a commit
to espetro/scriptc
that referenced
this pull request
Nov 8, 2026
…3309) into train/islands-1 ScrNiDynOps: main's function_identity and function_of_identity stay first; promise_get, promise_has and promise_set follow at the end. The bridge ABI stays at main's number here; the train bumps it once at the end. Co-Authored-By: Claude Code <noreply@anthropic.com>
espetro
pushed a commit
to espetro/scriptc
that referenced
this pull request
Nov 8, 2026
…lands-1 ScrNiDynOps: inspect and report_uncaught follow promise_get, promise_has and promise_set at the end. scr_json.c builds once since vercel-labs#3135, so the five island blocks that vercel-labs#3123, vercel-labs#3190 and main's Promise-subclass change keep under SCR_NODE_ISLANDS would be compiled into no program: they lose the #ifdef and test the installed table, as vercel-labs#3135 did for the blocks it met. vercel-labs#3123 replaced error_constructor by error_get, so vercel-labs#3135's rewrite of that call is dropped. Co-Authored-By: Claude Code <noreply@anthropic.com>
espetro
pushed a commit
to espetro/scriptc
that referenced
this pull request
Nov 8, 2026
vercel-labs#3166 and vercel-labs#3190 each made an island promise's mirror subscribe to its V8 promise at the first native reaction. The train keeps one mechanism, vercel-labs#3190's (scr_promise_mark_island_mirror, scr_promise_subscribe_fn, which also covers typed views and settles inside V8's reaction job), and drops vercel-labs#3166's copy of it (island_source, scr_promise_island_fn, SCR_PROMISE_REACTED); the subscriber keeps vercel-labs#3166's guard for a released handle. The new library functions and the events unit are added after main's and the other members' entries. Co-Authored-By: Claude Code <noreply@anthropic.com>
espetro
pushed a commit
to espetro/scriptc
that referenced
this pull request
Nov 8, 2026
vercel-labs#3123 (emit and stop), vercel-labs#3190 (an error's export id) and vercel-labs#3289 (an error's class kind, export id and own properties) each changed the bridge and took a number of their own (11, 12, 13) below or at main's 13. The train carries all three under one number, so a libnode pack built from any other tree is refused instead of half-working. Co-Authored-By: Claude Code <noreply@anthropic.com>
espetro
pushed a commit
to espetro/scriptc
that referenced
this pull request
Nov 8, 2026
The self-compile budget judged the compiler's own compile by its lowering passes, its heap and its time. What that compile emits was not judged, and it is the exact number: vercel-labs#2878 added 6.4% of the compiler's functions (9,464, and 40% of its dynamic-tier functions) with 428 lines, because a memo made the Lowerer a dynamic value, and vercel-labs#3190 doubled the compiler's LLVM the same way. Neither showed in any gate row. The gate already builds the candidate's compiler source and the baseline's with the candidate compiler. It now compares the two LLVM texts: bytes and functions may grow 1% beyond the growth of the source, and the functions of the dynamic tier 5% (and 100). Across 102 pairs of commits of main from 2026-10-26 to 2026-11-07 the functions grew beyond the source by 0.51% at the 95th percentile and 0.79% at most, except for vercel-labs#2878. A change that means to grow them sets outputGrowthAllowance with a reason. Co-Authored-By: Claude Code <noreply@anthropic.com>
yowainwright
pushed a commit
to yowainwright/scriptc
that referenced
this pull request
Nov 9, 2026
…ercel-labs#3047) The base is main 5b64e49 with train/islands-1 (712aeab) merged in (689813c). Two files conflicted by text, scr_node_island_handle.c and scr_node_island_export.c. The rest of this merge is by meaning: every pinned lookup and island entry point the base added since this branch's merge base was written for one thread and for lookups that return a borrowed pointer, and this branch makes a lookup return +1 under the pin lock and routes island operations from other threads. Pinned lookups the base added, each now releasing the lookup's reference: nh_promise_subscribe, the pinned-error identity check of nh_from_node, the NH_PIN_NATIVE_ERROR lookups of nh_error_of and nh_to_node, nh_error_get, nh_error_has, scr_ni_error_link (two lookups used as tests) and nh_promise_object. nh_pinned_obj retains a NH_PIN_NATIVE_ERROR object like the other kinds: it answered that kind without a reference, which its caller then released. One definition of nh_error_info: the read runs on Node's stack on the island's thread for every caller (main's vercel-labs#3253), and a request from another thread copies the bridge's strings where they are read (this branch); the island's thread reads them in place, as on main. nh_promise_box keeps the base's lazy subscription (vercel-labs#3190) and loses this branch's eager one. A mirror stays the island thread's object: an island promise is refused crossing into another thread before it is boxed. Entry points the base added, on a thread other than the island's: - a twin adoption (vercel-labs#3271) makes a native object and its export, which is the island thread's: the island object stays a handle there; - a native error crosses by reference since vercel-labs#3190, as an export: refused by name like the thread's other native objects (ERR_SCRIPTC_ISLAND_WORKER_VALUE) instead of minting an export of another thread's object; - the thenable adoption (vercel-labs#3190) answered "not a thenable" off the main thread, which fulfilled a promise with the thenable itself: the read of `then` is routed like every read and the thenable's job rejects by name; - nh_error_get converts a routed answer and releases it (nh_routed_done). Export table: a twin class's attach and detach run outside the table's lock (they are a class's own code), and scr_ni_exports_release_all keeps both sides' work. The bridge is untouched by this branch, so the tree keeps the train's ABI 19. Co-Authored-By: Claude Code <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #3123 (
test/island-stress-lifecycle, merged in at f96f851); the diff against that branch is this PR. It includes one commit by the agent working on top of it (e496a03, the V8 Error of a native error is freed once the program and the island drop the error). The regenerated island value matrix is commit 12870c1.Three ways promises and errors went wrong when they crossed between native code and a Node island, found by #3123's stress suite and decided there (pi-decisions, island promises = A, native errors in island code = A), plus one found since (members of an island Promise subclass).
What changes
An island promise that only island code handles is no longer reported as unhandled, and awaiting one takes Node's number of turns.
try { await lib.all([lib.failing(), lib.other()]) } catch { … }printed the caught error and thenUnhandled promise rejection, exit 1. The program's mirror of an island promise subscribed to the V8 promise when it crossed, so every island promise the program had seen had a native reaction, and the runtime reported the mirror. Now the mirror subscribes when native code first reacts to it, settles inside V8's reaction job, and runs the native reactions in that job (no microtask of their own). A typed conversion of a mirror (const p: Promise<number> = lib.f()) waits with it. V8 decides whether an island promise is unhandled: one island code handles is not reported, one nobody handles ends the program through Node's report as before.A native error keeps its identity, class and fields through island frames. After
lib.run(() => { throw original }),e === originalwas false,e instanceof ValidationErrorfalse ande.statusundefined, and island code that caught a native error saw none of its fields,causeorerrors. A native error now crosses by reference: one V8 Error per native error (an export;SCR_NV_ERRORcarries the export id, bridge ABI 12), which is the native error again when it crosses back. Island code reads its own enumerable fields (copied onto the V8 Error at each crossing, whereObject.keys,JSON.stringifyandutil.inspectlook) and everything else (cause, an AggregateError'serrors) through the export. The message, name, stack and code the V8 Error holds are brought up to date at each crossing too, so island code that keeps a native error sees what native code changed on it. A native AggregateError is V8's AggregateError.An island object with a
thenmethod is a thenable to every resolution. #3123 fixedawait.Promise.resolve(v), a combinator's entry, an async function'sreturn v, an executor'sresolve(v)and a reaction's result still answered the object. The Node island build ofscr_async_dyn.creadsthenin V8 and the runtime's own thenable job calls it;awaituses the same job now (one mechanism instead of V8's adoption for one form). That also answers the review thread on #3123 atscr_node_island_handle.c:1224(thenread twice during adoption): the resolution readsthenonce and calls what it read, pinned bynode-island-crossing-then-read-once(a getter that answers a function on the first read only: Node and the program both printthe function of the first read 1). An executor that resolved its promise with an island thenable has resolved it: its laterresolve,rejector throw changes nothing.Members an island class adds to Promise reach the island object.
class Request<T> extends Promise<T> { url; describe() }(an SDK's request promise) crossed as a plain promise: a read typedanyanswered undefined, a typed read threw, a method call threw. Reads, writes and calls through the package's declared types, and dynamic reads andin, now go to the island object;then,catchandfinallystay the program's promise.Where the code is
Island-only:
scr_node_island_handle.c,scr_node_island_export.c,scr_node_island.c, the bridge, and#ifdef SCR_NODE_ISLANDSblocks inscr_async.c,scr_json.candscr_async_dyn.c.scr_async_dyn.cgets Node island builds beside its existing ones (the pack matrix test covers the selection); every other program keeps the objects it had.ScrPromisekeeps its size: the lazy flag takes padding, and a pending lazy view keeps its source in the payload words. The compiler changes are the bridge ABI pin, one library function (nodeIsland.promiseResolve) and four lines in the existing island-thenable pass, which only runs for programs with islands.Not in this PR (each with a repro, in the fleet's follow-up list)
p.then(n1); lib.then(p, i2); p.then(n3)runs n1, n3, i2 (Node: n1, i2, n3). One native reaction, or native reactions with no island reaction between them, keep Node's order.constructorof a program error class are not visible to island code (typeof e.retryableis undefined); fields,cause,errors,instanceofare right. This is the remaining difference in the stress programsr02andd02.Promise.resolve, an executor'sresolve, an async return or a reaction's result in JavaScript source (on main; not island-specific).Proof
Linux x64 Sandboxes (Intel Xeon 2.90 GHz), Node 24.22.1, a libnode pack built from this branch's bridge (ABI 12). Head def1687, base f96f851.
--gate-corpus=all, lanes, native bootstraps): gate PASS, 5,242 programs identical, 142 both-fail (the Node island programs, which the gate builds without a pack on both sides), 0 differing rows, 0 runtime-only rows; bench geomean +0.36% [95% CI -0.57%, +1.10%], self-compile -0.76% [-1.75%, +0.97%], fixtures -0.25% [-1.44%, +0.95%]. Lanes: plain 22,110 passed, 0 failed; sanitized 18,875 passed, 0 failed (90 jobs), including the plain and the sanitized native bootstrap lanes.b03-await-ticks,r03-await-island-thenable,r11-await-island-promise-tick), and no program that matched stopped matching. The other repros of these bugs are down to the remainders listed above:b06-combinatorsno longer ends in an unhandled rejection and differs in one line (instanceof AggregateErrorof an island error, on Node islands: fix error properties, process.argv, thenable awaits and work after 'exit'; pin lifecycle behaviour with 40 corpus programs #3123's list);r02-native-error-through-islanddiffers in one line andd02-native-throwsin six of 70 (methods andconstructorof a program error class). With the programs written for this PR, 62 of 95 match, plain and sanitized.tests/corpus/node-island-crossing-*(JavaScript and TypeScript, three with a typed island package): await order across the boundary at each level; combinators run by each side over promises of both sides; rejections handled by one side only; an unhandled island rejection (three exit-1 programs); error identity, class, fields,cause,errors,stackand later changes after one and two round trips in both directions; every thenable form with athenthat resolves, rejects, throws, resolves then throws, has a throwing getter, never settles or is not callable, and an executor's first resolution;thenread once; the Promise subclass members; a lock taken from an island package and released infinally. Allnode-islandcorpus programs passdifferential.test.tsandllvm-differential.test.ts, plain andSCRIPTC_SAN=1(276 cases per flavor). The sanitized lane is the leak audit: it caught a typed view of an island promise that is never awaited keeping the mirror, fixed in 97be309.exitbefore they were reached, and are named refusals, island-write cells that already existed for the other direction, or the native tier's own answers. Four cells change kind,n2i2n/U.clone: unsupported -> wrongin the untyped error kinds:structuredCloneof a native error after a round trip is now the native tier's own answer for an Error, which the control rows pin aswrongwith no island. No cell went from a match to a difference.Record<string, unknown>view. The gate did not see it; the native bootstrap lanes did, as time limits: in the compiler compiled by itself every IR node type became a dynamic record, the LLVM input doubled (2.06 GB against 1.03 GB) and the bootstrap's seed build took 1,926 s against the base's 767 s (one 32 vCPU machine, each build alone). def1687 uses the typed IR rewrite instead: 751 s, 1.03 GB, the base's profile match (81.9% against 82.0%).pnpm lint,pnpm format:check,pnpm test:ts7 --baselines-only: clean.pnpm manifestleavessurface-manifest.jsonunchanged.🤖 Generated with Claude Code