Repository navigation
Train lang 2: one identity for empty arrays, every argument through function slots, copy warnings from the lowered program, holed arrays in generic loops, typed promise resolution, built-in member writes refused where read natively - #3368
Conversation
… elements in array conversions with a TypeError An array that can hold a hole or undefined, read by a for-of loop inside a generic function, method or class, aborted with `scriptc: RangeError: array index N is a hole` (or `is undefined`) where Node carries on or throws a catchable error. The element-state analysis keeps its facts per element key, a generic array has no key outside an instance, and the loop's decision was stored once per syntax node, so every instance read with the proven getter. - The analysis records, for each loop it judged present, the generic arrays the answer read, including those a slot that owns a copy was built from. When an instance lowers the loop, the same query runs with the instance's type-parameter bindings; a key that can hold a hole or undefined gets the checked read a concrete function already gets. - At a call of a generic function, method or constructor, an argument key that can hold holes gives every array the call can reach, and its result, hole and undefined facts: the body may keep the holes under another element type or copy them as present undefined. - The copying array conversion tests the element state when the source key can hold a missing element, and reads a dynamic source element as undefined. A target that cannot hold undefined throws a TypeError. Co-Authored-By: Claude Code <noreply@anthropic.com>
… positions
A generic function that maps an array held in a field of its argument
(`box: { items: T[] }`) still returned the holes under another element type
with no fact recorded. The generic-call rule now enters the fields of a
record or class instance one level deep where the callee's declared
parameter or result type mentions a type parameter, and follows map and set
values.
Co-Authored-By: Claude Code <noreply@anthropic.com>
…he array-fact rules A bare `v: T` argument gives the generic body no view of its fields, so the generic-call rule no longer enters them: `publish(graph)` marked every array field of the published object. A parameter typed by a type parameter whose constraint is generic (`B extends Box<T>`) is still entered. An implicitly typed JavaScript parameter is no longer a generic source for the per-instance loop decision. Its instance lowers bindings the analysis did not plan as optional, and a captured one has no lowering yet (`7748-npm-any-bytes-receiver`). Those instances read as before. Co-Authored-By: Claude Code <noreply@anthropic.com>
…a generic array was read The generic-call rule read the "some key can hold holes" fact at every call site, so the first holed key made every call site of the program stale. It now decides whether the callee is generic first, and computes the reachable keys only when some key can hold holes. A receiver's fields are entered only for methods of a generic class or interface. `elementsPresent` and `loopSources` judge an array a second time only when the first judgement read a generic array. Co-Authored-By: Claude Code <noreply@anthropic.com>
…neric ones A generic function that only reads its array, or returns it, creates no fact its caller does not already have: `first(xs)`, a visitor, a hole-punch helper. The analysis now records the functions and classes whose code builds an array from a generic array's elements or stores them into another array (a spread, a copying method, a rest pattern, a push of a spread), directly or by calling a generic function that does, and the rule fires only for those callees and for callees with no body in the program. Co-Authored-By: Claude Code <noreply@anthropic.com>
…e two program facts A function held in a parameter is not a generic callee: `visit(child)` inside a generic visitor made the visitor a builder of arrays, through the path for callees with no body, and with it every generic helper that takes a callback. Records reached through a callback's signature and functions held in fields are no longer entered when collecting the keys a call can reach. A loop source that has no element key even where the loop lowers is judged as the loop was. The generic-call rule and the "elements present" answer for arrays built from generic ones are registered in program-facts.ts. Co-Authored-By: Claude Code <noreply@anthropic.com>
… means Node's behaviour A value converted to another record, array or class layout is copied where JavaScript keeps one object, so a write through one name is lost and `===` is false. SC6001 came from a scan of the syntax tree that knew five spellings and looked for a write through the same binding: of 56 probed forms that differ from Node it named 7. The conversions are now read off the functions the lowering produced (frontend/lowering/width-copies.ts). Every call of a copy helper is one; helpers that only route a value hand their copies to the site that calls them. A conversion is reported when the converted value is aliasable (not a literal, a spread, another copy, or a value that only has a member read) and the whole program observes either layout: it writes a field or element, compares values of the copy's type by identity, or hands either to code the scan cannot follow. That is a statement about types, so no path of the copy escapes it; it can name a conversion on a path the program does not take. No emitted code changes. A record something else can reach, converted to a class instance in a program that writes either type, is refused (SC1090): nothing keeps that value one object, and no corpus program or pi site has one. tests/harness/copy-soundness.test.ts builds 291 generated cases and asserts that a case with no SC60xx warning prints what Node prints. Its baseline holds the aliasable conversions per kind, which may only go down, and the one case known to differ silently (a promise payload converted at the checked boundary). The corpus sweep in coverage.test.ts keeps "no divergence warning on a corpus program" for every code but SC6001, whose counts per program are in corpus-width-copies.json (58 programs of 4,743 entries). scriptc build renders at most 25 warnings and counts the rest by code. Co-Authored-By: Claude Code <noreply@anthropic.com>
The scan formatted the type of every field write, array write and identity comparison it met, then kept the description only for the first one under each key. A program writes the same fields many times, and the compiler's own source formats large node unions. The description is now built when the observation is recorded. The facts are unchanged. Co-Authored-By: Claude Code <noreply@anthropic.com>
Compiled code calls a built-in member's native entry point and never reads the member from its object. `Array.prototype.push = f` built without a diagnostic, in TypeScript and JavaScript sources, and `xs.push(1)` then ran the built-in: the binary printed `1,2,3 0` where Node prints `1,2,3 2`, both with exit status 0. Existing members of built-in objects are now read-only. A write the build can see that replaces or deletes one is refused with SC1092: an assignment, a compound assignment, an update, `delete`, `Object.defineProperty`, `Object.defineProperties`, `Object.assign` or a `Reflect` call whose target is a built-in constructor, its prototype, a namespace object or the global object, through `globalThis`, a `const` alias, a literal key, a well-known symbol or the prototype of a value the source spells out. The refusal is a build error in JavaScript sources too, where a deferred fence could be caught and the built-in would keep running. A member the object inherits from another built-in counts, and so do `toJSON`, `then` and well-known symbol keys, which the runtime's own readers do not look up on a built-in's prototype object. Redefining a member with its own value and assigning `Error.stackTraceLimit` keep their lowering, and so does adding a member to `Array.prototype`, a constructor or a namespace object. Adding a member to any other built-in prototype is refused with SC1093: only untyped arrays read a member from their prototype object, so `Map.prototype.added = 7` left `new Map().added` undefined where Node answers 7. Like other refusals it becomes a run-time fence in a JavaScript source. "Existing" is a fact of the pinned Node release, generated by scripts/gen-builtin-object-members.mjs. Because an existing member cannot change, a feature test of one has one answer, so a polyfill behind `if (!Array.prototype.at)`, `typeof X.m !== "function"`, `X.m || (X.m = f)`, `X.m = X.m || f`, `X.m ??= f` or an early return lowers to nothing. The fold applies only where the code that never runs contains a refused write. A write the build cannot see throws at run time: when a program reads a built-in prototype as a value (Array, String, Number, Boolean, Date, Map, Set, WeakMap, WeakSet, DataView), the object's members get the fenced bit the namespace objects already use. The bit is set at the value entry points, by a header helper that keeps no state outside the object, so a program that never holds a built-in prototype keeps its exact executable. Array.prototype has its own entry point for that (dyn.arrayPrototypeValue), because the method table also installs for programs that never hold the object. Four corpus programs whose subject was replacing an existing member are removed and three are trimmed; their shapes are diagnostics snapshots now. Co-Authored-By: Claude Code <noreply@anthropic.com>
…assed through A function held at a type with fewer parameters than it has is a view: the signature adapter that calls it with undefined for the parameters the type lacks. A later conversion to a type with more parameters wrapped that view in a second adapter, which dropped the arguments the first type lacks, so a function with an optional parameter stored in a one-parameter slot and then in a two-parameter slot never received the second argument. JavaScript has one function object throughout. When lowering ends, every adapter that makes a view of a function type is known with the type of the function it forwards to. A widening adapter now tests, per such view, whether its operand is that view (a comparison of the closure's code) and builds its result from the function the view forwards to, through the conversions the two adapters already are with the extra parameters threaded through. Two IR expressions carry it: viewOf and viewSource. A checked-dynamic callable held at a function type (a bound function) is looked through the same way, boxing a view into a checked-dynamic value boxes the function it stands for, and a function asserted to a type with more parameters converts instead of erasing, so a call through the assertion passes its extra arguments. An extra argument the function's parameter cannot hold takes the stranded TypeError when the wide view is invoked. Nothing changes where a function is narrowed or called; a widening adapter changes only in a program that holds a view of its source type that can be handed more. Co-Authored-By: Claude Code <noreply@anthropic.com>
…a narrower slot JavaScript calls every array-method callback with the element, its index and the array. The method's helper passes the parameters the callback's type has, so a function with an index parameter that came through a slot declaring only the element never received it: items.map(cb) with cb: (x) => string holding format(x, index?). Each shared array-method helper (the one callback values use; a literal callback has its own) records what it was built from. When its callback type has a view of a longer function, the helper tests for that view and calls the same helper built for the full parameter list with the function the view forwards to. A helper with tests leaves the registry pipeline fusion reads, so a fused chain does not skip them. A function whose parameters past the element cannot hold what the method passes, handed over where the checker cannot object, was refused as a value "that cannot be compiled yet"; the refusal now names the parameter and the rewrite. Co-Authored-By: Claude Code <noreply@anthropic.com>
…ir gets none Co-Authored-By: Claude Code <noreply@anthropic.com>
…member The run-time fence on prototype objects a program holds, the refusal of members added to built-in prototypes (SC1093), of protocol keys and of copies from a source the build cannot read move to a change of their own, stacked on this one. Co-Authored-By: Claude Code <noreply@anthropic.com>
Both sides added a check at the start of finishModule (the refusal of a record-to-class copy the program can tell, and #3097's refusal of a source field delete); both stay. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Two programs merged since this branch was cut hold a conversion the scan reports: 63 programs, 251 warnings, of 4,804 entries. Co-Authored-By: Claude Code <noreply@anthropic.com>
…at typed slots
`const o = { files: [] }; const f: string[] = o.files; f.push("x")` left
`o.files` empty: the field was the unit-element array and every typed slot
its value reached received a fresh array from the empty-array lift, so two
names for one JavaScript array were two arrays. No diagnostic, exit 0.
An empty array has one identity, so its element representation is now
decided once, where it is written:
- A property `name: []` (also `[] as const`) takes the array type of its
contextual type, or the one the typed slots its value reaches agree on.
The search (empty-array-places.ts) follows the value out of the literal
that holds it, through unannotated bindings and function results, every
read of the property in the program, aliases, conditional and logical
operands, spreads, assertions through `unknown`, callback results and
generic calls, until a declared type names the element.
- The copying lift is removed (`emptyArr`, `%arr.empty`). The empty literal
itself still becomes its destination's empty array; any other
unit-element array at a typed slot is refused by name, SC2015, with the
fix in the hint. That is what remains when one array is used at two
element types or reaches a slot through a path the search does not follow.
- A binding, a declared function's result, a function-valued binding, an
alias property and a spread copy that the checker types `never[]`
because they read such an array take the array's type instead of never's
f64 element array.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The per-program list broke for whoever merged second: three corpus programs that carry SC6001 landed on main in the four hours this branch was open, and each one fails the sweep until it is listed. The sweep keeps asserting that a corpus program has no other divergence warning. The conversions that copy and the warnings that were not needed are counted by copy-soundness.test.ts, whose programs no other change adds to. Co-Authored-By: Claude Code <noreply@anthropic.com>
…replayed verdict, and bound the warning's type text Three findings on the width-copy scan: - A copy-helper result counted as fresh, so a value that reaches a third layout through a second (`w` to N to M in one expression, or through a function that returns its converted parameter) was reported only for the first conversion. A write through M was then lost with no warning when the program wrote neither N nor W. A copy of an aliasable value is now as aliasable as the value. (Vercel bot review.) - The launcher replays an unchanged program's coverage verdict without loading the compiler. It hid SC6001 for --no-copy-warning and then let --fail-on=divergences pass, where the full CLI refuses the pair. It now leaves that pair to the full CLI. (Vercel bot review.) - The compiler's own source carries several thousand SC6001, each naming up to three types in full: the self-hosting tests that return a compile result from a child overflowed the child's 16 MiB stdout. The message cuts a type's text at 120 characters, and the child's buffer is 64 MiB. Co-Authored-By: Claude Code <noreply@anthropic.com>
No conflicts. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
A guard such as `effect() && !Array.prototype.at` is false whatever `effect()` answers, so the polyfill behind it is still dead, but `effect()` runs. The test was folded to a literal and the call was lost. A decided guard now lowers its test whenever an operand runs for its effects, and only the dead arm is dropped; a logical expression in statement position and an early-exit guard fold only when every operand that runs is a feature test. Co-Authored-By: Claude Code <noreply@anthropic.com>
…ter at any depth
An untyped empty array in a generic function or class kept the array type
of the first instance when the slot's element only contained the type
parameter (`{ value: T }[]`), so later instances were refused. The answer
is no longer kept across instances for a literal inside a generic
declaration. Found by the Vercel bot on #3288.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
The family gained the case the review asked for (a value that reaches a third layout through a second): two more record conversions that copy an aliasable value, 364 in all. Co-Authored-By: Claude Code <noreply@anthropic.com>
lowerer.ts: main settles the x.constructor identity helpers in the two reachability fixpoints where this branch processes function views. Both steps are kept, identity first; the loop ends when neither changed. Co-Authored-By: Claude Code <noreply@anthropic.com>
A write or delete through `<T>process.env` changed a snapshot copy, not the environment: `isAmbientProcessEnv` looked through parentheses, `as`, `satisfies` and `!` but not the angle-bracket assertion. `require("node:path") satisfies T` was refused (SC1090) and `fork(path satisfies string)` was refused (SC2020) where the `as` spellings compile: the require and fork-target recognizers left out `satisfies`. The name in "x is not iterable" was lost behind `<T>x`.
Each of these wrappers leaves no trace at run time, so each site now looks through the same set as its `as` spelling.
Co-Authored-By: Claude Code <noreply@anthropic.com>
`function strict(): undefined { return loose(); }` with `loose(): any` returning 5 printed `undefined`; Node prints 5. A result declared `undefined` has the void representation, so the value was dropped without a look, while a binding declared `undefined` checks it.
A `return` and a concise arrow body now test an untyped value in a function whose declared result is `undefined` and throw a TypeError unless it is undefined. Functions declared `void`, typed operands and async functions lower as before.
Co-Authored-By: Claude Code <noreply@anthropic.com>
`class Proc { read = (x: number) => x + 1 }` named its arrow "" wherever the name was read from a boxed value: `const f: unknown = io.read; f.name` and `console.log(new Proc())` as `unknown` printed `[Function (anonymous)]`, where Node prints "read".
The lifted function now gets a displayName from the field's key (identifier, string, number or private name), as an object literal's method does, in modules that box a function without a static name.
Co-Authored-By: Claude Code <noreply@anthropic.com>
A polyfill guard was decided without evaluating its test when the test
read the member through the prototype of a value made for it:
if (!Object.getPrototypeOf([note()]).at) { Array.prototype.at = ... }
Object.getPrototypeOf([note()]).at || (Array.prototype.at = ...)
Node calls note() each time; the compiled program never did, with no
diagnostic, and `new Promise(note())` there threw under Node and not
compiled (#3241's fold, found by the review bot on #3368; the same family
as the `void` operand). The same held for a `const` alias of a built-in
object read before its declaration has run, where Node throws a
ReferenceError.
The rule is now one predicate, readRuns: evaluating the member access or
object a test reads can do something besides answer when it reads the
prototype of a value that is not an inert literal (any call, binding,
spread, computed key or `new X(...)` in it), or starts at an alias that is
not declared by an earlier statement of a list that holds the read. Every
place that decides a test asks it (member, typeof, comparison with
undefined, `in`, hasOwnProperty, `??`, `||=`, `X.m = X.m || f`): the guard
of an `if` keeps the test's evaluation, as `effect() && !X.m` already did,
and a statement form is no longer a polyfill that does nothing. An object
literal that names its prototype (`{ __proto__: null }`) is not an
Object.prototype object. Every other part of a decided test is a name or a
literal key: a computed key, a comma, a call argument or a call in front of
`.prototype` is not resolved, so the test is lowered as any other.
tests/corpus/builtin-polyfill-guard-prototype-operand.ts pins the shapes
that keep their evaluation, tests/corpus/builtin-polyfill-guard-operand-
positions.ts one case per operand position, and tests/diagnostics/builtin-
polyfill-prototype-operand.ts the statement forms that are refused by name
(SC1092) instead of being dropped.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Main added three reads to module-bindings.ts (the awaited builtin dynamic import) that peel with withoutParenthesesAssertionsNonNull. The train's #3302 moved this file to withoutParenthesesTypeWrappers, which also peels `<T>x`, and dropped the other import: the merge was clean and did not build (TS2304). The three new reads use the file's helper. Co-Authored-By: Claude Code <noreply@anthropic.com>
…ns program A guard the compiler decides records the member it read, and a live write of that member in front of it is refused (SC1092). The program mixed both over one member. Its undecided cases now write Array.prototype.findLast and its decided cases read Array.prototype.findLastIndex; the cases and what they print are the same. Co-Authored-By: Claude Code <noreply@anthropic.com>
Main's #2761 refuses a width copy the program writes through (SC2021). The family case partial/declaration/increment is such a copy: interface S { a: number; b: string } type T = { a: number; b?: string }; const w: S = { a: 1, b: "x" }; const n: T = w; n.a++; It was a copy the family counted; it is a refusal now, so it joins the baseline's refused list. Regenerated with SCRIPTC_UPDATE_COPY_BASELINE=1 on the merge with main 37323b0: the aliasable width copies go from 364 to 363 and nothing else changes; the family passes plain and sanitized. Co-Authored-By: Claude Code <noreply@anthropic.com>
No conflict. Main now holds #3371, whose branch the train had merged: the same commits. Co-Authored-By: Claude Code <noreply@anthropic.com>
…d that runs `if (Object.getPrototypeOf([note()]).indexOf) return;` in front of a polyfill lowers as any other `if`, so its test is evaluated there and note() runs; what the guard decides is only that the statements behind it never run. The previous commit asked for a test with nothing to run here as well, which turned this shape from a program that prints what Node prints into a refused write. The guard now accepts an operand that runs, as the guard of an `if` with a dead arm does. The shape moves from the diagnostics fixture to the corpus program, where it prints what Node prints, plain and sanitized. Co-Authored-By: Claude Code <noreply@anthropic.com>
`Math.PI = Math.PI || 3.14` was erased as a polyfill that never runs. It does run: it assigns the member its own value. In strict code that assignment throws a TypeError when the member is read-only, or when the program froze the object, and the compiled program did nothing (#3241's fold, found by the review bot on #3368). A table of every guard and assignment form the fold decides, by the member's attributes (writable, read-only, falsy, accessor, inherited, missing, on a frozen and on a sealed object), in strict and in sloppy code, each under Node and compiled, has these as its only silent cells: the two keep-value forms over a read-only member and over a frozen object, in strict code. Whether a member is writable when the statement runs is not a fact of the build (a freeze, or a redefinition that keeps the value, is not a write the census records), so: - In strict code the statement is no longer erased. It lowers as the assignment it is, and the runtime answers as Node does: nothing for a writable member, a TypeError otherwise. - In sloppy code it is still erased, over a data member the object itself holds: there the write changes nothing or is ignored. - `X.m ||= f` and `X.m ??= f` never assign over a member that is there, and stay erased in both. - `X.m = X.m` over a data member counts as keeping the member's value too, so it is no longer refused as a replacement (SC1092); it lowers as on main, and is erased where the keep-value forms are. After the change the table has no silent cell, and no cell where main prints what Node prints and this branch does not. tests/corpus/builtin-polyfill-keep-value-assignment.mjs (strict) and builtin-polyfill-keep-value-assignment-sloppy.cjs pin the three assignment forms over a writable member, a read-only member and a frozen object, beside the logical assignments. Co-Authored-By: Claude Code <noreply@anthropic.com>
Main now holds train islands-1 (#3377). Conflict, frontend/lowering/lowerer.ts: islands-1 removed the lowerer's stdioPatchSites field and what reported it (a patched process.stdout.write now works beside a Node island); #3241's three built-in census fields and #3221's loweredFunctions sat beside those lines. The field and its two mentions are gone, the train's lines stay. Co-Authored-By: Claude Code <noreply@anthropic.com>
8738-builtin-polyfill-guards.ts is a module, so its `String.prototype.trimStart = String.prototype.trimStart || function …` is strict code: it is no longer erased, and an assignment to a member of String.prototype has no lowering (SC1090), as on main. The form is pinned where it runs: in sloppy code by 8751-builtin-polyfill-guards-js.cjs (erased), in strict code by builtin-polyfill-keep-value-assignment.mjs (lowered). What 8738 prints does not change. Co-Authored-By: Claude Code <noreply@anthropic.com>
No conflict. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Zero-cost gate on the pushed head
The type check of the round (the candidate's |
|
CI on
I will re-run the failed job once the workflow run has finished. |
Main now holds train cli-5 (#3386). Conflicts, each two additions at one place, both kept: - diagnostics/diagnostic.ts: cli-5's SC6008 and SC6201 diagnostics in front of the SC6001 function whose description #3221 rewrote. - diagnostics/render.ts: #3221's WARNINGS_RENDERED and cli-5's renderStartupWarnings. - frontend/program.ts: the import lines. #3302's withoutParenthesesTypeWrappers stays the file's peel helper, beside the two imports cli-5 adds. Co-Authored-By: Claude Code <noreply@anthropic.com>
Main's variadic rework (0d53262) refuses, at the end of lowering, a variadic function stored under a fixed signature in a program that hands a function of that type to a caller that passes more arguments (SC1090): a rest function, a reader of `arguments`, a bound partial application. #3263's views made some of those stores answer as Node does and its tests pinned them; the rule now refuses those programs. Measured on the merged tree (90 programs, 9 function kinds by 10 callers, under Node and compiled): with the refusal left out, six of main's eight pinned shapes are silently wrong, because a store main's variadic adapter made is not a view. So the rule stays as main has it, one rule for a variadic store, and the refused shapes leave 8762, 8765, 8916 and one dyncheck fixture. No compiler source changes. Co-Authored-By: Claude Code <noreply@anthropic.com>
Main now holds trains startup-1 (#3350) and cli-4 (#3384). Conflict: ir/codec.generated.ts only (both sides changed the IR). The file is regenerated on the merged sources with pnpm generate:ir-codec (layout dfa62312528c7983); ir/fields.generated.ts regenerates to itself. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Full round on the pushed head
Not run in this round: bench-runtime and Test262 (they ran in the round of section 1 of the description and on no later head). |
Eight language fixes, most of them for programs that compiled and then printed something other than Node with no diagnostic, validated together in one round and landed as one branch. Each was proven by its owner on its own head; this is the eight merged on main, with their conflicts resolved once. A ninth member of the round, #3156, was merged by itself while the round ran and is main's now.
What lands
const o = { files: [] }; const f: string[] = o.files; f.push("x"); console.log(o.files.length): Node1, main0. Its element type is decided where it is written; a use that cannot decide it is refused by name (SC2015) instead of copied.const ops = { make: label }; const wide: (p: string, o: { upper: boolean }) => string = ops.make; wide("abc", { upper: true }): NodeABC, mainabc.m.set("k", w); m.get("k")!.a = 3; console.log(w.a): Node3, compiled1; no diagnostic on main, SC6001 now.--no-copy-warningleaves the warning out.<T>process.envis the environment (writes and deletes reached a copy);satisfiesis looked through in therequireand fork recognizers; an untyped value returned where the result is declaredundefinedis checked; an anonymous function is named after the class field it initializes.undefined, read by afor…ofloop inside a generic function, no longer aborts the process, and a missing element in an array conversion is aTypeError. The branch's local head is taken (the PR's head plus the fix of its own failing test and a corpus program, by the agent that owns main's red tests).fix/member-call-receiver-narrowing(no PR; the follow-up to Pass the holder as the receiver of a typed member call #3308 that was decided when it landed): a member call carries its holder only where a store under the member's name may hold the function that escaped. The threethis-forwarding wrapper programs (6380,6381,6383) are byte-identical to before Pass the holder as the receiver of a typed member call #3308 again and link no dynamic-tier symbol;(Registry.f)()on a class's static field passes the class, where main passed no receiver.fix/f5-typed-resolve(no PR; on top of Adopt a promise that an untyped promise is resolved with (executor, Promise.resolve, async generators, finally) #3345):new Promise<T>((resolve) => resolve(v))withv: T | Promise<T>adopts the promise as Node does. On main the promise arm rejected the new promise at run time,v: Promise<T>did not build, and withT = voidthe promise fulfilled at once and lost a rejection, silently.Array.prototype.push = f) and also reads that member natively is refused by name (SC1092, naming the write and the use); main built it and ignored the replacement. A write the program never reads natively is lowered as before.Not in this train: #3156, #3345, #3310 and #3169 (merged by themselves), #3150 (its gate proof needs the allow pattern
\bawait\b, which would hide other members' rows) and #3053 (it conflicts with main in the runtime pack matrix and is a train by itself).Conflicts and fix-ups on the train
program-facts.ts,lowerer.ts: members and main added rows and fields at the same places; every row stays.coercions/structural.ts, Report width copies from the lowered program and test that no warning means Node's behaviour #3221 against Type an untyped empty array where it is written, and stop copying it at typed slots #3288: Type an untyped empty array where it is written, and stop copying it at typed slots #3288 deletes the empty-array lift that Report width copies from the lowered program and test that no warning means Node's behaviour #3221 renamed. The deletion stays and the names Report width copies from the lowered program and test that no warning means Node's behaviour #3221 gave it go with it.tests/harness/copy-soundness-baseline.json: the one case that stored an empty array under two element types is refused by Type an untyped empty array where it is written, and stop copying it at typed slots #3288 now (SC2015), so it moves to the baseline'srefusedlist and theemptyArrcount goes.executable-cache.test.tsis main's.packages/cli/src/bootstrap.ts, Report width copies from the lowered program and test that no warning means Node's behaviour #3221 against main's table of the options a launcher fast path answers for: main's table stays, Report width copies from the lowered program and test that no warning means Node's behaviour #3221's rule for--fail-on=divergenceswith--no-copy-warningis kept, andcopy-warningjoins the coverage fast path's options.--no-copy-warninggets its cache key rule (executable-cache.test.ts), and the width-copy scan's prefix list is registered inbuiltin-attributes.test.ts.ir/codec.generated.tsis regenerated.Validation
All in Vercel Sandboxes. One full round on the train, then the checks on the tree that is pushed.
1. The full round (
aa197fe248= main4e3f07164bplus the nine members, #3156 among them at the time), 49.0 minutespnpm -r buildpasses.tests/harness/island-value-matrix.test.tsin both lanes: main's table is stale at that commit. The table regenerated on plain main4e3f07164bis byte-identical to the table regenerated on the train (same sha256), so the train changes no cell; another train's round on a main of the same hour fails the same 108 cells, and a round on main4a71162f4cfails none.tests/harness/effect4.test.ts,cluster-entity.ts, sanitized lane only: the compile step hit the test's time limit (542 s) on a loaded lane box. Alone on one Sandbox (Intel Xeon 2.50GHz, 8 vCPU) the same test passes in 326.8 s on plain main and in 319.1 s on the train.--gate-corpus=all, 5,580 programs: 5,389 identical, 121 fail to build on both sides, 45 allowed, no runtime-only row, no program of main's that differs outside the allow list, and no program of main's gained a dynamic-tier symbol. The gate also compiles the corpus programs a PR adds with main: 11 of the train's do not build on main, and 14 build and print something else, which is what they pin; the tool counts those 14 as "differs", so its verdict line reads FAIL and it stopped the self-compile and fixture shards. The gate-only round on the pushed tree (below) has them in the allow list by id and measures those shards.differential.test.tsandllvm-differential.test.ts82 of 82 plain,differential.test.ts41 of 41 sanitized; 87 unit and harness test files, 1,258 tests;scripts/gen-builtin-object-members.mjs --check; docs gate 25 of 25.An earlier round of the same train (17:26, with #3345, #3310 and #3169 still in it) is what found the two guard rows listed under fix-ups; it passed both bootstraps too.
Allow list
45 allowed rows; every row matches the list of the member that explains it. Executables of programs that exist on main change by -14.41% to +3.11% (one of #3288's own new programs, which main builds with the copying lift, is 45.17% smaller):
8808-asserted-write-in-place.ts.8872-asserted-method-callee.tsby id (same class; it came to main with Train lang 1: JSON.stringify of undefined stays undefined, RegExp source is escaped, method values check their receiver, empty containers through generic calls, Object.create as a record is refused #3357).undefined(largest growth,2024-width-array-elems.ts, +3.11%).this-forwarding wrappers (6380-14.41%,6383-3.52%,6381-1.64%) and8915-member-call-receiver.ts, all smaller.4040-adapted-function-identity.tsand8924-boxed-function-name-lookup.ts;self/cliandfixture/llvm-emitterby its recorded exception (GATE-EXCEPTIONS.md).8110-builtin-prototype-own-members.js,array-from-checked.js,collection-constructor-sparse-array-seed.js.--gate-max-size-growth=6(Give a function every argument of the final call, whatever slots it passed through #3263'sfunction-extra-optional-params.ts, +5.66% in its own round).2. The pushed tree (
258566b418= that train merged with mainfdf68fdf57)Main gained fourteen merges after the round started (#3156 among them). Two merges of main into the train:
cc77cb7a1b(maine7378e4fa1, no conflict) and258566b418(mainfdf68fdf57; one conflict,ir/builtin-never-throw.ts, where main's #2832 and the f5 branch each add a row: both stay).On
cc77cb7a1b(Intel Xeon 2.50GHz, 8 vCPU):pnpm -r build(the type check) passes;pnpm generate:ir-codec,pnpm manifest,pnpm node-compat --localandscripts/gen-builtin-object-members.mjs --checkchange no generated file;pnpm lint,pnpm format:check,pnpm node-compat:checkpass;pnpm test:ts7 --baselines-only: every fixture has a baseline. Main's guard tests and the members' unit tests (85 files, 1,282 tests) pass. The corpus programs the train adds, with their neighbours:differential.test.tsandllvm-differential.test.ts76 of 76 plain,differential.test.ts38 of 38 sanitized.On
258566b418(the pushed head): the build, the generated-file checks, lint, format and baselines again; the guard tests (builtin-attributes,outer-expressions,executable-cache,program-facts,function-references, the launcher's option test,copy-soundness): 97 of 97; the f5 program and main'sexistsSyncprograms: 5 of 5.Zero-cost gate on
258566b418against mainfdf68fdf57,--gate-corpus=all(ended after the PR was opened): 5,416 identical, 123 fail to build on both sides, 62 allowed, 8 of the train's new programs that main cannot build, none that differs outside the allow list, no runtime-only row. The allowed rows are the members' lists above, the train's own new programs (main builds 29 of them and prints something else),self/cli,fixture/llvm-emitterandfixture/llvm-layouts(Give a function every argument of the final call, whatever slots it passed through #3263's recorded exception and Judge generic loops by each instance's array facts; reject missing elements in array conversions with a TypeError #3243's rows in the compiler's own source). The tool's verdict line reads FAIL by two size lines, both programs the train adds, compared with what main makes of them (8751-builtin-polyfill-guards-js.cjs+24.8%,8765-fn-view-array-methods.ts+8.4%), and by one of four self-compile pairs (+2.76%). Compile time: benchmarks +0.90% [95% CI +0.39%, +1.75%], within tolerance; self-compile +1.49% [+0.27%, +2.76%] and fixtures +0.37% [-1.90%, +2.64%], which the gate calls inconclusive. The train adds 1.3% to the compiler's own source; compiling that source costs 1.9% more, with 1.0% more LLVM text and 1.1% more functions.3. The review fix (
3bf19d3309) and the head after it (74e48e3548= maincb9665e11amerged)The review bot found a silent difference in #3241's fold on this PR: a polyfill guard that compares a built-in member with
void ewas decided without evaluatinge.if (Array.prototype.at === void note()) { … }under Node callsnote(); the compiled program did not (reproduced on258566b418: fournotelines under Node, none compiled, in theifform and in the||and&&statement forms). Fixed on the train: avoidwhose operand is not a literal counts as an operand that runs, so the statement form is not a dead polyfill and anifguard keeps the test's evaluation, wherevoidwith an effectful operand in value position is refused by name (SC1090), as everywhere else.void 0decides the test as before. Pinned bytests/diagnostics/builtin-polyfill-guard-void-operand.ts(five shapes) andtests/corpus/builtin-polyfill-guard-void-literal.ts.On
74e48e3548(Intel Xeon 2.50GHz, 8 vCPU): the build (the type check) passes; the diagnostics snapshots, 337 of 337 with the new one; the built-in member programs with the new corpus program,differential.test.tsandllvm-differential.test.ts20 of 20 plain,differential.test.ts10 of 10 sanitized;scripts/checker-identity.mjsfor the new program; main's guard tests 97 of 97;pnpm lint,pnpm format:check,pnpm test:ts7 --baselines-only,pnpm node-compat:check; no generated file changes. The merge of maincb9665e11abrings main's regenerated island value matrix; it had no conflict. A gate-only round on this head runs beside CI.4. The second review fix, main's new refusal, and the head after them (
4f6d08df91, which contains main0f154df0e1)The fold's rule, once (
0d3aab12e6). The review bot's second thread on #3241's fold:if (!Object.getPrototypeOf([note()]).at) { Array.prototype.at = … }callsnote()under Node and did not when compiled (reproduced on74e48e3548in eleven shapes, statement forms andnew Map(note())among them). Both threads are one question, answered by one predicate: a feature test is decided without evaluating it only when nothing in it runs. Going through every operand position found a third silent one: aconstalias of a built-in object read before its declaration has run (function probe() { if (!proto.at) { … } } probe(); const proto = Array.prototype;throws a ReferenceError under Node and did nothing compiled). Where something runs, the guard of anifkeeps its test's evaluation and a statement form is lowered as any other statement. An early-exit guard (if (X.m) return;in front of a polyfill) always evaluated its test, so it stays decided with an operand that runs (4f6d08df91). The other positions (a computed key, a comma, a call argument, the key ofin, a call in front of.prototype) were never resolved by the fold; each was measured under Node and natively and is evaluated or refused by name.new X(…)always counts as running, soif (!Object.getPrototypeOf(new Map()).get) { … }is no longer decided: its test is evaluated and stops at the existing run-time refusal of that call, loudly, as it does on main. Pinned bytests/corpus/builtin-polyfill-guard-prototype-operand.ts,tests/corpus/builtin-polyfill-guard-operand-positions.ts(one case per position) andtests/diagnostics/builtin-polyfill-prototype-operand.ts.CI's one failure on
74e48e3548that was not main's:copy-soundness.test.ts. CI tests the merge with main, and main's #2761 now refuses a width copy the program writes through (SC2021). #3221's family has such a case,partial/declaration/increment(const n: T = w; n.a++whereTmakes a field ofw's type optional): a copy the family counted, a named refusal now. That is main's intended refusal meeting the train's test, not something that built on main and stopped building: no program of main's changes. The case joins the baseline'srefusedlist and the counts are regenerated by the test's update mode: aliasable width copies 364 to 363, nothing else (715a3d473c).Merges. Main
37323b08f8(355145304d): no conflict, and it did not build: main added three calls ofwithoutParenthesesAssertionsNonNulltobuiltins/module-bindings.ts, a file #3302 moved towithoutParenthesesTypeWrappers; the three calls use the file's helper (4904372f64). The branch of #3371 (main's fix for its weak-collection tests and the runtime refusal baseline; on main since) as the same commits, and main0f154df0e1(5012504325), both without a conflict.On
5012504325(Intel Xeon 2.50GHz, 8 vCPU): the build (the type check) passes;pnpm generate:ir-codec,pnpm manifest,pnpm node-compat --local,scripts/gen-builtin-object-members.mjs --checkchange no generated file;pnpm format:check,pnpm node-compat:check,pnpm test:ts7 --baselines-onlypass. Guard and unit tests,copy-soundness, the diagnostics snapshots (339) and the three test files #3371 fixes: 518 of 519. The built-in member and polyfill programs with the two new ones, main'srecord-width-aliasand three members' programs:differential.test.tsandllvm-differential.test.ts26 of 26 plain,differential.test.ts13 of 13 sanitized. Two things fail, and both are main's at0f154df0e1, with nothing of the train in them:pnpm lint: one runtime refusal site missing from main's baseline,scr_events_emitter.c:442(node:events: symbol event names and the max-listeners warning's fields #2766; the file is identical to main's).builtin-attributes.test.ts"every builtin says whether it can throw":worker.shareEnv(Support the Worker env option and worker_threads.SHARE_ENV #3023) andemitter.symName(node:events: symbol event names and the max-listeners warning's fields #2766).On the tree before the last merge of main (
c2c4044afbwith the two later commits' files): the members' unit and harness tests, 86 files and 1,302 tests;copy-soundnessplain and sanitized, 12 of 12 each; the members' corpus programs, 84 plain and 42 sanitized;scripts/checker-identity.mjsfor the polyfill programs. After that, one more commit (4f6d08df91: the early-exit guard, one predicate call and two fixtures), checked on its tree: the build; the 41 repro programs under Node and natively (each prints what Node prints or is refused by name); the diagnostics snapshots, 344 of 344, and the two polyfill fixtures sanitized; the built-in member and polyfill programs, 16 of 16 plain and 8 of 8 sanitized;copy-soundnessand two guard tests, 35 of 35;scripts/checker-identity.mjs;format:check, oxlint, the ts7 baselines. Main has since fixed both of its failures (#3373), which CI's merge with main picks up. A gate-only round on4f6d08df91against main0f154df0e1runs beside CI; its result follows as a comment.5. The third review fix: the fold's table (
a332810c74), and the head after it (0463579800, which contains main14960424a1)The review bot's third thread on #3241's fold:
Math.PI = Math.PI || 3.14was erased as a polyfill that never runs. It does run, it assigns the member its own value, and in strict code that assignment is a TypeError when the member is read-only. Node throws; the compiled program did nothing.That was the fourth thing the fold dropped that Node does (the
voidoperand, the value behind a prototype lookup, an alias read before its declaration, and this). So the fold got the table it should have had from the start: every guard and assignment form it decides (17) by the member's attributes (9: a writable function, read-only data, read-only falsy data, an accessor, an inherited function, a missing member, a member of a frozen object, of a sealed object, writable falsy data), in strict (.mjs) and sloppy (.cjs) code: 306 programs, one per cell, each under Node and compiled, on the train and on main.4f6d08df91): four silent cells, one class:X.m = X.m || fandX.m = X.m ?? fin strict code, over a read-only member and over a member of a frozen object.X.m ||= fandX.m ??= fnever assign over a member that is there: erased in both modes.X.m = X.m || f,X.m = X.m ?? fandX.m = X.massign the member's own value: erased in sloppy code over a data member the object itself holds (the write changes nothing or is ignored), lowered as the assignment it is in strict code, where the runtime answers as Node does.X.m = X.mis no longer refused as a replacement (SC1092).RegExp.prototype,Map.prototype.hasOwnPropertyand the like, on main too), and in 8 both sides throw the TypeError and the message names the object differently, on main too.String.prototype.trimStart = String.prototype.trimStart || function …is not erased and meets SC1090 (an assignment to a member ofString.prototypehas no lowering), as on main today. Refuse a write to a built-in member only where the program also reads it natively #3241's own program8738had that line in a module; it is gone from there and pinned where it runs:8751(sloppy, erased) and the newtests/corpus/builtin-polyfill-keep-value-assignment.mjs(strict, lowered) with its sloppy twin. Deciding it in strict code again needs the census to record freezes and redefinitions that keep the value (FOLLOWUPS.md).Merges. Main
ee6d7f7240(fe192725fd), which carries train islands-1: one conflict,lowerer.ts, where islands-1 removed thestdioPatchSitesfield beside #3241's three census fields and #3221'sloweredFunctions; the field and its two mentions are gone, the train's lines stay. Main14960424a1(0463579800): no conflict, no file in common.On
fe192725fdwith the 8738 change (Intel Xeon 2.50GHz, 8 vCPU): the build (the type check); no generated file changes (generate:ir-codec,generate:ir-fields,manifest,node-compat --local,gen-builtin-object-members --check);pnpm lint(390 runtime refusal sites, none missing),format:check,node-compat:check, the ts7 baselines; guard and unit tests with the diagnostics snapshots,copy-soundnessand main's formerly red files, 93 files, 1,343 tests;copy-soundnesssanitized 12 of 12; the members' corpus programs, 96 of 96 plain (differentialandllvm-differential) and 48 of 48 sanitized;scripts/checker-identity.mjsfor the seven polyfill programs; the 306-cell table and the 41 repro programs.On
0463579800, the pushed head: the build; no generated file changes;pnpm lint,format:check,node-compat:check, the ts7 baselines; the guard tests withcopy-soundness,dyncheckand the diagnostics snapshots, 17 files, 621 tests; the built-in member and polyfill programs with five other members' programs, 34 of 34 plain and 17 of 17 sanitized; the 306-cell table again: no silent cell. A gate-only round on this head against main14960424a1runs beside CI; its result follows as a comment.Zero-cost gate on
4f6d08df91against main0f154df0e1,--gate-corpus=all(the head before this fix; the fix changes only what the fold erases): 5,461 identical, 125 fail to build on both sides, 65 allowed, 8 of the train's new programs that main cannot build, none that differs outside the allow list, no runtime-only row. The tool's verdict reads FAIL by the same two size lines as before, both programs the train adds (8751+24.9%,8765+8.4% against what main makes of them). Compile time: benchmarks +1.04% [95% CI +0.08%, +1.87%], within tolerance; self-compile +3.02% [+1.82%, +5.93%] and fixtures +0.91% [-0.76%, +2.58%], which the gate calls inconclusive.6. Main's variadic rule meets #3263's function views (
5b32646f38, which contains main851b59b90f)Three more trains landed on main (cli-5, startup-1, cli-4) and one earlier one (perf-2) brought a rule that meets #3263 on the same conversion: a variadic function (a rest function, a reader of
arguments, a bound partial application) stored under a fixed signature is refused at the end of lowering (SC1090) when the program hands a function of that type to a caller that passes more arguments. The merge was clean in text and refused three of #3263's own corpus programs and one dyncheck fixture, whose views had made some of those stores answer as Node does.Which rule holds was measured, not argued: 90 programs, 9 function kinds (untyped rest held by a constant, returned by a call, declared; a reader of
arguments; typed rest three ways; optional parameters; a bound function) by 10 callers (a wider function type, a chain of two, a cast call,map,forEach,reduceandfindIndex, through a function, a record slot, an untyped call,applyandcall), each stored as(x: number) => numberfirst, under Node and compiled, on three trees:952d9fb2be0463579800)[3, 1, 2].map(one)prints1,1,1): a store main's variadic adapter made is not a view. So the rule stays exactly as main has it: one rule decides a variadic store, and no compiler source changes on the train.loose(1, 2, 3)of a stored optional, typed-rest orargumentsfunction, andapplyof a storedargumentsfunction). Nothing is silent here that main does not have, and 22 of main's 27 silent cells are gone. The five are written down for the untyped-call branch that follows this train.8762,8765,8916, one dyncheck fixture); main'stests/diagnostics/variadic-store-longer-call.tspins the refusal.[3, 1, 2].map(one)over a function with three optional number parameters hands it the array as its third argument, which its parameter type excludes.Merges. Main
952d9fb2be(d10eec1a8b): three conflicts, each two additions at one place, both kept (diagnostics/diagnostic.ts,diagnostics/render.ts, the import lines offrontend/program.ts). Main851b59b90f(5b32646f38):ir/codec.generated.tsonly, regenerated on the merged sources (pnpm generate:ir-codec).On
5b32646f38, the pushed head (Intel Xeon 2.50GHz, 8 vCPU): the build (the type check); no generated file changes (generate:ir-codec,generate:ir-fields,manifest,node-compat --local,gen-builtin-object-members --check);pnpm lint(390 runtime refusal sites, none missing),format:check,node-compat:check, the ts7 baselines; guard and unit tests with the diagnostics snapshots,dyncheck,copy-soundnessand the function-view tests, 93 files, 1,367 tests; the members' corpus programs with main's variadic programs,differential.test.tsandllvm-differential.test.ts114 of 114 plain,differential.test.ts57 of 57 sanitized. The Node island conformance lane in check mode (which CI skips for a PR that touches no island path) ran ond10eec1a8b, the merge with952d9fb2be: four shards, 422 passed, none failed, the list untouched; it runs again on the pushed head. A full round on the pushed head (zero-cost gate, both lanes, both native bootstraps) runs beside CI; its result follows as a comment.Not run
On the merged trees and after the review fixes: the native bootstraps, bench-runtime, Test262 and the full lanes (they ran in step 1; CI runs the lanes on the merge). Anywhere: the macOS lanes, the Node island conformance lane (no member touches island code), a pi build (the f5 branch and the narrowing were measured against pi by their owners).
🤖 Generated with Claude Code