Skip to content

Train pi 1b: generic function identity, union classes, dictionary identity, process streams and seven lowering forms - #3427

Open
cramforce wants to merge 41 commits into
mainfrom
train/pi-1b
Open

cramforce wants to merge 41 commits into
mainfrom
train/pi-1b

Conversation

@cramforce

@cramforce cramforce commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Lowering fixes for typed code, as commits by compiler area on current main. This replaces #3352: the same compiler changes with test programs rewritten around the one behaviour each pins, minus one refusal that is taken back out, plus the corrections the rebuild needed.

What the commits do

  1. Generic functions. A generic function declared inside another function compiles. A generic function used as a value has one identity per declaration (===, Map and Set keys, listener removal), across objects, worker threads and library instances; the build-time pins that stood in for that are gone. A nested generic function used as a value is still refused, by name.
  2. Process streams and child processes. process.stdout and process.stderr passed or stored where stream.Writable is declared are withheld (see "Not in this train"): such a program stops at the build by name (SC1090), as on main. process.stdin where stream.Readable is declared is withheld (see "Not in this train"): such a program stops at the slot with the TypeError main gives it. A program that reads process.stdin with descriptor 0 closed ends, as in Node, instead of waiting forever. Once setEncoding has been called on process.stdin, a consumer compiled for bytes stops by name where a chunk is handed to it (SC2020 at run time), and a 'data' listener whose parameter is declared string receives the decoded text. The stream unit scr_stream.c is main's file and its objects are byte-identical to main's. spawn and spawnSync with shell and a separate argument list join the arguments onto the command as Node does (with DEP0190), and shell may be decided at run time. Merged with main's maxBuffer work and with main's evaluation of option values a launch does not read: one check function orders the launch's values, the joined line, those option values and the limit as Node does.
  3. Five forms of typed code that were refused: Object.keys and Object.entries over what typeof v === "object" leaves of a JSON value; left ?? right with a wider right side (see 7); a function created above let x: T;; a call through an optional callable member every arm of a record union declares; a generic rest binding typed by the type argument's fields. (Three more forms of Train pi 1: lowering fixes on pi's path (generic identity, Proxy slots, shell arguments, stdin as a stream, dictionaries and unions) #3352 reached main by other changes meanwhile: iterating a typed Headers, Promise.all over an entry of type Promise<T | undefined> | undefined, and reduce and reduceRight on a checked-dynamic array. Main's lowering stands for them; this branch keeps its test programs.)
  4. Dictionaries. A dictionary that keeps identity can be an arm of a recursive union. A dictionary type is one type however it is spelled ({ [key: string]: V }, Record<string, V>, an alias, another module), so a value passed between two spellings is the same object.
  5. Unions. Members that share a discriminant literal are stored as one record, so a value of one member is the object the union holds. A value whose type only has the layout of such a member (an inferred literal type, as const, a function's inferred result, an assertion's type) is built as that record where it is created, so it enters the union with no copy: every program of that shape that builds on main builds here and prints the same (a family of 1,491 programs: 1,332 equal Node; 144 differ from Node exactly as on main, with main's copy warning; 15 are refused, as on main). The same member written in two unions is one record in both. Where several arms hold a record of optional members, a value takes the arm that declares the properties it has. A dictionary record that is a union arm no longer crashes when it converts to an untyped value.
  6. A dictionary copied where its identity is observed is reported (SC6001) by the lowered-program scan. A record that holds such a dictionary, declared or passed as a record whose dictionary has another value type and written through, is refused as on main (SC2021, SC1090). This depends on how the dictionary type is spelled: a field whose type is another spelling of the type the object entered is refused, a field declared with the very type the object entered is a reported copy. Over 1,362 programs of the dictionary family and 144 of the holder family, every program main builds still builds and prints what main prints.
  7. left ?? right with a wider right side compiles where taking the left side keeps the value, and stays refused where it would copy it (SC1090, with the spellings that compile). JavaScript answers the left side itself; converting an array, a tuple or a record of another layout builds a second object, so a write through the result would miss the left side. Main refuses every such expression. In a family of 2,156 programs (7 forms of the left side, 19 kinds of value, a plain or side-effecting right side, present or absent, five things the program observes): main has 642 equal to Node, 1,324 refused and 190 that differ; this branch has 738, 1,228 and the same 190 with the same output. No program main builds is refused; 96 that main refuses compile and equal Node.
  8. The island conformance list is regenerated on this tree; the check mode passes (460 of 460 on 1c406f827c, before the merges of main).
  9. The weak-key memory test measures bytes per additional key.
  10. Two corrections found by building the branch. The dictionary-copy scan and the ?? change did not type-check together. And the search that finds a copied dictionary compared a large union of record types with itself at every step: the compiler's own build did not get past one declaration (stopped after 55 minutes there; the build takes about nine). The search now asks first whether the target can hold a native dictionary at all. With it the self-compile budget check (scripts/self-compile-budget.mjs --check) passes: 454 s, one lowering pass, 131 bytes per source byte against a budget of 136, and CopyScan stays out of the dynamic tier (main alone in the same sandbox: 487 s, 132 bytes). The compiler's own build prints 9,850 copy reports here against 9,770 on main.
  11. Three test-only commits. The narrowing test that was red on an earlier head says what the tree does: ?? over a binding only a closure assigns builds, || stays refused. The stdin iteration test this branch adds printed the bytes of its first read and of the iteration that follows separately, so its output depended on where the pipe's chunks ended; it failed once on a loaded machine. Its fixture now reports all the bytes together (one output over twelve runs with four arrival patterns, where the old fixture had three). And the snapshot of one of main's fixtures is recorded again; see "A build-time refusal that becomes a run-time stop" below.

Not in this train

  • The build-time refusal of a Proxy stored where a record type or a class is declared (part of Train pi 1: lowering fixes on pi's path (generic identity, Proxy slots, shell arguments, stdin as a stream, dictionaries and unions) #3352) is reverted here. Where the store runs it replaced a wrong answer; where the store is in code the program never reaches it rejected a program that builds on main and prints what Node prints. Proxy stores behave as on main. The form returns as a store that throws by name when reached, with a build warning.
  • process.stdout / process.stderr passed or stored where stream.Writable is declared is withheld by the last commit (e5da0f4dc3). The train had made that flow ask for the general stream representation by itself, so every such program reached main's conversion. Printing the writable state through both names after one operation through one name (500 runs, see "Evidence") found 103 runs on 83489c44ec that main refuses by name and that differed from Node with no diagnostic: a write through process.stdout after stream.end() or stream.destroy() was written; the stream stayed ended, destroyed and closed where Node's stdio stream is writable again; with the reader gone the program ended with status 0 where Node ends with EPIPE and status 1; a 200 kB write to a pipe and a corked write read writableLength 0 where Node reports the queued bytes. Now the flow stops at the build with SC1090: 'WriteStream' values where 'Writable' is expected is not supported yet, as on main; a program that asks for the general representation through another member reaches the conversion as on main, with main's 52 silent runs of the family unchanged. The cure (one writable state behind both names) is a train of its own, with the stdin one.
  • process.stdin passed or stored where stream.Readable is declared is withheld by the commit before it (83489c44ec). In Node the two names are one object with one readable state. The review of 21:38 UTC found that process.stdin.readableLength answered 0 while the stream held the unread input. Printing every readable-state property through both names (41 programs, two inputs each) found 20 cells on 53e1c31e7e that differ from Node with no diagnostic, from five causes: that one; process.stdin.destroy() not destroying the stream; input held before a late conversion missing from the stream; destroyed and closed after the end of a file's input; closed right after destroy() on a file's input. Main stops such a program by name, so the conversion is not made until the two names answer as one: the program stops with main's TypeError (expected Readable, got ProcessStream). The runtime unit scr_stream_stdin.c, its link feature and its fixtures stay in the tree and no program links the unit; the differential cases and tests of the conversion are skipped with Node's answers kept, and one test pins the stop by name for the 14 fixtures that convert. The conversion returns with those five causes cured and that family as its evidence.

What is refused

Every diagnostics fixture this branch adds or changes was built with main's compiler and with this branch's, site by site: no site is refused here that main builds. Three fixtures hold lines main refuses and this branch compiles (the typed union arm and the two nested generic function fixtures). Refusals this branch words anew, all at sites main refuses too:

  • left ?? right where the left side would be copied to reach the result type (SC1090): give both sides one type, or write the copy out.
  • A record holding a dictionary, converted to a record whose dictionary has another value type and written through (SC2021, SC1090, as on main; see 6 for the spelling).
  • A function created above let x: T; with no initializer that reads x (SC1030 instead of main's SC1090).
  • A nested generic function declaration used as a value (SC1090, named; main refuses the declaration itself).

process.stdin where stream.Readable is declared stops at run time with a TypeError that names both types, exactly as on main (see "Not in this train"). The limitations page says so.

A build-time refusal that becomes a run-time stop

let seen: { n: number } | null = null, assigned only inside a closure, then seen ?? word() with word(): string | undefined. The checker narrows seen to null and types the pair as string | undefined. Main refuses the line at build time. This branch builds it: while the binding is still null the program answers as Node does; when it holds a value the program stops with TypeError: a '{ n: number }' value is not representable in the target union (a value narrowed or asserted past it still held it), where Node answers the binding. Over 100 programs of the shape (five stored types of the left side, five types of the right, ?? and ||, the binding holding a value or not): 3 that main refuses stop with that error, 3 that main refuses now equal Node, none answers differently in silence, || is unchanged, and nothing main builds is refused. The build does not warn at such a site yet; that warning is the first item of the next train.

Evidence

All in Vercel Sandboxes. The pushed head is 0e6a32a9c3: five merges of main on top of 6ad5580db0. It holds main 9a6d7290f2 (#3471, #3473, #3474). Main was b4e908ef00 when it was pushed (#3476, #3477, #3478, #3479, #3480 and train cli 28 #3463 on top): that delta merges without a conflict, nine files are both sides' and no hunk of main's lies in a region this train changes, but nothing below ran on that merge; CI's merge commit runs it (see "Not run"). 0e6a32a9c3 merges main 9a6d7290f2. No file conflicted and no line is changed by hand. Three files that both sides change merged by themselves, and both sides' hunks were read. In lower-stdlib.ts main changes npmMemberFence and this train the Headers fences; in type-mapper.ts main adds packageMemberIsData, which calls two functions this train does not change. In lower-calls.ts both change lowerOrdinaryCall, in different branches: main answers a member call off a receiver that evaluates ((await import("./m")).f(1)) as the namespace member's own call, and this train adds the call of an optional callable that every arm of a record union declares, about 450 lines later; main's branch returns only for a namespace member, this train's only for a receiver that is a union of records (see "Where the two sides meet"). 33d8ebd7d8 merges main e0d43347aa (#3465, #3470, #3472). One file conflicted, the generated packages/compiler/src/ir/codec.generated.ts: it was regenerated from the merged sources, not resolved by hand. Eight files that both sides change merged by themselves (dyn.ts, emit-module.ts, emitter.ts, coercions/structural.ts, expressions/narrowing.ts, lowerer.ts, ir.ts, runtime-features.ts); main's hunks there are additions. 859d769484 merges main 8152700b48 (#3469). There is no textual conflict, but both sides change lowerNullishPair in expressions/binary.ts, and one line is changed by hand in the merge: main now lowers the default of a ?? b once, where it can be a literal built wider than the checker typed the pair, and replaced every later lowering of the default in that function with lowerDefault(); this train adds one more place that lowers the default (the present value that converts into the result and stays the value it is), which main could not see, and that place now calls lowerDefault() like the others. 067bd10eed merges main 7569a9ca6b (#3466 and #3467): no conflict, and main changes no file this train changes. dd8031f5b4 merges main 44f9f34d6b (#3461, #3464, and #3462, which moves the TypeScript 7 frontend to TypeScript 7.1). Two files conflicted there, both hand-written. In packages/compiler/src/frontend/lowering/class-protocols.ts both sides added an import at the same line (this train TypeSameness, main extendsElementOf), and both stay. In tests/harness/__snapshots__/child-process.ts.txt this train's fixture has two refusals fewer, which moves the remaining overload diagnostic from line 37 to line 26, and main re-recorded that diagnostic's type text as TypeScript 7.1 prints it; the snapshot was re-recorded by its test on the merged tree and is this train's file with 7.1's type text in its first line. The ts7 baselines are unchanged: the whole order-parity suite passes on the merged tree as recorded (353 of 353). 6ad5580db0 is a merge of main 6ae2f2d365 (train perf 8a #3458 and #3459) into 20e05369a6. One file conflicted there, the generated packages/compiler/src/ir/codec.generated.ts: it was regenerated from the merged sources, not resolved by hand. 20e05369a6 is a merge of main 0424e4e684 (train react 1 #3455, train cli 27 #3456, and #3457, which makes main's five red Linux tests green) into d57ec3e16c. Two files conflicted. In packages/compiler/src/frontend/lowering/expressions/narrowing.ts both sides added a function at the same place (this train classRecordMemberRead, main the functions for stranded instance arms), and both stay, no line of either changed. packages/compiler/src/ir/codec.generated.ts is generated: it was regenerated from the merged sources, not resolved by hand, and regeneration on the merged tree changes no generated file. d57ec3e16c is a merge of main cf9b741144 (trains #3451 and #3450, #3452, #3454, #2178 and the release preparation) into e5da0f4dc3. One file conflicted there, tests/harness/coverage.test.ts: both sides added a test at the same place, and both stay; nineteen files that both sides change merged by themselves, the lowering and backend files among them, and no generated file conflicted. e5da0f4dc3 is 83489c44ec plus the commit that withholds the Writable hand-on, and 83489c44ec is 53e1c31e7e plus the one commit that withholds the stdin conversion. 53e1c31e7e is the head of the full round below, then the commit that moves two stdin functions out of the stream unit, then four merges of main (6c1e5ac877, no conflict; d3f840eb2e, one conflict: both sides added a function at the same place in the call lowering, and both stay; 5d42c2b87c and a57eaacffd, no conflict). After each, the files both sides change were read. Main's runtime is built with the hardening flags of #3442 from the second of them on.

  • Gate-only round on the pushed head 0e6a32a9c3, against main 9a6d7290f2 (round 21; lanes, bench-runtime and Test262 skipped): passes. Type check passes. Gate: 5,814 programs identical, 176 allowed (below), 367 fail to build on both sides, none outside the list and no executable-only row outside it. The three more that fail on both sides are the Node island programs main gained with Read a shared module's var export only after its declaration ran, evaluate a dynamic import before a member read, and read package data members through a copy #3474: the gate builds without the Node pack, and the test list below builds and runs them with it. No program joins the list: main's new 2607-dynamic-import-member-read is byte-identical, and the two process-stream-write-error programs main changed with Fix the macOS process.hrtime clock and make the three macOS-failing corpus tests deterministic #3471 were listed already (they link the event dispatcher). Timing: bench geomean +0.44% [-0.29%, +1.32%] and fixtures +0.39% [+0.29%, +1.06%] within tolerance; compiler self-timing +1.37% [+0.57%, +2.20%], which this round reports as inconclusive (two runs; the full round one merge earlier, with the same train commits, measured +0.64% [-0.12%, +0.88%]). The compiler builds its own source in 548 s in one lowering pass, at 135 bytes per source byte against a budget of 136.
  • On the pushed head, in a sandbox with main 9a6d7290f2 built beside it:
    • Tests: the build, the generated-file checks (regeneration changes nothing), lint, format, the ts7 baselines (complete); the whole order-parity suite as recorded (354 of 354: it had not been rerun since the merge of TypeScript 7.1); 23 files of guard and unit tests (614 of 614); the type mapper tests (22); the whole diagnostics suite (429); the members' corpus programs in both backends (986 cases), the same programs and the diagnostics with the type walk memo in verify mode (525); the 4 programs main gained with Read a shared module's var export only after its declaration ran, evaluate a dynamic import before a member read, and read package data members through a copy #3474 and the 3 it changed with Fix the macOS process.hrtime clock and make the three macOS-failing corpus tests deterministic #3471, in both backends (14), sanitized (7) and in verify mode (7); the stdin tests plain and sanitized (44 passed and 18 skipped each); the node-types project tests sanitized (104); the cases of the test file that conflicted seven merges ago (6); the Node island suite (47).
    • The cell families, against main in the same sandbox: the writable-state family, 500 runs: 410 refused on both, 36 silent on both, 44 equal on both, 10 stop by name at run time on both, 0 hang; all 256 rows are main's, verdict and text. The readableLength cells: 9 of 9 rows are main's. The readable-state family: 73 of 73. The stdin family of 400 runs: 4 equal Node, 396 stop by name, 0 differ in silence, 0 hang; main: 3, 396, and the one control cell that hangs.
    • Where the two sides meet: main's member call off a dynamic import and this train's call of an optional callable of a record union, both in lowerOrdinaryCall: a program that calls a function off (await import("./m.ts")), calls the optional callable on records made through a namespace and through a dynamic import, and calls it on a namespace member that holds such a record (the one call both branches look at): 18 of 18 lines equal Node here and on main. The generic-function identity probe described below: 8 of 8 lines equal Node here; main differs in 4. The ?? probe of Answer a literal right operand of &&, || and ?? as it was built #3469 (next bullet): 7 of 7 here and on main.
  • Full round one merge earlier, on 33d8ebd7d8 against main e0d43347aa (round 20, nothing skipped; the lanes, bench-runtime and Test262 were not rerun on the pushed head, whose only difference is the merge of main 9a6d7290f2): passes. Lanes: plain 26,296 passed, 0 failed; sanitized 22,599 passed, 0 failed; both native bootstraps among them. Gate: 5,813 programs identical, 176 allowed (below), 364 fail to build on both sides, none outside the list and no executable-only row outside it. Timing, all within tolerance: bench geomean +0.31% [-0.13%, +0.98%], compiler self-timing +0.64% [-0.12%, +0.88%], fixtures -0.09% [-0.87%, +0.37%]. The compiler builds its own source in 560 s in one lowering pass, at 142 bytes per source byte against a budget of 136, inside the gate's 5% tolerance; that number is main's since Move TypeScript 7 support to TypeScript 7.1 #3462 (see the round on dd8031f5b4 below). bench-runtime: 75 workloads, geomean +0.5%, 74 neutral, 1 slower: value-records +11.4% [+2.7%, +37.1%], whose executable is byte-identical to main's in the same round's gate, so the mark is measurement (the two rounds before had it neutral). Test262 (Function, Reflect, JSON, Proxy, Object): 0 regressed, 0 gained.
  • On 33d8ebd7d8, in a sandbox with main e0d43347aa built beside it: the same test list with the same counts (without the order-parity suite), the cell families row for row main's, and the 5 programs main gained with Keep one object when an untyped value meets a typed record slot, and read typed arrays through untyped values from the array itself #3465 and Keep types that carry the collector header only for the weak analysis out of the collector #3470 and the one of Answer a literal right operand of &&, || and ?? as it was built #3469, in both backends (12), sanitized (6) and in verify mode (6), each of which builds to a byte-identical executable on main's tree and on that one. The ?? rules of Answer a literal right operand of &&, || and ?? as it was built #3469 and of this train (a present class instance defaulted to a literal that builds wider than the checker typed the pair, directly and through a || pair, with a write through the answer): 7 of 7 lines equal Node there and on main.
  • Full round three merges earlier, on 859d769484 against main 8152700b48 (round 19): passes. Lanes: plain 26,278 passed, 0 failed; sanitized 22,581 passed, 0 failed; both native bootstraps among them. Gate: 5,808 identical, 176 allowed, 364 fail to build on both sides, none outside the list and no executable-only row outside it. Timing: bench geomean -0.56% [-1.31%, +0.41%] and fixtures +0.54% [-0.25%, +1.38%] within tolerance; compiler self-timing +0.79% [+0.21%, +2.02%], which that round reports as inconclusive (the rounds before and after it measure it within tolerance). bench-runtime: 75 workloads, geomean -0.3%, 75 neutral. Test262: 0 regressed, 0 gained.
  • Full round five merges earlier, on dd8031f5b4 against main 44f9f34d6b (round 18):
    • Gate: 5,800 programs identical, 176 allowed (below), 364 fail to build on both sides, none outside the list and no executable-only row outside it. Timing, all within tolerance: bench geomean +0.09% [-1.00%, +0.97%], compiler self-timing +0.42% [-0.01%, +0.92%], fixtures +0.46% [-0.27%, +0.89%]. The compiler builds its own source in 518 s in one lowering pass, at 142 bytes per source byte against a budget of 136, inside the gate's 5% tolerance. That number is main's since Move TypeScript 7 support to TypeScript 7.1 #3462, not this train's: rounds of other branches against the same main measure 3,519 to 3,521 MB, this one 3,532 MB, and this train measured 130 bytes per source byte one merge earlier.
    • Lanes: plain 26,250 passed, 1 failed; sanitized 22,552 passed, 2 failed; both native bootstraps passed, plain and sanitized. The three failures are time-outs, so the round's own verdict line is FAIL: self-hosting-serialization.test.ts "llvm round-trips IR and produces a working program" at its 300 s limit (plain), and commander-calc at its 120 s limit in npm.test.ts and in npm-shape-overlay.test.ts (sanitized, one shard). Each passes alone, on this tree and on main's tree in the same sandbox, in the same time. The round trip: 120 s here, 117 s on main. commander-calc, sanitized, from a cold cache: 96.5 s here, 96.6 s and 93.8 s on main, of which 85 to 87 s are the sanitized link. Its compile by phase is equal on both trees, and its LLVM module has the same number of lines and functions. These are two budgets that main's tree nearly fills by itself and a busy lane machine exceeds; both tests have timed out in rounds of unrelated branches.
    • bench-runtime: 75 workloads, geomean -0.4%, 75 neutral. Test262 (Reflect, Function, JSON, Object, Proxy): 0 regressed, 0 gained.
  • The same lists on the heads between: on dd8031f5b4 all of them, with the same counts (612 unit tests) and the cell families row for row main's; on 067bd10eed and 859d769484 the build, the static checks, the unit, type mapper and diagnostics tests and the members' corpus programs in both backends and in verify mode. On 067bd10eed also the 7 programs main gained with Arenas no longer drop objects kept reachable through virtual calls, generators, library callbacks or callback-made cycles #3466 and Name the fields of the entry a layout-mode property copy builds #3467, in both backends (14), sanitized (7) and in verify mode (7): each builds to a byte-identical executable on main's tree and on this one.
  • On 6ad5580db0, against main 6ae2f2d365:
    • Gate-only rounds: 5,800 programs identical, 176 allowed, 364 fail to build on both sides, none outside the list and no executable-only row outside it; self-compile 535 s, one pass, 130 bytes per source byte. The first round reported bench geomean +0.53% [-0.49%, +2.39%] and fixtures +1.82% [+0.26%, +3.14%] as inconclusive; the second, with twice the timing runs, measured bench +0.36% [-0.27%, +1.10%], compiler self-timing +0.38% [+0.03%, +1.35%] and fixtures +0.58% [-0.18%, +1.04%], all within tolerance. The round one merge earlier, on 20e05369a6 against main 0424e4e684 (the same train commits): 5,798 identical, 176 allowed, 364 fail on both sides, none outside the list; bench geomean -0.23% [-1.66%, +1.22%], compiler self-timing +0.40% [-0.10%, +0.63%], fixtures +0.48% [-0.21%, +1.45%], all within tolerance; self-compile 594 s. The round before that one, with the list of 176, had two rows outside it of 6,356 programs compared: terminal-color-methods-piped.ts and terminal-method-answers-collected.mjs, which main gained with Train cli 27: destructured parameters without a static type, Promise.all entries, held createRequire loader members, primitive constructors as values, generic instances that bind several layouts, the terminal color methods, one island round per statement #3456 and which link the event dispatcher (see "Gate rows").
    • Where the two sides meet: main now gives a generic function one instance per set of type arguments its signature hides, and this train gives a generic function value one identity. One function demanded as a value at two hidden type arguments, then compared, added to a Set, used as a Map key and searched with indexOf: 8 of 8 lines equal Node here; main differs from Node in 4 of them without a diagnostic.
    • Tests: the build, the generated-file checks (regeneration changes nothing), lint, format, the ts7 baselines; 23 files of guard and unit tests (612 of 612: the two of main's that failed here before are fixed on main by Decide the checked-data promise writes can throw, record their unit reference, and prove factory-made exports through namespaces #3457); the type mapper tests (22); the whole diagnostics suite (430); the members' corpus programs in both backends (984 cases), the same programs and the diagnostics with the type walk memo in verify mode (524); the 2 programs main gained with that merge, in both backends (4), sanitized (2) and in verify mode (2); the stdin tests plain and sanitized (44 passed and 18 skipped each); the node-types project tests sanitized (104); the cases of the test file that conflicted two merges earlier (6); the Node island suite (47). The same lists ran on 20e05369a6 with the same counts, there with the 27 programs main gained with that merge (56, 28 and 28).
    • The cell families, against main 6ae2f2d365 in the same sandbox: the writable-state family, 500 runs: 410 refused on both, 36 silent on both, 44 equal on both, 10 stop by name at run time on both, 0 hang; all 256 rows are main's, verdict and text. The readableLength cells: 9 of 9 rows are main's. The readable-state family: 73 of 73. The stdin family of 400 runs: 4 equal Node, 396 stop by name, 0 differ in silence, 0 hang; main: 3, 396, and the one control cell that hangs.
  • Full round on 1c406f827c against the main it contains (a7783ba94c): type check passes. Lanes: plain 25,737 passed, 0 failed; sanitized 22,237 passed, 0 failed; both native bootstraps are among them, plain and sanitized. bench-runtime: 73 workloads, geomean -0.3%, 73 neutral. Test262 (Function, JSON, Proxy, Reflect, Object): 0 regressed, 0 gained. Timing: bench geomean -0.17% [-1.53%, +1.20%], compiler self-timing +0.55% [-0.29%, +1.05%], fixtures -0.36% [-1.42%, +0.81%]; the compiler builds its own source in 704 s in one lowering pass at 131 bytes per source byte (budget 136).
  • That round's identity check failed, and the next commit is the fix. 297 stream programs that do not convert stdin differed in the executable only: two functions added in the middle of scr_stream.c made the C compiler lay the unit's other functions and string constants out in another order (equal file size, equal symbol names and sizes). The two functions now live in the stdin unit; scr_stream.c is main's file and scr_stream.o is byte-identical to main's in all 12 flavor objects.
  • Gate-only round after the first merge (f6d05db1ac against main 6c1e5ac877): 5,724 programs identical, 173 allowed (below), 358 fail to build on both sides, none outside the list and no stream program among the rows; bench geomean +0.52% [-0.26%, +1.68%], compiler self-timing +0.30% [-0.37%, +1.13%], fixtures +0.69% [-0.77%, +2.16%]; self-compile 758 s, one pass, 131 bytes per source byte.
  • Gate-only round after the second merge (cd0bd7c6db against main d3f840eb2e): 5,736 identical, 173 allowed, 358 fail on both sides, none outside the list; bench geomean +0.73% [+0.08%, +1.42%], compiler self-timing +0.55% [-0.19%, +1.23%], fixtures +0.15% [-1.03%, +0.92%], all within tolerance; self-compile 535 s, one pass, 130 bytes per source byte.
  • After the third merge (ce3c3e0a04 against main 5d42c2b87c) the gate-only round was cut off by a restart of the machine that drives it, before its self-compile and timing lines: at that point 5,742 of 6,294 programs were identical, 358 failed on both sides and none differed outside the list. It was not repeated; the next round covers its tree.
  • Gate-only round on 53e1c31e7e against main a57eaacffd: 5,748 programs identical, 173 allowed, 358 fail to build on both sides, none outside the list and no stream program among the rows; bench geomean +0.31% [-0.48%, +1.23%], compiler self-timing -0.26% [-0.90%, +0.27%], fixtures +0.47% [-0.69%, +1.07%]; self-compile 498 s, one pass, 130 bytes per source byte.
  • Gate-only round on the withholding commit against main a57eaacffd (on its first form b8867c93a4; the pushed 83489c44ec differs from it in two test files only): 5,748 identical, 173 allowed, 358 fail on both sides, none outside the list and no executable-only row outside it; bench geomean +0.82% [-0.09%, +1.83%], compiler self-timing +0.59% [-0.05%, +1.17%], fixtures -0.35% [-1.22%, +1.63%], all within tolerance; self-compile 494 s, one pass, 130 bytes per source byte.
  • The stdin cell family (400 runs per tree: 9 states of process.stdin before the hand-on, 11 consumers of the Readable, 4 inputs). On the pushed tree: 4 equal Node, 396 stop by name with main's TypeError, 0 differ in silence, 0 hang. Main: 3 equal Node, 396 stop by name, 1 hangs; the one cell that differs is a control without a conversion (a 'data' listener with descriptor 0 closed), which hangs on main and ends here. With the conversion, on 53e1c31e7e: 340 equal Node, 60 stop by name.
  • The writable-state family (process.stdout and process.stderr; handed to a function typed Writable, stored in a typed field, or only a pipe / pipeline destination; 19 operations through one name; 11 writable-state properties read through both names; the reader holding a pipe, a file, or a pipe it closed early; 500 runs, each against Node, one source per cell in every tree). Main a57eaacffd against 83489c44ec: 103 refused by name to silent, 105 refused to equal, 202 refused on both, 52 silent on both, 28 equal on both, 10 stop by name at run time on both, 0 hang. Main against the pushed tree: 410 refused on both, 52 silent on both, 28 equal, 10 loud; all 256 rows are main's, verdict and text.
  • The readable-state family (every readable-state property through both names: 22 programs of one read and 19 scenarios, with three lines piped in and with empty input). With the conversion, on 53e1c31e7e: 20 runs differ from Node with no diagnostic (see "Not in this train"). On the pushed tree: all 73 rows are main's, row by row (64 stop with the TypeError, 9 are refused at build time by name).
  • On the pushed tree 83489c44ec: the build, lint, format, the ts7 baselines, the stdin tests plain and sanitized (44 passed and 18 skipped each; the skipped ones are the conversion's), and the cell families above. On its first form b8867c93a4, which has the same compiler and runtime source: the generated-file checks (regeneration changes nothing), 23 files of guard and unit tests (610 tests), the type mapper tests and the whole diagnostics suite (428).
  • On 53e1c31e7e: the build, the generated-file checks (regeneration changes nothing), lint, format, the ts7 baselines, 23 files of guard and unit tests (610 tests: the stream layout test and the island value matrix, 239 of 239, among them), the type mapper tests, the whole diagnostics suite (428), the members' corpus programs and main's newest in both backends (984 cases), the same programs and the diagnostics with the type walk memo in verify mode (524 cases), the stdin tests plain and sanitized (48 of 48 each, on the runtime built with the hardening flags of Harden the vendored libraries and the runtime's stack #3442).
  • On 1c406f827c, before the merges of main: island conformance in check mode (460 passed, 0 failed), order parity (153 of 153), the docs gate (25 route tests).
  • The three program families of 5, 6 and 7 were measured on the head before the Proxy refusal was taken out and before the merges of main; none of those touches their rules.
  • Probes on the earlier head c909fcd1a5 against the main it contains: the 13 fixtures site by site (above); 26 hand-written programs, each member's feature in a function the program never calls and in one it runs: every program main builds builds here with the same answer; the Proxy programs answer exactly as on main.
  • A large third-party TypeScript program, unedited, on c909fcd1a5: this branch reports about three fifths as many lowering errors as main alone (main measured one merge earlier), no site that main does not report, and one site with the ?? wording of 7 where main refuses the same expression in its own words. On 1c406f827c it reports the same 18 sites.

Gate rows

178 programs are allowed by id, one reason per class:

Not run

🤖 Generated with Claude Code

cramforce and others added 21 commits November 7, 2026 23:28
…ion one identity

A generic `function` declared inside another function compiles: it is specialized like a top-level generic declaration, over the bindings it captures.

A generic function used as a value has one identity per declaration. Its value instances are rooted in one interned closure, so `===`, Map and Set keys and listener removal agree with JavaScript when one declaration is held at several signatures, across objects, worker threads and library instances. The build-time pins that refused or warned about such comparisons are gone with the comparisons' bookkeeping. An initializer that names a thread-local is refused by name.

A program that holds one generic declaration at two value instances gains one closure initializer field; other programs are unchanged.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A Proxy stored where a class instance or a record type is declared has no native layout to be read through: the read would bypass the handler. The store is now a build-time refusal that names the slot's type, whether the Proxy has traps or not, instead of a failure when the program runs.

A binding a Proxy initializes is held as a checked-dynamic value, also when a closure captures it above its declaration, and a shorthand read of it is refused.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…rguments onto its shell line

Process streams. A process stream passed or stored where a stream class is declared (a `Readable` parameter, for example) is answered by the thread's one runtime stream, created on first use (runtime unit `scr_stream_stdin.c`, linked only by programs that use the form). Once `setEncoding` has been called, a consumer compiled for bytes is refused by name where a chunk is handed to it (a bytes `'data'` listener, `read()`, `for await`); async iteration hands out Buffers. While an encoding is set the stream still receives every raw chunk, so a character split across two reads is not lost.

Child processes. `spawn` and `spawnSync` with `shell` enabled and a separate argument list join the arguments onto the command as Node does, sparse lists included, with the DEP0190 warning; `shell` may be a boolean decided at run time (runtime unit `scr_child_shell.c`, linked only by programs that use the form). Values Node rejects stay refused.

`cp.shellLine` and `stream.stdioReadable` are declared never-throwing, and the shell unit's reference to the warning functions is listed as reached only through `cp.shellLine`.

The stdin checks live in the one build of `scr_events.c`, so every executable that links the stdin delivery path changes by a few hundred bytes; IR and LLVM are unchanged for them.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Each is a rule a neighbouring form already had.

- A typed `Headers` iterates through `keys()`, `values()`, `entries()` and `for...of`.
- `Promise.all` over an entry of type `Promise<T | undefined> | undefined`: the absent side joins as a promise fulfilled with undefined, so the position has one type whichever side settled. An optional entry whose promise cannot fulfill undefined keeps a refusal that names the spelling that compiles.
- `reduce` and `reduceRight` on a checked-dynamic array, also with a rest-parameter callback: the dynamic call passes JavaScript's four arguments.
- `Object.keys` and `Object.entries` over what `typeof v === "object"` leaves of a JSON value: an array or primitive arm beside the record arms answers through the checked-dynamic walk. `Object.entries` converts an index signature's values with the general untyped conversion and hands them back by reference, so a write through an entry reaches the object.
- `left ?? right` where the right side's type is wider: the present value converts into the result after the nullish test, and the default stays lazy. (A later commit keeps this form refused where the conversion would copy the left value.)
- A function created above `let x: T;` without an initializer: the dead-zone box of a forward-captured `const` serves when the slot can hold the unassigned state; a type that cannot keeps a refusal (SC1030).
- A call through an optional callable member that every arm of a record union declares, inside a generic function whose type parameter is bound to the union.
- A rest binding in a generic function is typed by the type argument's fields, so it keeps the fields the argument adds beyond the constraint.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…nary type as one type however it is spelled

A dictionary that keeps identity (it holds the record or class instance it was given, so writes are shared) can be an arm of a recursive union such as a JSON value: the union holds it by reference, and enumeration, writes and narrowing go through the same object.

A dictionary type is one type however it is written: `{ [key: string]: V }`, `Record<string, V>`, an alias, or the same spelling in another module share one decision about identity storage, so a value passed between two spellings is the same object. A mapped type whose members are not known yet is not compared.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…choose a union arm by the value's properties

Union members that share a literal of the member that separates them best form a class and are stored as one record over the members of all of them (members only some declare are optional slots), so a value of one member is the object the union holds and entering the union does not copy it. A member outside a class keeps its own arm when its discriminant names it, and a value under assertions is asked for the discriminant that names its arm. Library builds keep the member layouts. A class record that would have to be copied into a slot is refused.

Where several arms hold a record of optional members, a value entering the union takes the arm that declares the properties it has; routes by properties exist only among arms that keep each property at its own type, and an arm no rule places takes its site route first.

A dictionary record that is a union arm reads its key map at its own field when it converts to an untyped value (it crashed).

`scripts/union-matrix.mjs` generates the matrix of union shapes these rules were measured on.

Co-Authored-By: Claude Code <noreply@anthropic.com>
`left ?? right` whose default has a wider type than the left side's value
lowers the present value through the conversion a slot of the result type
applies. JavaScript answers the left side itself, so that is right only
where the conversion keeps the value. Where it copies, the result was a
second object: after `const list = papers ?? editions; list.push(x)` the
push was missing from `papers`, and `list === papers` was false. The copy
warning (SC6001) does not make that answer right, and before the rule
existed the expression was refused.

The rule now asks what the conversion it built does (conversionCopy in
width-copies.ts, which owns the copy families). A copy is a call of a copy
helper in the conversion or in any helper or adapter it reaches: a routing
helper that copies in one run-time arm, an adapter that copies a result of
each call. A checked conversion between two typed layouts is one too (a
promise stays the same promise, its array payload would be copied when it
settles), and so is an object built in place of the value. Such an
expression is refused with SC1090; the message names the copy (an array,
a tuple, a record, a class instance entering a record type) and the
spellings that compile: one type on both sides, or the copy written out
where a copy is meant, which Node then makes too.

What keeps the value still compiles: a function entering a function type
with more parameters or a wider result (its adapter is that function to
`===`), a class entering the type of its base class, a promise whose
payload type widens to a union of numbers and strings. A value with no
identity converts freely. The default stays lazy and the left side is
evaluated once, as before.

The check runs only inside this rule, which is reached after every other
`??` rule declined, so a program that compiled without the rule lowers as
it did: the IR and LLVM of existing `??` programs are byte-identical.

Tests: corpus nullish-wider-default-keeps-left replaces
nullish-default-widens-array, whose array cases are refused now, and pins
the forms that keep the value (identity, writes through both names,
laziness, present and absent, the explicit copy). Diagnostics fixture
nullish-wider-default-copies-left pins the refused forms: an array, a
tuple, a record of another layout, a class instance entering a record
type, a promise of an array; as a member read, a lookup and a parameter.
width-copies.test.ts covers conversionCopy on hand-built conversions.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The test compared the peak resident set of two runs of one fixture binary (800,000 and 1,600,000 keys) with `large < small * 1.05 + 8 MB`. The peak of a run is not one number: one binary on one machine peaks at about 105 MB or about 110 MB for 200,000 keys and for 2,000,000 alike, and on a loaded runner the smaller run peaked 15 MB below the larger one with nothing leaked, missing the bound by 2 to 4 MB.

The measure is now the difference of the two peaks divided by the number of additional keys, over a longer lever for the same number of iterations (400,000 against 2,000,000 keys, the collection cadence fixed at 20,000). The bound is 32 bytes per key; the levels can account for 12, and a leak costs 865 to 970 bytes per key (measured with the cross-heap collector off). The test also runs the leaking pair and requires more than ten times the bound from it, so the measure is shown to see a leak.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A dictionary that keeps the object it was given is an untyped value in
the lowered program. Converted to a dictionary type with another value
type it becomes a native dictionary built from its entries: a copy, as a
record converted to another layout is, and a write through either name or
an identity comparison tells the two apart. The lowered program alone does
not say where that happens. The conversion is the checked conversion every
untyped value takes, alone or inside a copy helper that is shared by
layout, and only the checker's type of the converted value says that the
value is such a dictionary.

The lowering notes the conversion where it makes it. After a slot
conversion of an untyped value, or of a value that holds one, it asks
whether the value's checker type has a dictionary that keeps identity
where the slot's lowered type has a native dictionary: the value itself,
or a field, an element or an entry of it. The note names the lowered
conversion by its place and type and carries the dictionary's own type
and the layout of its entries. It is a table beside the program, so the
lowered program is the same with and without it; a unit that adds a note
has no record and lowers again instead of being replayed without one.

The width copy scan counts the lowered conversions that carry a note as a
kind of their own. The copy is observed as every copy is: a key of its
type is written or deleted, values of its type are compared, or it reaches
code the scan cannot follow. The original has no layout to look for, so
any store through an untyped value counts as a write to it; the dynamic
store facts now hold the first such store, for the message. An entry whose
layout the copy's value type lacks is rebuilt as well and is a member fact,
paired as the checked conversions pair layouts. One conversion is one
warning, and its hint names the dictionary's own type.

The assertion predicate no longer counts such a dictionary as a copy. An
assertion that initializes a binding keeps the dictionary's object, so the
refusal of a write through that binding rejected programs that print what
Node prints.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Generated by the lane's update mode over the whole lane (427 programs), then checked without it.

- `npm/types-boundary` (island:rowparse) and `npm/zod3-boundary` (island:zod) match Node now and leave the list: the union and generic forms they stopped at compile.
- `npm/zod4-boundary` (island:zod) is refused with SC8010 alone instead of SC1090 and SC8010.
- `npm/hono-zod-validator` (island:@hono/zod-validator) no longer stops at the refusal of Headers iteration, which compiles now; it stops at the next difference, a TypeError of the Headers constructor.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The scan of noted dictionary copies called `this.layoutPairs`, which is a
function of the module since the `??` change, and the text of a
conversion's copy had no case for a dictionary copy: the two changes were
written side by side and the compiler package did not type-check with
both. The scan calls the function with the union lookup read into a local
first (called as `this.unionArms(…)` the lookup is handed the scan as its
receiver), and a dictionary copy has its own sentence.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A record whose field is a dictionary of one value type, declared or passed
as a record whose field is a dictionary of another, is refused when the
program writes through the declared name, the callee writes through the
parameter, or a write lands on an assertion's binding (SC2021, SC1090): the
conversion copies, and the write would reach only the copy. Those rules
compare the layouts of the two fields. Since every spelling of a dictionary
type that keeps the object it was given is that one type, such a field is
untyped, and so is a record literal that holds it: the rules found no pair
of fields, and the conversion compiled as a copy with a warning.

The three rules now ask the width copy module for that case: a record that
is untyped where it holds such a dictionary, converted into a record with
the same fields whose field is a native dictionary. They refuse with their
own codes, and the message names the field, the two dictionary types and
the spelling that compiles. The question is answered for a field whose
dictionary type keeps identity as another spelling of the type an object
entered: that field had a native layout before, and its conversion was the
one refused. A field declared with the very type the object entered never
had one; its conversion stays a reported copy, as it was.

The note of a copied dictionary now also covers the conversions that reach
the same copy another way: a tuple and a class instance that hold the
dictionary, and the checked conversion an assertion makes. They built the
copy with no warning.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…self-compile class guard

Sixteen files conflicted.

withChildArgsCheck (builtins/child-process.ts) is one function for both
sides. It keeps main's evaluation by source order and its `maxBuffer`
checks, and this branch's mapping of a shell launch with separate
arguments onto its joined line: the file, the arguments and `cwd` are
checked, the line is built, the environment is checked. The mapping is
the eighth parameter, after main's source order. No entry point passes
both a limit and a mapping today; the function orders them as Node does
all the same (a limit validated after the launch's values is validated
after the line is built, execFile's before it). The unit test's helper
reports both sides' calls, and both sides' tests are kept. The
diagnostics snapshot is regenerated.

The feature tables (executable features, link info, contracts, the unit
list, the runtime pack, the executable cache, the runtime feature rules,
the pack matrix) keep each side's rows: streamStdin and childShellLine
from this branch; streamPipeEvents, processFeatures, childStreamFlow and
functionOwnWrite from main. The table of calls that never throw keeps
both sides' rows.

lowerer.ts: finishModule takes main's signature and settleLoaderShareEnv
and does not call checkGenericIdentityPins, which this branch deletes;
coerceIntoSlot keeps this branch's Proxy and class-record checks and
main's CommonJS module fence. width-copies.ts: layoutPairs stays the
module function this branch made it (main edited the method to keep the
scan out of the dynamic tier; the function takes the union lookup as a
parameter, which serves the same end).

Co-Authored-By: Claude Code <noreply@anthropic.com>
The note of a dictionary copy compares the checker's type of a converted
value with the slot's lowered type, member by member and four steps deep,
to find a dictionary that keeps its object where the slot has a native
one. For a union of many record types whose fields hold the union again
(a syntax tree) every member was compared with every arm at every step:
the compiler's own build did not get past one declaration of
lower-comptime.ts, an untyped array converted to an array of IR
expressions (stopped after 55 minutes in that search; the whole build
takes about twelve).

A copy can only be found where the target is a native dictionary or holds
one within the steps left, which is a question about lowered types alone.
The search asks it first, per search and per type, and asks the checker
nothing about a target, an arm or a field that holds none. A pair of a
source type and an arm that was compared at a depth is not compared again:
the search ends at its first find, so the pair held none. Neither changes
what the search answers.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ts record

The members of a union that share one discriminant literal are stored as
one record (a class), and a value typed as one of those members is that
record. A value whose type is not one of the members but has the layout of
one (an inferred literal type, the member's type written a second time, an
`as const` literal, a function's inferred result, the type an assertion
names for a parsed value) kept that member's own layout. Storing it in the
union would have copied it into the class's record, so the conversion was
refused with SC1090. Before the classes such a value was the union's arm,
the one object under both types: programs that built and printed what
Node prints stopped building.

A type that belongs to no class and maps to the layout one of a class's
types has on its own now maps to the class's record (classRecordOfLayout
in type-mapper.ts). The value is built as that record where it is
created, so it enters the union at a binding, an argument, a return, a
push, a property or an assertion with no conversion: writes through
either name reach the one object and `===` holds. A class is decided the
first time one of its types is mapped; a type with a member's property
names that is mapped earlier decides the class then, so every type has
one answer in a pass. A member that itself holds such a type leaves the
class separate, like a member that holds its class.

Three places follow the record:

- A read of a member that the receiver's type requires and the class's
  record holds as optional is the checked extraction for these types too,
  not only for the class's own types: a value asserted past the member
  throws a TypeError instead of reading a slot that holds nothing.
- A rest binding whose type is a class's record leaves the members only
  the class's other types declare absent, where it refused them as fields
  the source does not carry.
- The same member written in two unions is two types with one layout.
  Their classes are now one class, so a value is one record in both
  unions; it was copied from one record into the other (reported by
  SC6001, and a runtime TypeError where the two records could not hold
  each other's value). Where the two unions' other members declare one
  name with different types no record holds both classes, and neither is
  a class: each member keeps its own layout, which the unions share.

The refusal is gone with its fixture: no use it covered was wrong before
the classes. A conversion that still copies (a value with a property no
member declares, or without a member's optional property) is the record
width copy it always was, in the helper family the SC6001 scan reads.
With the layout rule switched off and the refusal removed, every such
copy the program could tell from its source was reported.

Programs without a class map every type as before. Among programs with a
class, only those that hold a type with a member's layout outside the
class, or the same member in two unions, change.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This reverts "Refuse at compile time a Proxy that enters a class or
record slot" with its programs and fixtures.

The refusal named a Proxy stored where a record type or a class is
declared. Where the store runs it replaced a wrong answer. Where the
store is in code the program never reaches it rejected a program that
builds today and prints what Node prints: a Proxy assigned to a
record-typed field and passed to a record-typed parameter inside a
function that is never called. A build must not turn red for a program
that works.

Proxy stores behave as they did before the branch. The form comes back
as a store that throws by name when it is reached, with a build warning,
in a change of its own.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…tries

One file conflicted: the generated IR codec, where both sides add
entries. It is main's file regenerated on the merged tree by
scripts/generate-ir-codec.mjs; nothing else is written by hand.

Twenty-five other files changed on both sides and merged without a
conflict, the feature tables among them (executable features, link
info, contracts, the runtime pack, the executable cache, the runtime
feature rules, the pack matrix): each keeps both sides' rows.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The test feeds process.stdin three lines 150 ms apart and compares the
compiled program's output with Node's. The fixture printed the bytes of
its first read() and the byte count of the iteration that follows
separately, so its output depended on how many of the writes had arrived
when each process took its first read. On a loaded machine Node started
late and read two lines at once while the compiled program read one; both
were right for what they saw, and the test failed.

The fixture now reports that the first read and every iterated piece are
Buffers, and the count and bytes of all of them together. Under Node and
as a plain and a sanitized binary it prints the same three lines whether
the input arrives at once, as two writes and one, as one and two, or
spaced; the old fixture printed three different outputs over those runs.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…d download changes

Two files conflicted.

lower-calls.ts: the list of method names a checked-dynamic value
dispatches at run time. Main adds findLast, findLastIndex and flat where
this branch added reduce and reduceRight, and main adds those two as
well: the list is main's.

The diagnostics snapshot of array-callback-rest-parameters.ts: main
removed the fixture's three calls a checked-dynamic array now runs and
recorded the snapshot again; this branch had only recorded it again. The
snapshot is main's.

Fourteen other files changed on both sides and merged without a
conflict. Regenerating the codec, the surface manifest, the Node
compatibility ledgers and the refusal baseline on the merged tree
changes none of them.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Main's fixture logical-value-over-closure-assigned-binding.ts pins three
pairs over a `let` that only a closure assigns. The checker narrows such a
binding to its null initializer and types `seen ?? word()` as the right
operand alone. Main refuses that line at build time. This branch's `??`
rule builds it: the left side's stored value is converted into the pair's
type, and a value that type cannot hold stops the program by name
("value is not representable in the target union") instead of being
refused. While the binding is still null the program answers as Node
does; once it holds a value the program stops with that TypeError, where
Node answers the binding. The `||` pair and the always-null `&&` stay
refused.

The snapshot loses the line's error and the fixture's comments say what
happens. Over 100 programs of the shape (five stored types of the left
side, five types of the right, `??` and `||`, the binding holding a value
or not), three that main refuses stop by name, three that main refuses
answer as Node does, none answers differently in silence, and `||` is
unchanged. The build does not warn at such a site yet.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…l line's mapping

Two files conflicted.

withChildArgsCheck (builtins/child-process.ts) stays one function. Main
adds `effect` steps to the source order: the value of an option the
launch does not read is evaluated where its member sits, so the function
no longer returns early when the order holds one. This branch maps a
shell launch with separate arguments onto its joined line. The early
return takes main's condition and still answers the mapped launch when
nothing is checked, limited or ordered. The tail is this branch's:
checkAll() adds the checks that can be refused and answers the arguments
the entry point takes. For a launch without a mapping it adds exactly
main's statements: nothing where main's new guard adds nothing, the
check before the limit where a value can be refused. An effect is
evaluated at its step before the first check, with or without a mapping.
The doc comment keeps both sides' paragraphs.

project-config.test.ts: both sides append entries to the node-types
list; all three are kept.

Twenty-nine other files changed on both sides and merged without a
conflict, the feature tables among them. Regenerating the codec, the
surface manifest, the Node compatibility ledgers, the refusal baseline
and the child-process diagnostics snapshot on the merged tree changes
none of them.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Nov 9, 2026 1:30pm UTC

Comment thread packages/runtime/src/scr_stream_stdin.c Resolved
@cramforce

Copy link
Copy Markdown
Contributor Author

The gate-only round on this head (c909fcd1a5) against the main it contains (6642a17b5f) has ended; the body said its timing lines would follow.

  • Type check passes.
  • Zero-cost gate: 5,662 programs identical, 166 allowed (the classes in the body), 328 fail to build on both sides, 2 executable-only rows outside the list as it was when the round started: 9753-file-handle-stream-signal.ts and 9754-fs-stream-signal-option-values.ts, programs main gained with its file-stream signal change. They link the event dispatcher and belong to the first class in the body (IR and LLVM identical, scr_events_dispatch resized, +0.07% each); the body's count of 142 includes them.
  • Time: bench geomean +0.13% [-1.23%, +1.22%] and compiler self-timing +0.33% [-0.81%, +0.98%], both within tolerance. Fixtures geomean +0.50% [-1.35%, +2.46%]: the gate calls this interval inconclusive; the two earlier rounds measured +1.01% [-0.92%, +1.98%] and +0.68% [-1.15%, +1.64%], both within tolerance.
  • Self-compile: the compiler builds its own source in one lowering pass at 131 bytes per source byte (budget 136), with Lowerer, LlEmitter and CopyScan out of the dynamic tier; its source costs the same compile time as main's source built by the same compiler (-0.0%, one pair), with +0.6% LLVM bytes and +0.7% functions for +0.4% source bytes. The wall time of that build was 716 s in this round's shard (511 s and 524 s in the two earlier rounds).

Not run on this head: the full lanes, both native bootstraps, bench-runtime and Test262. They ran on the head before the three merges of main and the two test-only commits (the body's "Full round"); the third merge resolved a conflict in the child launch check function, which this round and both sides' child-process tests cover.

@cramforce

Copy link
Copy Markdown
Contributor Author

HOLD: do not merge yet.

The review bot left one finding on this train (packages/runtime/src/scr_stream_stdin.c:79): when process.stdin is paused before it is passed on as a Readable, async iteration of the converted stream never receives input, because its _read callback leaves the underlying stdin paused. A program that hangs where Node reads may not land: the finding is being reproduced under Node, on main and on this head. This hold is lifted here when it is fixed or shown not to apply.

🤖 Generated with Claude Code

cramforce and others added 4 commits November 8, 2026 08:08
In Node process.stdin is the Readable: handing it on creates no second
stream. The compiled program kept two paused states, one in the stdin unit
and one in the stream. After process.stdin.pause() the stream's reads
resumed nothing, the unit delivered nothing and counted as no pending work,
and the program exited 0 having read nothing where Node reads the input:
through async iteration, 'readable' with read(), readline, a pipe, or a
'data' listener after resume(). isPaused() answered for one name only.

The stream now holds the one state for both names. It starts in the state
process.stdin was put in before the hand-on (paused, resumed, read by a
'readable' listener, an encoding), and pause(), resume(), isPaused() and
setEncoding() through process.stdin act on it. The stdin unit becomes the
stream's source: the stream's _read is a pull, answered with the next input
whether the stream flows or is read in paused mode, so pausing stops 'data'
events and not reads. A 'data' listener registered on process.stdin gets
each chunk as the stream takes it, which is also what a read() hands such a
listener in Node; 'readable' and 'end' listeners registered there follow
the stream's events, and process.stdin.read() reads the stream.

A pull holds the process until the stream emits 'pause', as Node's stdin
stops reading there: the pull is taken back and the stream's read in flight
is closed with an empty chunk, so a paused stdin releases the event loop
and the next read, iteration step, 'readable' listener or resume() asks
again.

Two forms are refused by name (TypeError, SC2020) instead of answering
otherwise: for await over process.stdin in a program that also hands it on
(iterate the Readable), and a pipe of the Readable while process.stdin has
a 'readable' listener (in Node that listener keeps the stream paused; the
runtime's pipe starts the flow whatever listens).

A closed descriptor 0 is now an input that ends at once, as in Node, which
opens /dev/null there. Before, any program that read stdin with descriptor
0 closed waited forever.

Programs that do not hand process.stdin on take none of the new paths.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…he input does

Node reads a pipe, a socket or a terminal through a stream that reads once
more after the end of the input, and a file through one that does not. With
nothing held and nobody reading, a pipe's stream therefore emits 'readable'
and 'end', and a file's stream emits 'readable' and waits: 'end' follows the
next read, whoever makes it.

The stream process.stdin is handed on as ended in both cases. Under a
'readable' listener that never reads, with input redirected from an empty
file (or from a closed descriptor 0, where Node opens /dev/null), a 'data'
or readline consumer of the stream saw an 'end' that Node does not emit.

The stdin unit now tells the stream what its input is. For a file the
stream takes the end without reading on: a paused stream with nothing held
emits 'readable' and ends at the next read() through either name, the next
iteration step, a 'readable' listener added later, or resume(), which reads
once in Node whether or not the stream then flows. A stream that flows,
holds input or has an iteration waiting is read at its end in any case and
ends as before. The two functions this adds to the stream unit are linked
only by programs that hand process.stdin on.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…the type walk memo

Main gained three things this branch also touches.

Headers iteration is on main with its own profile rows, fixture and test.
This branch had turned the same rows on: main's rows, messages and test
stand, and this branch keeps its corpus program. The fence for Headers
iteration stays removed here, since nothing calls it once iteration is
static.

A union arm that enters the destination union by its own discriminant is
one more plan beside this branch's route by declared properties: both
plans, both branches of the conversion, and the copy scan takes the noted
dictionary conversions as well as main's named-copy filter. The `??` rule
keeps this branch's present-value conversion and main's rule for a left
side that holds only null or undefined.

The type walk memo keeps what a type mapped to inside one walk. The frame
keeps main's shape with this branch's class-record step after the
structural mapping; the back-reference sites note the back-reference and
then answer the checked-dynamic value where a dictionary keeps identity.
While a class of union members that share a discriminant literal is
mapped, a member answers its own layout to the class and the class record
to everyone after it: one type with two answers in one walk. From the
first read of a class that is unmapped or in progress the walk therefore
keeps and serves no more entries.

Promise.all and Promise.allSettled take main's lowering whole: its
promise-or-value entries cover this branch's optional promise entry.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Main's test of a binding initialized from `read && other` also pins that
`seen || fresh()` and `seen ?? fresh()` over a `let` only a closure assigns
are refused. On this branch the `??` pair builds: the stored value is
converted into the pair's type after the nullish test, and a value that
type cannot hold stops the program with a TypeError that names it. The
diagnostics fixture and its snapshot already say so; the unit test now does
too. `||` stays refused.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

Held again: please do not merge yet. The review of 21:38 UTC on head 53e1c31 is right, reproduced: after process.stdin is handed on as a Readable, process.stdin.readableLength answers 0 while the shared stream holds unread bytes. Node answers the same number through both names.

Three lines piped in, read 300 ms later (process.stdin.readableLength / the Readable's readableLength):

cell Node main this head
pause(), hand-on, 'readable' listener on the Readable 19 / 19 stops by name at the hand-on (TypeError) 0 / 19
hand-on, 'readable' listener on the Readable 19 / 19 the same TypeError 0 / 19
hand-on, 'readable' listener on process.stdin 19 / 19 the same TypeError 0 / 19
pause(), hand-on, one read(5) 14 / 14 the same TypeError 0 / 14

With empty input every runner prints 0 / 0. The cause is the one the review names: process.stdin.readableLength counts the stdin unit's own buffer, which the stream empties when it pulls. The 400-run cell family printed isPaused() and readableFlowing through both names and not readableLength, so it did not see this. The fix and its cells follow on this branch; the hold is lifted in a comment when they are pushed.

🤖 Generated with Claude Code

In Node, process.stdin and the Readable it is passed as are one object with
one readable state. The conversion this branch added answers a stream beside
the stdin unit, and the two still answer apart without a diagnostic:

- process.stdin.readableLength answers 0 while the stream holds the unread
  input (Node answers the same number through both names);
- after process.stdin.destroy() the stream answers destroyed false and
  readable true;
- input the stdin unit held before the conversion is not in the stream at
  the conversion: readableLength answers 0 and read() answers null through
  the stream, where Node hands out the held bytes;
- with input from a file, the stream answers destroyed and closed true after
  the end, where Node's stream for a file answers false.

Before the conversion existed such a program stopped at the Readable slot
with a TypeError that names both types. A stop by name must not become a
wrong answer, so the conversion is not made until the two names answer as
one: processStreamAs no longer answers stream.stdioReadable, and the program
stops as before (TypeError: expected Readable, got ProcessStream).

The runtime unit, its link feature and the fixtures stay; nothing emits the
call that links the unit. The differential cases and the tests of the
conversion are skipped with their Node answers kept, and one test pins the
stop by name for every fixture that converts. process.stdout and
process.stderr as a Writable are unchanged.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

On the review of 21:38 UTC (process.stdin.readableLength after the hand-on): the finding is right, and it is not cured here. The conversion is withheld instead (83489c44ec, pushed 22:21 UTC), so the program stops by name as on main.

The cells of the finding, three lines piped in, read 300 ms later (process.stdin.readableLength / the Readable's readableLength):

cell Node main before (53e1c31e7e) now (83489c44ec)
pause(), hand-on, 'readable' listener on the Readable 19 / 19 TypeError at the hand-on 0 / 19 TypeError at the hand-on
hand-on, 'readable' listener on the Readable 19 / 19 the same TypeError 0 / 19 the same TypeError
hand-on, 'readable' listener on process.stdin 19 / 19 the same TypeError 0 / 19 the same TypeError
pause(), hand-on, one read(5) 14 / 14 the same TypeError 0 / 14 the same TypeError

The TypeError is main's: Uncaught TypeError: expected Readable at $, got ProcessStream, exit status 1.

Why not the one-function fix. Before trusting it I printed every readable-state property the two names share through both names (readableLength, readable, readableEnded, readableFlowing, isPaused(), destroyed, readableHighWaterMark, readableEncoding, readableObjectMode, closed, errored), before and after a read, paused and not, at the end of the input: 22 programs of one read and 19 scenarios, each with three lines piped in and with empty input. On 53e1c31e7e, 20 runs differ from Node with no diagnostic, from five causes:

cause runs Node 53e1c31e7e
process.stdin.readableLength counts the stdin unit's buffer, which the stream empties (the finding) 7 19 or 14 0
process.stdin.destroy() does not destroy the stream 2 stream destroyed true, readable false false, true
input held before a late hand-on is not in the stream 3 stream readableLength 19, read() hands it out, the stream ends 0, read() null, not ended 200 ms later
input from a file (/dev/null): the stream after the end 7 destroyed false, closed false true, true
input from a file: right after stream.destroy() 1 closed false true

Through the process.stdin name the other nine properties are refused at build time by name, on main as well.

Main stops every one of these programs at the hand-on, and a stop by name must not become a different answer. Three of the five causes need more than the helper (one is how the stream is created, one needs a ruling on listener order), so the conversion leaves this train and returns with all five cured and this family as its evidence.

Now, on 83489c44ec (row by row against main, same sandbox):

family main 83489c44ec
the finding's cells (9 rows) 8 stop with the TypeError, 1 refused at build time the same 9 rows
readable-state family (73 rows) 64 stop with the TypeError, 9 refused at build time the same 73 rows
the train's stdin family (400 runs) 3 equal Node, 396 stop by name, 1 hangs 4 equal Node, 396 stop by name, 0 differ in silence, 0 hang

The one run that differs from main in the last row is a control without a hand-on (a 'data' listener with descriptor 0 closed): it hangs on main and ends here as in Node.

What the commit changes: processStreamAs no longer answers the stdin stream, so nothing emits the call that links scr_stream_stdin.c; the unit, its link feature and its fixtures stay in the tree. The conversion's differential cases and tests are skipped with Node's answers kept, one test pins the stop by name for the 14 fixtures that convert, and the limitations page says the conversion throws. process.stdout and process.stderr as a Writable are unchanged. A gate-only round against main a57eaacffd: 5,748 programs identical, 173 allowed, none outside the list (run on the commit's first form, which differs from the pushed one in two test files). The description is updated.

The hold of 21:46 UTC is lifted for this finding: the head no longer answers differently from main where process.stdin is passed as a Readable.

🤖 Generated with Claude Code

process.stdout or process.stderr passed, stored or returned where a
node:stream Writable is declared no longer asks for the general stream
representation by itself. The flow stops at the build by name (SC1090:
'WriteStream' values where 'Writable' is expected), as on main.

In Node the process stream and the Writable it is handed on as are one
object with one writable state. With the conversion made for every such
program, the two names answered apart without a diagnostic:

- a write through process.stdout after stream.end() or stream.destroy()
  was written and returned true, where Node reports 'write after end';
- the stream stayed ended, finished, destroyed and closed, where Node's
  stdio stream is writable again after end() and destroy();
- stream.setDefaultEncoding was not seen by process.stdout.write;
- with the reader gone, Node ends with the EPIPE error and exit status 1;
  the program ended with status 0 and no error on the stream.

A program that asks for the general representation by another member
(cork, end with data, a pipe) reaches the conversion as it does on main.
The fixture stdout-writable-parameter stays and is now tested to stop by
name.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

Held again: please do not merge head 83489c4. The question left open in the last comment has an answer: process.stdout / process.stderr handed on as a Writable has the same two-names defect the stdin hand-on had.

A family of 500 runs, each against Node (stdout and stderr; handed to a function typed Writable, stored in a typed field, or only a pipe / pipeline destination; 19 operations through one name; the writable-state properties read through both names; the reader holding a pipe, a file, or a pipe it closed early). In its first run, 104 runs that main refuses at build by name (SC1090: 'WriteStream' values where 'Writable' is expected is not supported yet) build on this head and differ from Node with no diagnostic; no run hangs.

The clearest rows on this head:

  • after stream.end(), process.stdout.write("late") is written and returns true (Node: write after end, nothing written);
  • after stream.end() or stream.destroy() the stream stays ended, destroyed and closed (Node's stdio stream is writable again);
  • with the reader gone, Node ends with EPIPE and status 1; the binary ends with status 0 and errored null.

The stopgap is the same as for stdin: the hand-on stops by name again, as on main. It is committed and in the sandbox checks now. One push follows; the full table against main comes with it, and the hold is lifted in a comment then.

🤖 Generated with Claude Code

@cramforce

Copy link
Copy Markdown
Contributor Author

Pushed e5da0f4dc3 (one commit on 83489c44ec): process.stdout / process.stderr where stream.Writable is declared stops at the build by name again, as on main. Still held until the gate-only round on this head reports (about 23:15 UTC); I lift the hold in a comment then.

The writable-state family, 500 runs, each against Node, one source per cell in every tree (stdout and stderr; a function typed Writable, a typed field, or only a pipe / pipeline destination; 19 operations through one name; 11 writable-state properties read through both names; the reader holding a pipe, a file, or a pipe it closed early):

main a57eaacffd to the tree 83489c44ec e5da0f4dc3
refused at build by name (SC1090) to differs from Node with no diagnostic 103 0
refused by name to equal to Node 105 0
refused by name on both 202 410
differs with no diagnostic on both (main's conversion, reached through another stream member) 52 52
equal to Node on both 28 28
stops by name at run time on both 10 10
hangs 0 0

On e5da0f4dc3 all 256 rows are main's, verdict and text. The 52 runs that are silent on main are main's and go to the cure train with the stdin causes.

Also on the pushed tree: build, lint, format and the ts7 baseline check pass; the project-config tests pass plain and sanitized (102, the fixture stdout-writable-parameter is now tested to stop by name); the event-loop tests pass plain and sanitized (44 passed, 18 skipped); the readableLength cells are main's in 9 of 9 rows and the readable-state cells in 73 of 73; the 400 cells read 396 loud / 4 same / 0 silent / 0 hang.

The diff is one clause out of needsGeneralNodeStreams in general-streams.ts, one sentence for two on the limitations page, and the test. The description is updated.

🤖 Generated with Claude Code

@cramforce

Copy link
Copy Markdown
Contributor Author

Hold lifted for head e5da0f4dc3. The gate-only round on this head against main a57eaacffd passed: zero-cost gate: PASS — 5748 identical, 173 allowed, 358 both-fail, 21 changed-by-pr, the same counts as the round before the Writable stopgap; compile time within tolerance (bench +0.39%, self +0.23%, fixtures -0.03%).

With the rows in the comment above, both process-stream conversions are withheld on this head and stop by name as on main: process.stdin as a Readable (TypeError at the slot) and process.stdout / process.stderr as a Writable (SC1090 at the build). Their cure, one state behind both names, is a train of its own. CI on this head is running; I report on it when the Linux lanes are in.

🤖 Generated with Claude Code

@cramforce
cramforce requested a review from ctate November 9, 2026 01:29
…s and the release preparation

Main moved from a57eaac to cf9b741 (trains lang-13 #3451 and
perf-7 #3450, #3452, #3454, #2178 and the release preparation).

One file conflicted, tests/harness/coverage.test.ts: both sides added a
test after the Node island copies test. Both are kept, this branch's
dictionary-copies test first and main's key-order (SC6014) test after
it; neither side's lines changed.

Nineteen files merged without a conflict although both sides changed
them, the lowering and backend files among them (generic-functions.ts,
lower-calls.ts, lower-stmts.ts, lowerer.ts, narrowing.ts, binary.ts,
type-mapper.ts, emitter.ts, dyn.ts, the builtin and feature tables). No
generated file conflicted, and regeneration on the merged tree changes
none.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

Merged main cf9b741144 into the train; the head is d57ec3e16c (a signed merge commit, fast-forward from e5da0f4dc3).

Main has since gained #3455 (four files, none of this train's, no conflict).

🤖 Generated with Claude Code

@cramforce

Copy link
Copy Markdown
Contributor Author

CI on d57ec3e16c, read by cause from the job logs:

  • Linux: four red jobs, two causes, both main's. Linux, plain, 13/20 and Linux, san, 13/20: tests/harness/runtime-unit-references.test.ts. Linux, plain, 9/20 and Linux, san, 9/20: packages/compiler/test/builtin-attributes.test.ts. One failed test in each job, nothing else. Main cf9b741144 fails the same two tests in the same four shards (they came with Accept any ArrayBufferView in the fs write paths #3452), and they fail on main's tree in a sandbox with the same text. Every other Linux job that has finished is green, Linux, san, native frontend checks among them.
  • macOS: three causes, none this train's. 9811-node-island-load-reads-clock/main.cjs and the test of the same clock load in the two "LLVM artifacts, no clang" jobs; and Node itself aborting on 9444-process-stream-write-error.ts and 9445-process-stream-write-error-js.mjs (Assertion failed: (fd > STDERR_FILENO), function uv__close, under node, before any compiled binary runs).

So the branch has no conflict with main and no red of its own; it cannot be green on Linux until main is. Once main has the two tests fixed, I merge main again. That fix will edit the table of builtins that can throw, which this train also changes, so the merge gets the type check, the fast test list and a gate round of its own.

🤖 Generated with Claude Code

cramforce and others added 2 commits November 8, 2026 20:30
Main is 0424e4e: train cli 27 and the fix of main's five red tests (#3457).

Two files conflicted. In narrowing.ts both sides added a function at one place, the train's classRecordMemberRead and main's check of a generic instance's slot read at its declared type: both are kept. codec.generated.ts is regenerated from the merged IR types with scripts/generate-ir-codec.mjs.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Main is 6ae2f2d: train perf 8a and #3459 (build-time values kept out of island snapshots).

One file conflicted, codec.generated.ts. It is regenerated from the merged IR types with scripts/generate-ir-codec.mjs, not resolved by hand.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

Pushed 6ad5580db0: two merges of main on top of d57ec3e16c. The branch conflicted with main twice since the last push, both times in the generated IR codec.

The merges

Main has since gained #3461 (5bb6fb9617); it merges into this head without a conflict and touches no file this train changes.

Proof on 6ad5580db0 against main 6ae2f2d365

  • Gate-only round: PASS. 5,800 programs identical, 176 allowed, 364 fail to build on both sides, no row outside the list, no executable-only row outside it. Self-compile 535 s, one pass, 130 bytes per source byte.
  • Timing of that round: compiler self-timing +0.10% [-0.25%, +1.11%], within tolerance. Bench geomean +0.53% [-0.49%, +2.39%] and fixtures +1.82% [+0.26%, +3.14%] are reported as inconclusive, not as within tolerance. One merge earlier the same train commits measured -0.23% [-1.66%, +1.22%] and +0.48% [-0.21%, +1.45%], both within tolerance. A second round with twice the timing runs is running on this head; I will post its three lines here.
  • Regeneration changes no generated file; the refusal registry, the codec, fields and node-compat checks, lint, format and the ts7 baselines pass.
  • Tests: 23 files of guard and unit tests, 612 of 612 (main's two red tests are fixed by Decide the checked-data promise writes can throw, record their unit reference, and prove factory-made exports through namespaces #3457, so the "Red on main" section is gone from the description); type mapper 22; diagnostics 430; the members' corpus programs in both backends 984, in verify mode 524; main's 2 new programs 4, sanitized 2, verify mode 2 (and its 27 from the merge before: 56, 28, 28); stdin tests 44 passed and 18 skipped, plain and sanitized; node-types project tests sanitized 104; Node island suite 47.
  • Cell families against main in the same sandbox: writable 256 of 256 rows are main's (500 runs: 410 refused, 36 silent, 44 equal, 10 loud, 0 hang, on both); readableLength 9 of 9; readable-state 73 of 73; the stdin family 396 stop by name, 4 equal Node, 0 silent, 0 hang (main: 396, 3, and the one control cell that hangs).
  • The generic-function identity probe: 8 of 8 lines equal Node here; main still differs in 4.

The allow list grew by two, to 178. The first round on 20e05369a6 had two rows outside the list: terminal-color-methods-piped.ts (+0.30%) and terminal-method-answers-collected.mjs (+0.27%), which main gained with #3456. Both link the event dispatcher: IR and LLVM identical, scr_events_dispatch resized. That is the existing class of 147, now 149.

🤖 Generated with Claude Code

@cramforce

Copy link
Copy Markdown
Contributor Author

The timing rerun promised above (round 17: the same head 6ad5580db0 against main 6ae2f2d365, twice the runs) is clean: all three lines are within tolerance, so the two "inconclusive" lines of the previous round were measurement.

validate train-pi-1b @ 6ad5580db0 vs 6ae2f2d36: PASS in 41.5m
time       bench geomean +0.36% [95% CI -0.27%, +1.10%]: within tolerance
time       self +0.38% [95% CI +0.03%, +1.35%]: within tolerance
time       fixtures geomean +0.58% [95% CI -0.18%, +1.04%]: within tolerance
zero-cost gate: PASS — 5800 identical, 176 allowed, 364 both-fail, 21 changed-by-pr over 8 shards

CI on this head, by cause: test (Linux, plain, 17/20), test (Linux, san, 5/20) and test (TypeScript 7 parity) fail on the ts7 preflight baseline of tests/corpus/9812-node-island-load-reads-zone-locale/main.cjs, which was red on main itself and is cured there by #3464; test (Linux, san, native frontend programs) lost its runner ("The runner has received a shutdown signal").

Main has since moved to 44f9f34d6b (#3462, TypeScript 7.1) and this branch now conflicts with it in packages/compiler/src/frontend/lowering/class-protocols.ts and tests/harness/__snapshots__/child-process.ts.txt. The merge of main is next; it is pushed only after the gate round on the merged tree.

🤖 Generated with Claude Code

cramforce and others added 5 commits November 8, 2026 23:52
Main is 44f9f34: #3461 (the receiver proof no longer follows a call that redefines call or apply), #3464 (one ts7 preflight baseline re-recorded) and #3462 (the TypeScript 7 frontend moves to TypeScript 7.1).

Two files conflicted, both hand-written.

packages/compiler/src/frontend/lowering/class-protocols.ts: both sides added an import at the same line, this branch TypeSameness and main extendsElementOf. Both stay; no other line of either side changed.

tests/harness/__snapshots__/child-process.ts.txt: this branch removed the two shell-argument refusals from the fixture, which moved the remaining overload diagnostic from line 37 to line 26, and main re-recorded that diagnostic's type text as TypeScript 7.1 prints it. The snapshot is re-recorded by its test on the merged tree, not typed: it is this branch's file with 7.1's type text in its first line.

lowerer.ts merged by itself. No generated file conflicted. The ts7 baselines are unchanged: the whole order-parity suite passes on the merged tree as recorded.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Main is 7569a9c: #3466 (arenas keep objects reachable through virtual calls, generators, library callbacks and callback-made cycles) and #3467 (the entry a layout-mode property copy builds names its fields).

No conflict. Main changes no file this branch changes: its files are the arena and region analyses, may-throw.ts, scr_dyn.h, scr_json.c, and seven new corpus programs with their costs and baselines.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Main is 8152700: #3469 (a literal right operand of &&, || and ?? is answered as it was built).

No textual conflict, but both sides change lowerNullishPair in expressions/binary.ts, and one line is changed by hand in this merge. Main lowers the default of `a ?? b` once, where it can be a literal built wider than the checker typed the pair, and replaced every later lowering of the default in that function with lowerDefault(). This branch adds one more place that lowers the default, for a present value that converts into the result and stays the value it is, which main could not see. Merged as written it would lower the default a second time and outside main's rule, so it calls lowerDefault() like the others.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Main is e0d4334: #3465 (one object when an untyped value meets a typed record slot; typed arrays read through untyped values from the array itself), #3470 (types that carry the collector header only for the weak analysis stay out of the collector) and #3472 (CI: the sanitized native-frontend builds run on two cores).

One file conflicted, codec.generated.ts. It is regenerated from the merged IR types with scripts/generate-ir-codec.mjs, not resolved by hand.

Eight files that both sides change merged by themselves: dyn.ts, emit-module.ts, emitter.ts, coercions/structural.ts, expressions/narrowing.ts, lowerer.ts, ir.ts and runtime-features.ts. Main's hunks there are additions (a new expression kind in ir.ts, a function after checkedDynCast in narrowing.ts, feature rules).

Co-Authored-By: Claude Code <noreply@anthropic.com>
Main is 9a6d729: #3473 (one descriptor per source file in the TypeScript 7.1 symbol table), #3471 (the macOS process.hrtime clock and three corpus tests made deterministic) and #3474 (a shared module's var export read only after its declaration ran, a dynamic import evaluated before a member read or call, package data members read through a copy).

No file conflicted. Three files that both sides change merged by themselves, and both sides' hunks were read:

lower-stdlib.ts: independent. Main changes npmMemberFence and its import; the train changes the Headers fences (fenceStaticHeadersMember, and it removes fenceStaticHeadersIteration).

type-mapper.ts: independent. Main adds packageMemberIsData after packageTypeIsData; it calls packageDeclarations and packageDataPart, which the train does not change.

lower-calls.ts: both change lowerOrdinaryCall, in different branches. Main moves the namespace member call into lowerNsMemberCall and answers a receiver that evaluates (a dynamic import); the train adds lowerUnionOptionalCallableCall as the last typed attempt before the method-call fence, about 450 lines later. Main's branch returns only for a namespace member, the train's only for a receiver that maps to a union of records, so neither sees the other's calls.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

Pushed 0e6a32a9c3: five merges of main on top of 6ad5580db0, no train commit added. The head holds main 9a6d7290f2. Main was b4e908ef00 at the push; that delta merges without a conflict and is CI's to run (last section).

The merges since the last pushed head

On the pushed head 0e6a32a9c3 against main 9a6d7290f2

validate train-pi-1b @ 0e6a32a9c3 vs 9a6d7290f: PASS in 84.6m   (gate-only: lanes, bench-runtime, Test262 skipped)
PASS  typecheck
PASS  zero-cost gate
      time       bench geomean +0.44% [95% CI -0.29%, +1.32%]: within tolerance
      time       self +1.37% [95% CI +0.57%, +2.20%]: inconclusive (rerun with more runs)
      time       fixtures geomean +0.39% [95% CI +0.29%, +1.06%]: within tolerance
      listed     178 programs in --allow-programs: 177 differ, 1 identical
      zero-cost gate: PASS — 5814 identical, 176 allowed, 367 both-fail, 21 changed-by-pr over 8 shards
  • No row outside the list of 178 ids, none executable-only outside it, and no id joins the list. The three more programs that fail on both sides are main's new Node island programs (the gate builds without the Node pack; the tests below run them with it).
  • The self-timing line is inconclusive in this round (two runs). The full round one merge earlier measured +0.64% [-0.12%, +0.88%] with the same train commits. Self-compile: 548 s, one pass, 135 bytes per source byte (budget 136).
  • Tests in a sandbox with main built beside it: regeneration changes no generated file; lint, format, ts7 baselines; the whole order-parity suite as recorded, 354 of 354; guard and unit tests 614 of 614; type mapper 22; diagnostics 429; the members' corpus programs in both backends 986, in verify mode 525; main's 4 new and 3 changed programs 14, sanitized 7, verify mode 7; stdin tests 44 passed and 18 skipped, plain and sanitized; node-types project tests sanitized 104; Node island suite 47.
  • Cell families against main: writable 256 of 256 rows are main's; readableLength 9 of 9; readable-state 73 of 73; the stdin family 396 stop by name, 4 equal Node, 0 silent, 0 hang (main: 396, 3, and the one control cell that hangs).

The full rounds, on the heads before

  • 33d8ebd7d8 against main e0d43347aa (round 20, one merge before the pushed head): PASS in 79.1m. Lanes: plain 26,296 passed, 0 failed; sanitized 22,599 passed, 0 failed; both native bootstraps. Gate: 5,813 identical, 176 allowed, 364 fail on both sides, none outside the list. Timing within tolerance on all three lines. bench-runtime: 75 workloads, geomean +0.5%, 74 neutral, 1 slower: value-records +11.4% [+2.7%, +37.1%] on an executable that is byte-identical to main's in the same round's gate, so the mark is measurement (the two rounds before had it neutral). Test262: 0 regressed.
  • 859d769484 against main 8152700b48 (round 19): passes, lanes 0 failed plain and sanitized, gate with none outside the list, bench-runtime 75 neutral, Test262 0 regressed. Its self-timing line was inconclusive (+0.79% [+0.21%, +2.02%]).
  • dd8031f5b4 against main 44f9f34d6b (round 18): gate, bench-runtime and Test262 pass; the round's verdict is FAIL for three time-outs in the lanes, which I do not read as this train's. self-hosting-serialization.test.ts "llvm round-trips IR and produces a working program" hit its 300 s limit (plain): alone it takes 120 s on this tree and 117 s on main's. commander-calc hit its 120 s limit in npm.test.ts and in npm-shape-overlay.test.ts (sanitized, one shard): alone and from a cold cache 96.5 s here, 96.6 s and 93.8 s on main's, of which 85 to 87 s are the sanitized link. Rounds 19 and 20 have them green.

Not rerun on the pushed head, and what CI proves

🤖 Generated with Claude Code

@cramforce

Copy link
Copy Markdown
Contributor Author

CI on 0e6a32a9c3 (run 42626021764, the merge with main b4e908ef00), read by cause from the job logs: 82 passed, 7 skipped, 2 failed. The two are one failure, and it is not this train's.

  • test (macOS, san, darwin 1/3) (and the aggregate test, which fails with it) fails on one test: tests/harness/terminal-geometry.test.ts > getColorDepth and hasColors on terminal streams answer as Node does for every environment rule. The answers on stdout are equal; the native run's stderr holds one more line than Node's, written by Apple's sanitizer allocator before the program starts: malloc: nano zone abandoned due to inability to reserve vm space.
  • Why it is main's. The test file is main's own: it is byte-identical on this head and on main b4e908ef00. Its first two tests run the native binary with MallocNanoZone=0 on the darwin sanitized lane for exactly this line (the comment at line 30); the third test, which came with the terminal colour methods (ba9dc56f15), passes its own environments and does not. The same job fails on the same test with the same one extra line on Find a promise again as the key of an untyped Map or Set #3483's run (42628626454), a change to promise keys of Map that touches no stream code.
  • No main run to name. Main's own push runs skip the macOS jobs (the latest completed one, 42629902640 on 21213e5fde, is green with them skipped), so the comparison is the other pull request's run, not a run of main.
  • Everything else that ran passed: the 40 Linux lanes (20 plain, 20 sanitized), the two Effect lanes of this run, test (TypeScript 7 parity), both tsgo and tsc-ts give the same IR jobs, and the 13 other macOS jobs. Skipped on this run, so not proven by it: the Node island conformance lanes, the native bootstrap jobs, the shape overlay jobs and the thread sanitizer.

Nothing is pushed for this; the cure is one environment entry in that test, outside this train.

🤖 Generated with Claude Code

This branch was successfully deployed

1 active deployment
Preview — 0e6a32a9 Deployed Nov 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant