Repository navigation
Conversation
A file the island decision put in the set is no longer compiled natively: it is keyed by its absolute path, its declarations type the program as foreign, its diagnostics do not fail the program the build does not compile, and every import or require of it lowers to an island load. `--node-island=<path>` therefore takes effect for a user module, not only for a package. The recovery stays one round: when the lowering refuses a user JavaScript module, the reload adds every such module at once and each one is reported once as an SC8001 note naming the module and its reason. The build reports the islands it ran (packages and modules) with its result, and an executable cache hit reports them again, so a runner can tell an island result from a native one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three pre-scan markers name JavaScript whose semantics need the engine: a direct call of `eval` (it reads and writes the caller's scope), a function declaration in a block in sloppy code (Annex B.3.2 also binds the name as a `var` in the enclosing function), and `this` in a sloppy plain function (a plain call gets the global object). `with` now marks only sloppy code, where strict code is an early error Node refuses anyway. The `this` marker is conservative by design: a file cannot see its call sites, so every `this` in a sloppy plain function marks the module, except the shapes the file itself proves are receiver calls — a `new`-only constructor and an `X.prototype.name = function` method, neither of which is also called bare (decision S5). Markers narrow, never move, the native passes: inside a function body a block-level declaration is ordinary, the comma and `globalThis` spellings of `eval` stay indirect, and a file that does not parse as sloppy JavaScript keeps none of the sloppy markers, so a parse-negative test stays the compile refusal it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under the default `auto` mode a user JavaScript module whose pre-scan carries a sloppy-only construct no longer stays a refusal: it joins the island set, so the program runs it in Node at the position it is required (decision S3). The marked forms (`--node-island=<path>`, `scriptc.nodeIslands.modules`) keep working unchanged, as does the existing lowering-driven recovery. The entry stays native: a program whose entry is an island would run the whole file in Node, which is a different feature and not this slice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four programs whose user module needs the engine, each run under Node and as a compiled binary with byte-identical stdout, stderr and exit status: `with` (including the shadowed and nested lookups), Annex B.3.2 block functions (before, after and inside a loop), direct `eval` (reading and writing the caller's scope, and the `var` it declares landing in the caller's function), and `this` in a plain call (plain, `call`, `apply`, bound, and an arrow function's enclosing `this`). Each entry stays native and requires its module, so the programs exercise the module island path end to end rather than a whole-file island. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The profile gets an `--islands` lane (decision S4). It lifts exactly the five host exclusions a Node island can express — eval, Function, arguments, script-environment, globalThis — on sloppy variants only; harness limits, the assertion surface and `$262` stay excluded, because the adapter still runs inside the island. An island variant runs as a tiny native entry that imports the adapted test, so the pre-scan can island the test module. When the build reports that module in its islands, the outcome is `island-pass` or `island-fail`: Node proved the semantics, so it is never counted as a native pass and an island failure of a lifted variant is not a native regression. The report records `islands` and `liftedVariants`, `--list` marks lifted cases, and the harness exports each assertion helper so the island can rebuild `assert` in JavaScript (a native function crosses the bridge as a callable without its namespace members). `--filter` is now repeatable, so one run can select a measured set of files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Detect writes through a builtin module's CommonJS object (direct,
aliased, namespace, defineProperty, Object.assign, delete) as the new
island reason `builtin-patch` (R2.5), in Node islands they are collected
alongside npm and user modules, and the SC8001 note names the patched
member. A patching entry module is refused with SC8006 ("move the patch
into a module of its own"), as is a patcher whose TypeScript needs more
than Node's type stripping. A native module that reads a patched member
through the CommonJS object gets SC8020, and npm packages that read one
join the island set. Program modules also ship data files they name
beside themselves, since an island loads from the extracted tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`8423-accessor-member-kind-mismatch.ts` already owns 8423 on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pi re-measure (CENSUS-2026-10-29-int3) lists 27 refusals under its Node island class. Read as reasons, they split into two very different groups: five are needs of the JavaScript runtime itself (dynamic module loading, a worker entry path, a host class base, a require() the runtime owns), which an island can serve, and the rest are compiler gaps or another family's work. Without a reason id the report cannot tell them apart and counts every one as a gap. The refusal sites that predate reason ids now map to their rule: 'import.meta.resolve with a runtime-computed specifier' and 'this Worker entry point' are dynamic-resolve and worker-entry (both SC2020 in the census, SC1090 elsewhere, so a shape may name both codes), the CJS namespace import of a program module is cjs-namespace, and 'extending classes not declared in the program' is host-class-base (R1.6). The test table pins the exact census message shapes, taken from results/pi-run/cov/build-rem3-2026-10-29.txt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The island module tree refuses a program module whose loads it cannot trace: the module runs in Node from the shipped tree, where nothing else provides what it loads, so a computed import() or require() would fail there where the native build ran. The refusal pushes SC8006 and throws PoisonError, which a statement fence around a module body catches. Collection runs before those fences: collectIslandImport and collectIslandRequires are called from collectNpmImports, outside any fence, so the same refusal escaped as an uncaught PoisonError and the CLI died instead of reporting. A module that patches a builtin (H3) and loads a computed specifier is exactly that shape, and it is the first thing a migration of the pi census sites meets. The two collection entry points now catch their own PoisonError: the diagnostic the loader pushed is the build's answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A user TypeScript module that extends a class only the JavaScript runtime
has ('extending classes not declared in the program') needs the runtime
itself: the base class exists in Node, and native code cannot inherit it.
The island module tree carries such a module as written, so it is the one
runtime need an automatic island satisfies today, and the pi census's
openai-codex-responses site is exactly it.
Under --node-islands=auto, loweringModules now grows an island module for
that reason, as it already does for sloppy-only JavaScript modules. A
TypeScript module joins only when Node can run it as written: the scan's
stripProblem check refuses a file that needs a transform or resolves a
relative import without its extension, and the refusal then stays the
build's answer instead of a broken island. A load the shipped tree
cannot trace stays refused too (SC8006), so nothing diverges silently.
The other refusal reasons an island could serve are deliberately not
grown: a computed specifier, a worker entry path or a program-owned CJS
namespace needs the tree to ship what the module loads, which is a
shipping decision, not a lowering one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A module that runs in a Node island starts Node's own Worker, in a thread
with its own module instances and its own loader hooks. The island tree
ships program modules the island also uses as placeholders that only the
island thread's hooks answer, so a `new Worker(new URL("./worker.ts",
import.meta.url))` in an island module could not load its entry at all.
The tree now ships the entry and its static import subtree as real
sources: the collector asks a new scan (frontend/island-worker-entry.ts)
for the worker entries a module's shipped copy starts, and a fresh module
is shipped as its source even where the program provides a placeholder
(the worker thread never runs the island thread's hooks). Entries are
accepted only in the form the tree keeps, a path relative to the module
its own URL names; a computed entry, or a path that names the build
machine, stays refused as SC8006. A TypeScript file that ships this way
must pass stripProblem (a file the build never put in the program is
parsed and checked syntactically), because Node erases types in a worker
thread too, and refuses to below node_modules.
Corpus 6901 covers it: an island module starts a worker on a program module
that imports a helper, messages both ways, and the worker's helper state
stays separate from the program's, identical under Node and native.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scan parsed a file the program never held with the parser island's rpc-api module. The compiler is itself compiled natively, where rpc-api and rpc-process cannot lower, so the self-hosted build failed on surface those modules use (process.report, a computed createRequire, a spawn stdio option). The scan reads only the program's own sources now: a worker entry is a program root and its import graph is in the program, so the files the tree ships are the ones the checker bound. A module outside the program has no checked source, and its Worker stays Node's own business, as before the tree carried it; the limitations page says so beside the refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ip nested worker entries, and keep one compareArray
- A native module that destructures a patched builtin's CommonJS object
(`const { readFileSync } = require("fs")`, nested patterns, an alias of
the object) or reads it through require() directly
(`require("fs").readFileSync`) now records those member reads, so the
patch check reports SC8020 for them as it does for `fs.readFileSync`.
- An island module ending in .mts is an ES module and one ending in .cts
is CommonJS, whatever the nearest package.json says (isEsmFile and the
effect gate's copy), so a default import of an islanded .mts file in a
CommonJS package is its default export.
- A module of a worker entry's subtree runs in the worker thread from the
shipped tree even when the program also compiles it, so its Worker
constructions are scanned and their entries ship. The compiled-file
guard now applies only outside a worker subtree.
- The Test262 harness exported assert's compareArray as `compareArray`
beside assert.js's bare `compareArray` function, a duplicate identifier
that made every harness compile refuse. Assert's member is now
`assertCompareArray`, and the island lane binds the bare function too.
detect.test.ts covers the destructured and direct reads; node-island.test.ts
covers SC8020 through destructuring, the .mts default export run against
Node's output, and the nested worker entry in the shipped tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged origin/main: kept main's |
a92e4fa to
b2d7f5f
Compare
|
Follow-up after the merge: the reason-id map now names the refusal behind the fenced-SDK coverage fixture (R1.6 host-class-base), so the report classes ni-sdk as runtime rather than the unnamed gap; the coverage snapshot and that test's class expectation are updated to match (Linux CI had failed on exactly that snapshot). |
b2d7f5f to
e583703
Compare
|
Follow-up on the merge: the lowerer's journal test required the branch's two Node island memos ( |
One conflict in packages/compiler/src/node-islands/detect.ts.
Both sides generalised CJS read recording in the same place: main through
its `bindingAt` lookup (BindingValue builtin/cjs/createRequire) and the
branch through `cjsBuiltinObjects`. Kept main's `bindingAt`-based
`recordCjsRead` and ported the branch's additions onto it: the branch's
direct `require("fs").readFileSync` case (a chain with no identifier root)
now resolves through `isCjsObjectValue`/`builtinOf`, `isCjsObjectMember`
asks `isCjsObjectValue(root)`, and `memberBaseOf`,
`STANDALONE_SUBPATH_MODULES`, and the `recordCjsPatternReads` walk are
unchanged.
The branch's destructured-read test expected SC8020, but main's entry-read
rule makes any entry-module read of a patched member SC8006, and the test's
program reads in the entry: the same shape as main's "the entry module
reading a patched member is SC8006" test. The test now asserts SC8006.
Merged, the branch's reason-id map now names the refusal behind the
fenced-SDK fixture's island (R1.6 host-class-base), so the report classes
ni-sdk "runtime" — it needs the JavaScript runtime — instead of the unnamed
"gap" a refusal without a reason id falls back to. The coverage snapshot and
that test's class expectation were updated to match.
The lowerer journal test also required the branch's two island memo fields
by name: islandWorkerScans and islandWorkerCompiled are made on first use
from the program's modules, and the compiled set is made again whenever
moduleOrder grows (a checked table), so they are listed as caches in
Lowerer.unjournaled.
The npm suite's web-streams fixture expected a deferred SC1090 refusal, which
predates this merge: the branch's worker-entry scan ships the modules an
island starts, so the package builds and runs. Its entry is now the island
form the branch's node24-util-types change uses, so the harness compares the
program against Node with a libnode pack and expects SC8005 without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Follow-ups on the merge: kept main's bindingAt/recordCjsRead in detect.ts and ported the branch's cjsBuiltinObjects additions onto it; the fenced-SDK island now reports R1.6 host-class-base (class "runtime"); the lowerer's two island memos are listed as caches in Lowerer.unjournaled; and the npm suite's web-streams case (a pre-existing red on this branch) is now the island form its node24-util-types twin uses, since the worker-entry scan ships the package and the program runs. |
e583703 to
aad9018
Compare
One conflict, in lowerer.ts: the list of Lowerer fields a recorded lowering unit may leave unset. This branch added islandWorkerScans and islandWorkerCompiled where main added fixedFieldFamilies, familyDeletedNames and familyInstanceAnswers; the merged list has all five. Co-Authored-By: Claude Code <noreply@anthropic.com>
Main moved while the previous merge was being tested and conflicted again, in the Node island scan's options (pipeline.ts and scan.ts). Both sides gave scanFiles one more way to ask for a TypeScript module's strip problem: this branch `stripFor`, the files a build is considering for an island, and main `marked`, the modules the policy marks (#3176). The two are passed by different callers. The merged scan takes both and computes the strip problem when either asks. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Merged main into this branch, twice, because main moved while the first merge was in test: What conflicted. Both conflicts were two sides adding an entry at the same place.
One thing for whoever takes this PR out of draft: Re-run on
Two failures that are main's, not this branch's. Both reproduce on an untouched main
Not re-run: the zero-cost gate and the native bootstraps. The resolutions add list entries and one more optional input to a scan; no lowering, runtime or backend code was resolved by hand. |
Stacked on
fix/sloppy-builtin-patch-islands(#2591). Review and land that PR first; this branch adds five commits on top of it and is a second PR on the same work.What this does
dynamic-resolve(a runtime-computed specifier,SC2020in the census andSC1090elsewhere),worker-entry(a Worker entry path),cjs-namespace(a CommonJS namespace import of a program module) andhost-class-base(a class extending a base class the program does not declare, R1.6). Of the 27 refusals the pi re-measure lists under its Node island class, five are needs of the JavaScript runtime itself and now carry a reason an island can serve; without a reason id the report counted every one as a gap.cjs-namespaceis the reason the B3 implementer needs.6900-node-island-host-class): a module whose class extends a base class only the JavaScript runtime provides (the census'sclass extends WebSocketshape;net.Socketand friends look the same) is an island, so the base class is the real one and the entry stays native.6901-island-worker-entry): when an island module starts a Worker, the entry file and the modules it pulls in are written into the island tree, so the worker runs from the same island copy the parent loaded from. The worker scan reads the program rather than the TypeScript parser, so a worker entry named in plain JavaScript is found too.Zero cost
The reasons are recorded, not decided by: a program with no island compiles to the same IR, LLVM and executable as before. The worker-entry and host-class decisions only run for modules already classified as islands.
Tests
6900-node-island-host-class,6901-island-worker-entry.tests/harness/node-island.test.tsgrows by ~286 lines: the reason names, SC8006 for an untraceable load, host-class modules, and worker entries with their subtree (needs a libnode pack), plain andSCRIPTC_SAN=1.node-islands/classify.test.ts,node-islands/modules.test.ts.pnpm lint,pnpm format:check,pnpm test:ts7 --baselines-only.Gate
bin/validate islands-reasons-workers --skip=lanes,bench,t262,san-bootstrapvs base794ede51d: PASS — 3415 identical, 50 both-fail over 7 shards, zeroruntime-only,allowedordiffersprogram lines; the candidate compiler source builds with the candidate (700.3s); self +1.34% within tolerance (bench timing came out inconclusive under a loaded pool, with byte-identical binaries).🤖 Generated with Claude Code