Repository navigation
Train startup 2: a deferred island load reads nothing unseen, island-closed statements, the island's compile cache, build-time notes for guarded refusals, lazy initializers - #3392
Merged
Conversation
…sland
import { registry, bash, python } from "plugin-pkg"; // an island package
registry.register("bash", bash);
const languages = { bash, python };
for (const [name, language] of Object.entries(languages)) {
registry.register(name, language);
}
Each call above was lowered natively, and the first one started Node while
the program's modules were still initializing, whatever the build had
deferred. Their whole effect is on the island's state: no native code
receives a value from them, and none of their operands is native. The
island evaluates them itself now, each in a generated island module with
no export, loaded where the statement stands by the deferred load of a
static island import. Node starts at the program's first use of an island
value, and every deferred module evaluates first, in the order the loads
were registered: the program's.
A statement is closed when it is one call or `new` on island values with
closed operands (the rule of closed constants). A `for…of` is closed when
its bindings are `const` (a name, or an array pattern of names), its
iterable is closed, and its body is calls with closed operands;
Object.entries, Object.keys and Object.values of a closed value are closed.
A table is a constant of the island when it is a top-level `const`, not
exported, whose initializer is an object or array literal of closed parts,
and every identifier of its file spelled like it sits in a statement the
island evaluates: no native code can then read it or change it, and its
statement lowers to its generated module's load alone. The statements that
name a table are closed only if the table is the island's and the reverse,
so each is decided with the table taken as the island's and those
decisions are not kept. A shorthand member that names an island import is
closed like the member written out.
What keeps a statement where the program has it: everything that keeps a
constant there (the probe performs none of its effects and reads none of
its inputs; a statement that prints, throws, sets a timer or changes what
an earlier module's snapshot answers evaluates in the island where it
stands), and two things more. A statement that read the clock or random
numbers: a constant that fails when it finally evaluates raises its error
where its value is used, and a statement has no value, so its later
evaluation has to be the probe's. A statement that leaves a rejected
promise: Node reports it when the turn that made it ends; the probe
records such a promise as an effect of any load.
The comparison behind "a later island module's load changes its exports"
now covers what a snapshot answers of a function besides its identity
(its name, its length, whether instanceof against it is the ordinary
prototype walk).
The modules of a group that shares files carry each other's reads and
effects (loadReads). Every generated module shares the package's file with
the package's own import, so one statement that prints made that import
eager, and every other statement of the program with it: Node would have
started at the import instead of at the statement. A generated module is
loaded once, where the program has it, after the imports of the packages
it names, so what it read or did is its own body's: it carries the reads
and effects of the loads whose files it loads and of the generated modules
it imports, and gives its own to the generated modules that import it,
never to the packages.
tests/corpus/9340 to 9353 run under Node and natively with the same
output; tests/harness/node-island-closed-init.test.ts reads the build's
decisions and the boot trace for each (Node never starts while nothing
uses the registry; the first use finds every registration, in order; a
statement with effects, a native operand, a function of the program, a
table the program reads and a `let` binding stay as they were).
8750's `install();` and 8939's `registry.add(…)` are closed statements
now, and their tests say so.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The decisions in progress and each file's identifiers by text were kept in module-level WeakMaps keyed by the lowerer and the source file. Compiler sources keep caches on the object that owns their lifetime (compiler-weak-collections.test.ts): the compiler compiled by itself does not depend on which of its values can be weak keys. Both are fields of the lowerer now, named in its list of collections a unit's journal leaves out. Co-Authored-By: Claude Code <noreply@anthropic.com>
The test that named a module by its package directory went with the rule it pinned when these commits moved onto the train's shared-reads groups. Co-Authored-By: Claude Code <noreply@anthropic.com>
…ey make A module's top-level code runs before `main`, whether or not the program then uses what it built. A new backend pass, deferClosedInitializers, defers a group of module initializers when moving it cannot be observed: - closed: everything the initializers can reach (their statements, the functions they call, the closures they create) only computes on the heap: no output, no input, no scheduling, no dynamic-tier operation; - cannot throw: computeMayThrow, asked of exactly that code, so an error is never raised at the first use instead of at the import; - owns what it touches: every module global that code names is guarded everywhere in the program, and none is made by a module that stays (constants of a primitive type excepted). Initializers that share a global form one group behind one flag. Each member keeps its place in the module order: the importer runs the member's own imports there and notes that the place was reached. The first guard that fires runs the members reached so far, in order; after that a member runs at its place, as it always did. So a member never runs before its place, and after it only across code that touched nothing of the group's. Guards stand before the first statement of a function that names one of the group's globals, outside any loop and inside the branch that reads. A group the start-up code certainly uses, or one too small to matter, stays as it is, and with it the bytes of the programs that have only such initializers. Measured (Intel Xeon 2.90GHz): the new workload startup-unused-tables 2.33 ms to 1.31 ms per launch with two of its four modules deferred (the other two can throw and stay); the same program using every table 2.34 and 2.38 ms; 300 million reads of a module table through an accessor of another module 458 ms before and 443 ms after. Left where they are: the entry module, modules that can be loaded by import() or that await, import cycles, initializers with dynamic-tier operations or any operation that can throw, bindings with a temporal dead zone. Library builds and worker programs are not touched. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
A generated module stopped giving its reads and effects to the packages
whose files it loads, on the premise that those files evaluated in an
earlier load. The premise does not hold in three cases: the probe reaches
the generated module before the package's import (an import the order walk
does not reach is probed after the others), the package is not a candidate
at all ("evaluate": "eager"), or only a call of the generated module
reaches the file. The file's reads are then in the generated module's
record and the package's is empty, and the package, no longer in a group
with the generated module, deferred with a snapshot that could hold a
value computed from a private environment variable, with no guard.
A file belongs to the load that lists it first, in the probe's order. A
generated module still takes one way from the loads that evaluated the
files it got from them. A file that a generated module's load evaluated
first is a shared file like any other: every other load of it joins the
generated module's group, both ways, as before these commits.
Three tests beside the maintainer's: a package file reads a private
variable and the statement's module loads it, with the package first, the
generated module first, and the package's import where the walk does not
reach. In all three neither defers, both reasons name the variable and the
value is nowhere in the module. The last two failed before this commit:
the package deferred.
Co-Authored-By: Claude Code <noreply@anthropic.com>
… loaded from The pass that runs closed module initializers at the first use of what they make only placed modules of the entry's static import tree. A module that `import()` can load, or a static dependency of one, stayed where it was: on a large program that is nearly every module. Every call of a deferred initializer now goes to the module's place: an importer's header, the helper of an `import()`, a `require()` in a function. The order a group runs its members in is the order their places were reached at run time, kept as a list through one link slot per member, instead of a position in the static import tree. A module the program can load again after its evaluation threw has the error-recording form. A deferred module's own statements cannot throw, so the only error it can record is one of its imports: its place records that error where the initializer did and throws it again at every later load, and the module's statements never run, as before. Start-up code is now the roots' static import tree only: a module that only `import()` loads may never run. Guards stand inside a `try` body like inside a branch, since such a module's whole top level is one. Co-Authored-By: Claude Code <noreply@anthropic.com>
SCRIPTC_INITIALIZERS_TRACE=defer at build time prints one line per module initializer, in the form of the trace of deferred island loads: `[initializers] defer <file>: runs at the first use of what it makes`, or `[initializers] eager <file>: <the first reason found>`. Until now the verdicts were only reachable by calling the pass from a script. The variable reports on the backend, so it joins the names the frontend's output does not depend on: a build from a lowered program may set it. Co-Authored-By: Claude Code <noreply@anthropic.com>
Found with a table read through another module's one-line accessor, three
hundred million times: 614 ms on main, 607 ms with the first slice of the
pass, 1,054 ms with the list of places. The run of a group now holds the
loops that walk its list. Inlined into a guard, those loops sat inside the
caller's loop, which was then no longer optimized as an innermost loop; not
inlined, the store of the group's flag was hidden behind a call that could
write anything, so the loop tested the flag and loaded the table again in
every turn.
Two changes, measured together at 586 ms against main's 589 ms:
- The run is emitted `noinline cold`, by its name, beside the other
generated slow paths.
- A guard is `if (!active) { run(); active = true; }`. The store after the
call changes nothing for the program (the run sets the flag first) and
tells the optimizer the flag is set on every path out of the guard, so a
loop around an inlined accessor tests it once, ahead of the loop.
No workload of the bench set had this shape, so the set did not show the
cost. `module-table-reads` does: a hundred million reads through the
accessor and as many in the table module's own loop.
Co-Authored-By: Claude Code <noreply@anthropic.com>
…order-inputs The recorder's rule for a load's ambient inputs is written on the tree the startup-2 train will have: the startup-1 train head plus the closed-statement commits, merged as the same commit objects. Co-Authored-By: Claude Code <noreply@anthropic.com>
Differential plus llvm-differential on an 8-vCPU Sandbox: 3.0 s plain, 10.5 s sanitized. Co-Authored-By: Claude Code <noreply@anthropic.com>
A program with a Node island compiled every island module again at every start. Node can keep V8's bytecode for the modules it loads in a directory and load it at the next start (module.enableCompileCache); a program now has such a cache by default, so every launch that starts Node starts it faster. A program that loads the `typescript` package in an island starts in 142 ms instead of 287 ms; one whose island never starts does nothing new. Where. Beside the island's extracted module tree and named by it: <cache home>/scriptc/node-islands/<tree name>.compile-cache. The function that decided where the tree is installed (the first half of ni_cached_root) is now ni_islands_dir, used for both. A rebuilt program has another tree, another path for every module and another directory. Trust. Node checks nothing about the directory: it creates what is missing with mode 0777 and loads any entry whose CRC-32 sums agree, and an entry is code. The runtime creates the directory 0700 inside the islands directory, opens it without following a link and uses it only when it is a directory of the process's user that no one else can write. No cache for the $TMPDIR copy of the tree, when the real and effective user or group differ, or while the file system has no free space (Node leaves an empty temporary file behind for every entry it fails to write). NODE_COMPILE_CACHE names the user's own directory, which Node enables by itself; NODE_DISABLE_COMPILE_CACHE turns the cache off. A refusal says nothing: the cache changes speed, not behaviour. SCRIPTC_NODE_ISLANDS_TRACE=compile-cache prints the decision. How. The directory reaches Node as a second global in front of the bootstrap, which calls enableCompileCache before any module of the program loads: no entry into JavaScript that did not happen before, and the process's environment stays as it was. When it is written. Node writes new entries when its environment ends: on a normal return and on process.exit() called by island code. process.exit() from native code ends the process with Node running, so the island unit wraps the events unit's exit hook when Node starts with a cache and writes the entries there, after the 'exit' listeners and the program's output (0.008 ms when there is nothing new). The bootstrap also writes them once, a second after the start, from a timer that keeps no program alive, for a program that only ever ends by a signal. Tests: tests/harness/node-island-compile-cache.test.ts pins where the cache is, which launches write it, and each way the directory can be unusable or not the user's own. The tree tests pick trees by name now that each has a sibling directory. Co-Authored-By: Claude Code <noreply@anthropic.com>
…ferently A deferred island load whose evaluation read the machine or the moment kept the build's answer in the executable whenever the load recorder did not list the source: the time zone, the default locale, a date string without a zone, performance.now(), a member only a terminal's stream has. And a load that read a clock the recorder did list was deferred without constants and evaluated when Node started, after the program's own later readings. The recorder was a list of sources it knew to be dangerous. It is now a list of what it knows to be safe. Every member of process, of the global object, of the standard streams and of the listed core modules has a rule or is on an allowlist; a builtin module on no list is not classified when a load asks for it; an ES module's import of a builtin's member by name is the read (Node copies the value into the namespace once, so the second importer of process.pid was recorded as reading nothing). What is not classified keeps the load eager, and the trace names the source and the place that read it. The classes: deterministic (the language, what the libnode pack fixes, the package's own files); guarded (an environment variable, also behind os.homedir(), os.tmpdir(), util.debuglog(), the default locale on Linux and a time zone that TZ names; whether a standard stream is a terminal, also behind tty.isatty() and the members only a terminal's stream has); an input (the arguments, the working directory, the user, the host, the time zone of a machine whose TZ is unset, a terminal's size); a reading no run repeats (a clock, a random number, the process id, a meter). A load that read an input or such a reading evaluates where it is imported. That replaces the rule that a load which read the clock snapshots without constants: read late, a clock comes after the program's own readings; a snapshot without constants still answers which members are objects from the build's reading; and a load probed after it can hold, as a constant, what the first left in a global. "evaluate": "lazy" still defers such a load, and its snapshot now holds no constants for any unguarded read (it kept the build machine's host name and time zone). The recorder's Date gave its instances an empty prototype, so every load that made a Date threw in the probe and stayed eager for that. Dates work, and a Date made from a timestamp or a zoned string is deterministic. A string of a snapshot that names a directory above the module tree, as module.paths lists them, is a path of the build machine like one under it. Co-Authored-By: Claude Code <noreply@anthropic.com>
… for The docs sentence about the `typescript` package in an island carried the numbers of a run that shared its machine; these are from a quiet one, two runs of 61 launches per cell taken in turn (Intel Xeon 2.90 GHz, 8 vCPU): 205 ms without the cache, 102 ms with it, and a first start of 244 ms instead of 218 ms. The bootstrap's comment claimed that the write a second after the start delays no program's first output. A program whose first output comes later than that can wait for it once, on the launch that fills the cache; the comment now says what the second is for. Only the comment changes, and with it the bootstrap text the island unit embeds. Co-Authored-By: Claude Code <noreply@anthropic.com>
The recorder's text is a compiler source, and the guard that keeps weak collections out of the compiler's own sources reads it as one (compiler-weak-collections.test.ts). The map lives as long as the probe process, which ends with the probe. Co-Authored-By: Claude Code <noreply@anthropic.com>
…ry run
The deferred-refusal report writes to stderr the first time a JavaScript
statement that does not compile runs, so that a handler that catches its
Error cannot hide a compiler gap. For one kind of site that turns a program
that matches Node into one that does not: `try { optional() } catch {}`, the
way a program asks for something that may not be there. The program's own
handler deals with the failure, its output is the same on both paths, and
the report added two lines to stderr at every start (found on a command-line
tool whose entry file begins with such a call).
A deferred refusal that sits in the `try` block of a `try`/`catch` of the
same function is now named by the build and not by the binary:
- The build prints warning SC6012 at each such statement, with the refused
construct. Its text says what follows from it: the statement throws when
it runs, so the `catch` clause runs where Node's would not, and the
program's output can differ from Node's from there on.
- The binary writes nothing for such a site. SCRIPTC_DEFERRED_REPORT=all
reports these sites at run time too; SCRIPTC_DEFERRED_REPORT=0 still
silences every site.
- Every other reached refusal is reported as before: a site in a function
the `try` block calls or declares, in a `catch` or `finally` block, or in
a `try` that has only a `finally`. The handler that swallows those is not
the statement's own.
"Its own try" is decided where the source `try` statement is lowered
(lowerTry marks the deferred-diagnostic fences of the block when the
statement has a `catch` clause), so the try/catch nodes the compiler writes
itself do not count, and functions declared in the block are lowered apart
from it. The mark is a field of the runtimeFence node; the backend picks
scr_deferred_fence_guarded for a marked fence, and the warnings are read
from the lowered module beside the divergence scan, so a replayed unit
reports the same sites.
A silenced site no longer takes the report's one-line heading with it: the
heading goes to the first site that is reported.
Tests: tests/harness/deferred-fence-report.test.ts (a guarded site whose
stdout and stderr equal Node's; a guarded site whose catch takes another
path, named by the build and not by the binary; the lexical cases, guarded
through loops and an inner try/finally, not guarded in a try/finally, a
catch block, a finally block, a called function and a function written in
the block; both settings of the variable), ir/deferred-fence.test.ts.
Co-Authored-By: Claude Code <noreply@anthropic.com>
…rder Node's own Headers, Request and Response mark themselves uncloneable when they are made, through the public worker_threads.markAsUncloneable that undici takes when it loads. Whether the recorder saw that call depended on whether undici had loaded before the recorder wrapped the core modules (describing a lazily defined global such as MessageEvent loads it), so a load that made a Headers was eager with one order and deferred with the other. A mark on an object the caller holds reads nothing and does nothing to the machine: the three functions are on the recorder's allowlist, and a load that makes these objects is deterministic by rule. Co-Authored-By: Claude Code <noreply@anthropic.com>
…it on corpus programs Three corpus programs hold a refused statement inside its own try whose refusal coincides with Node throwing (a guarded require of a module that is not there through a run-time createRequire base): they match Node byte for byte and are named by the warning. Its text claimed that the catch runs where Node's would not; it now says that the catch runs also where the statement would have completed under Node. The whole-corpus coverage test, which treats every divergence warning on a corpus program as a false positive, leaves this one out for the same reason. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Members: the load recorder as an allowlist (9400903, b135ce7, aff4136) and, under it, instant-start-2's island-closed statements and loops (54e051a, 3c2dd49, d906f7f, a505b9a). Conflict: docs/content/docs/limitations.mdx (the include list): main's list plus this branch's bullet. A merge without a conflict that did not type-check: the train's builtin-patch rule (c589650) calls traceClosed with the signature it had before the closed statements gave it the lowerer; the call now passes the lowerer. Co-Authored-By: Claude Code <noreply@anthropic.com>
…iltin module The start-up train's rule (c589650) keeps a closed island constant where it is written when a module of the program patches a builtin module: Node would read the generated module through the patch. The island-closed statements and loops of this train are generated modules too, and the rule stood only in the function that decides a constant's initializer, so `mark("x");` after `installPatch();` was read through the patched fs.readFileSync. One function, readThroughPatch, now answers for a constant, an expression statement and a loop. A table is the island's only through a statement that is, so it stays native with them. Test: tests/harness/node-island.test.ts, "statements, a loop and a constant after a builtin patch are evaluated where they are written". Co-Authored-By: Claude Code <noreply@anthropic.com>
Node's module compile cache as the default for a program's Node island. Conflicts, with islands-1 (#3377) and startup-1 (#3350) in the island bootstrap: - scr_node_island.c, ni_bootstrap: main's process state (argv, exiting) first, then the compile cache's directory; the buffer counts both. ni_node_ending is set right after the stop job, before main's scr_timers_teardown. - scr_node_island_host.cjs: the cache is enabled first, then main's process state is applied. - scr_node_island_host.h: regenerated by scripts/node-island-host.mjs. Co-Authored-By: Claude Code <noreply@anthropic.com>
A deferred refusal inside the program's own try block is named at build time (SC6012) and not reported at run time. Conflict: packages/compiler/src/diagnostics/codes.ts (the code table): main's SC6008 and this branch's SC6012, both kept. Co-Authored-By: Claude Code <noreply@anthropic.com>
A module initializer that only computes runs at the first use of what it makes, wherever the module is loaded from. No conflict. Co-Authored-By: Claude Code <noreply@anthropic.com>
The compile cache of this train keeps `<digest>.compile-cache` beside a program's island module tree, so the cache home's node-islands directory holds two entries per program. The load plan's test (#3236) counted the entries; it now takes the tree by its digest name, as tests/harness/node-island.test.ts does. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
Author
yowainwright
pushed a commit
to yowainwright/scriptc
that referenced
this pull request
Nov 9, 2026
…'s code
The build's load recorder classifies a locale-sensitive call before the
call runs. To do that it read the arguments itself: `options.timeZone`,
the locales through `supportedLocalesOf`, the `lastModified` of a File's
options. A getter, a proxy trap or a conversion of the load's own then
ran once in the recorder and once in the call. A load whose getter
counts its reads, or answers differently the second time, computed
another result in the build than under Node, and since the recorder had
found the call deterministic, that result went into the executable as a
constant: `new Intl.DateTimeFormat("en-US", { get timeZone() { … } })`
answered the getter's second zone, with exit status 0.
The recorder now looks at an argument only where the engine answers
without running program code: a data property, own or inherited, of an
object that is not a proxy; a string, an Intl.Locale (through its own
slot) or a plain array of those as locales; `supportedLocalesOf` as it
was before any load; util.types.isDate in place of instanceof. An
argument it cannot read that way is an input: the load evaluates where
it is imported, and the build trace names it ("reads an options
argument that only the load's own code can answer").
Found in review of vercel-labs#3392.
Tests: corpus 9817-node-island-load-reads-argument-accessors (three
island packages: an options getter, a proxy, a tag converted from an
object, an element getter, an array-like with a length getter, a File
option getter), which printed `zone: UTC 2` where Node prints
`zone: Asia/Tokyo 1`; a unit test in probe-recorder.test.ts with seven
such shapes compared with Node and four plain shapes that stay
deterministic.
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.
Train
startup-2: one rollup of five branches about what a compiled program's Node island does when the program starts. None of them had a PR of its own; each was proven by its owner in Vercel Sandboxes and handed to this train. The branch merges their heads (merge commits; the members' commits are unchanged), resolves what they meet in each other and in main once, and was validated once on the final tree.Read this first: one start-up number moves, and one project setting brings it back
The load recorder becomes an allowlist (below). A package whose load draws a random number is then evaluated where it is imported, not deferred. For pi that is two packages,
minimatchand@google/genai(Math.random()at load, inbrace-expansionandbignumber.js). minimatch is the third load of a start, so Node starts there.pi --version, HOME without settings, measured on the pi run tree (the headline build; medians of 15 launches alternating with Node, one Xeon 2.90 GHz sandbox): 92 ms before, 246 ms with this train and pi as it is, 88 ms with the two packages marked lazy.--help,-pand the first frame start Node anyway and do not move (272, 367, 272 ms against 272, 352, 280).packages/coding-agent/package.json, beside theproper-lockfileentry the headline build already has:"scriptc": { "packages": { "minimatch": { "run": "node", "evaluate": "lazy" }, "@google/genai": { "run": "node", "evaluate": "lazy" } } }."lazy"is the user's statement that what the load draws and leaves behind does not matter; the build cannot make it.What lands
os.homedir(),os.tmpdir(),util.debuglog(), the default locale on Linux, a time zoneTZnames, whether a standard stream is a terminal), an input, a reading no run repeats (a clock, a random number, the process id), or not classified; a load that read one of the last three evaluates where it is imported, and the build trace names the source and the place."evaluate": "lazy"still defers such a load, and its snapshot then holds no constants.newon island values with closed operands, afor…ofwithconstbindings over a closed iterable whose body is such calls, and a table only those statements name each become a generated module that waits like a closed constant does.tryis named at build time, not reported at every run. A JavaScript statement that does not compile and sits in thetryblock of atry/catchof the same function gets warningSC6012at build time and prints nothing when it runs; every other reached refusal is reported on stderr as Report a deferred refusal on stderr the first time it fires #3211 does.SCRIPTC_DEFERRED_REPORT=allreports these sites too.import(), arequirein a function).Members
None of the members has a PR, so nothing is superseded on GitHub. Heads are the commits merged; each owner's hand-over, with its raw output, is in the fleet's records.
fix/load-recorder-inputs(aff4136efd, three commits): Defer an island load only when it read nothing a run could answer differently. Owner's proof: full round on its first commit, gate--gate-corpus=allPASS with no allow list (5,482 identical, 188 fail to build on both sides), both lanes with both native bootstraps (two tests red: one the base's own, one its own and fixed by its second commit), bench 68 workloads geomean +0.7%; island executables 179 of 180 byte-identical; the 220-row table (main 54 silent, the change 0); docs gate 25 of 25.feat/closed-island-statements-train(a505b9a55b, four commits; the branch above is built on it): Evaluate island-closed module-level statements and loops inside the island. Owner's proof: gate PASS (5,353 identical, 161 both-fail), island corpus plain 364 of 364 and sanitized 182 of 182, island harness 140 of 140, three unit tests for the rule that a file belongs to the load that lists it first in the probe's order; a statement program that never reads the registry starts in 0.5 ms and 12.9 MB instead of 35.1 ms and 90.4 MB. Its first commit (the boot-guard fix) reached main with Train startup 1: start Node for island packages only when the program needs it, and answer island module resolution at build time #3350.feat/island-compile-cache(2c0b592d6d, two commits): Keep Node's module compile cache for a program's Node island. Owner's proof: round with gate PASS and no allow list (5,493 identical, 125 both-fail), lanes red only in tests that were red on main that hour, 16 security and robustness cases as tests; a program that runs thetypescriptpackage in an island starts in 102 ms instead of 205 ms warm, and its first launch takes 244 ms instead of 218 ms.fix/deferred-report-guarded-sites(c9d6bf5fc3, two commits and a merge of startup-1): Name a refused statement inside its own try at build time, not at every run (the orchestrator's decision of 00:16 UTC for the Vercel CLI, whose entry point callsmodule.enableCompileCache()in atry). Owner's proof: lanes plain 23,938 passed, 0 failed, sanitized 20,581 passed, 0 failed; gate PASS, 5,499 identical, 278 both-fail, 36 allowed by id.perf/lazy-initializers-2(be2dcc2b12, five commits): Run module initializers that only compute at the first use of what they make, wherever the module is loaded from. Owner's proof: gate PASS (5,457 identical, 8 allowed: the compiler's own three programs and the five the branch adds), both native bootstraps, bench 69 workloads withstartup-unused-tables3.09 ms to 1.97 ms; lanes red only in the island value matrix, which was red on main at its base.Not on the train, and why:
feat/binding-forms-nativemerged cleanly and was dropped before the round, because ten of its own eleven tests intests/harness/errors.test.tsno longer fail the way they pin on this base (main now gives a builtin module held in an object literal a value); it goes back to its owner.feat/settled-var-forwardconflicts with main in the analysis #3169 changed. The "late class" for loads that draw a random number is not provable (its owner's verdict). Shared globals' conditions have no hand-over yet.Conflicts resolved on the train
frontend/lowering/closed-island-init.ts: a merge without a conflict that was wrong twice. Startup-1 (c5896507d0) keeps a closed constant where it is written when a module of the program patches a builtin module, because Node would read the generated module through the patch. On this train a statement and a loop are generated modules too. The rule's trace call had the signature from before the closed statements (it did not type-check), and the rule stood only in the function that decides a constant. One function,readThroughPatch, now answers for a constant, an expression statement and a loop; a table is the island's only through a statement that is, so it follows (9845861a02). Test:tests/harness/node-island.test.ts, "statements, a loop and a constant after a builtin patch are evaluated where they are written"; without the rule on statements that program ends with exit 1 andReferenceError: patched is not definedin the generated module (measured on the train tree with the file as the first merge left it).packages/runtime/src/scr_node_island.c,scr_node_island_host.cjs(the compile cache against islands-1 Train islands 1: errors, promises, process events and process.stdout.write keep Node's behaviour across the Node island boundary #3377 and startup-1 Train startup 1: start Node for island packages only when the program needs it, and answer island module resolution at build time #3350, which give the island the program'sprocess.argvand exit state): the bootstrap carries main's process state first and then the cache's directory, and its buffer counts both; the host script enables the cache before anything else and then applies the process state;ni_node_endingis set right after the stop job, before main'sscr_timers_teardown.scr_node_island_host.his regenerated byscripts/node-island-host.mjs.tests/harness/node-island-load-plan.test.ts(Answer island module resolutions from a load plan computed at build time #3236's test against the compile cache): the test counted the entries of the island cache directory, which now holds<digest>.compile-cachebeside the tree; it takes the tree by its digest name, asnode-island.test.tsdoes (0e2c05261d).docs/content/docs/limitations.mdx(the include list): main's list plus the new bullets.packages/compiler/src/diagnostics/codes.ts: main'sSC6008and this train'sSC6012, both kept.ir/codec.generated.tsandir/fields.generated.tspass their generators'--checkon the merged tree.node-islands/snapshot.ts, the file startup-1 merged by hand: the allowlist's and the closed statements' hunks merge with startup-1's last head without a conflict, and the result is byte-identical to the merge another agent built and tested on that head. The maintainer's private-environment rule and its five tests are untouched.Validation (once, on the final tree, in Vercel Sandboxes)
Everything below ran on
0bd2727c9e, the commit pushed, against main58993e3690(the commit merged into it), in one round (validate/runs/20261108T033059-train-startup-2) and one 32-vCPU sandbox beside it. Nothing was built or tested on a workstation.pnpm buildsteps: tsc inpackages/cli,packages/compiler, the hono starter fixture).--gate-corpus=all: PASS. 5,507 programs identical, 308 fail to build on both sides (programs that need a libnode pack; the gate has none), 44 allowed by program id, none other differs, no runtime-only row outside the list. Compile time within tolerance: bench geomean +0.63% [-0.20%, +1.08%], self-compile -0.84% [-1.96%, +0.48%] (four pairs), fixtures -0.09% [-1.57%, +1.38%]. The candidate compiler builds its own source.scr_deferred_fence.c, which the change edits: 35 executables grow by 104 to 280 bytes (at most +0.05%; IR and LLVM identical in 33 of them), and three have the refusal inside its owntry, so their start function calls the guarded entry (%init.0only:8717-require-missing-module,create-require-base-evaluated,create-require-factory-base-evaluated). No program without a deferred refusal links the unit; no benchmark is among them.self/cli,fixture/llvm-emitter,fixture/llvm-layouts: module initializers that only compute are deferred in them; the two native bootstraps below are the proof that the compiler built that way still compiles itself), and the five the member adds, which main's compiler builds without deferring anything (corpus/9651,9652,9653,bench/startup-unused-tables,bench/module-table-reads; -0.38% to +0.76%). No program that exists on main differs.scr_deferred_fence.candscr_node_island.c(the compile cache; only island programs link it).commands(1,771 s) andrebuild(1,722 s). Sanitized:commands(1,432 s),rebuild-emit(2,145 s) andrebuild-frontend(2,861 s). CI runs the native bootstraps only for a PR that touches bootstrap paths, and this one does not, so this round is the evidence for them.6883-node-island-builtin-handles,8918-node-island-closed-const-prints,8932-node-island-closed-const-caught-refusal, each about 13 KB larger: the package's interface snapshot). The three programs of main with a module that patches a builtin (8722,8840,8883) are byte-identical: their statements stay where they are written.node-island*files, the deferred report's file anderrors.test.ts): 360 tests, 0 failed. Island value matrix: 239 of 239 cells.startup-islands-cold+9.0% [+3.8%, +11.3%]: the first launch, which fills the compile cache; by design, and the owner's round measured the same (+6.8%).startup-islands-used-4.0% [-5.6%, -2.4%],startup-islands-unusedneutral.startup-unused-tables-23.9% [-33.4%, -6.8%] (the lazy initializers' workload).atomics-contended+3.2%,regex-logs+3.8%,dynamic-class-tests-7.0%: the gate reports all three executables byte-identical to main's, so these are the machine.pnpm lint0 warnings,pnpm format:checkclean,pnpm test:ts7 --baselines-onlycomplete,generate-ir-codec.mjs --check,generate-ir-fields.mjs --checkandnode-island-host.mjs --checkpass,pnpm manifestandpnpm node-compat --localchange nothing,pnpm node-compat:checkpasses.NEXT_DIST_DIR=.next-check pnpm checkindocs/): passes, 25 of 25 route tests.closed-island-init.tsas the first merge left it, the new test ends with exit 1 andReferenceError: patched is not defined; with9845861a02it passes.What was not run
fstatvfsandclock_gettime).🤖 Generated with Claude Code