Repository navigation
Conversation
…n identity A generic `function` declaration inside a function now compiles through the function-scoped generic machinery that already compiles a generic `const` arrow inside a function: its instances are module functions, the frame's captured values travel as trailing parameters (or through a boxed closure environment when the frame reassigns one), the declaration statement emits nothing, and hoisting needs no guard because the declaration answers for a call that comes first. The frame fences still refuse by name what the machinery cannot carry: the declaration taken as a value, a capture whose type depends on an enclosing type parameter, and `this`, `super` or `new.target`. Comparing a generic function against a value-typed operand answered false natively where Node answers true: the comparison pinned the generic to the operand's signature, so two uses of one declaration compared unequal. Each declaration now has one identity root. Every instance of a declaration held as a value at more than one type adopts the first instance as its identity root (`identityRoot`/`identityLength` on `IrFunction`), which the runtime's existing shared-root rule in `scr_closure_identity_equal` already honors for `===`, Map keys, `indexOf`/`includes` and JSON property reads. The pin machinery (`checkGenericIdentityPins` and the five pin records feeding it) is deleted. A comparison against a held value still pins the operand to that value's signature, and because the instances share one root it no longer matters which instance answers. IR: `IrFunction` gains `identityRoot` and `identityLength`; the closure initializer's optional trailing fields carry them, so programs with no such declaration emit byte-identical IR, LLVM and executables. Codec regenerated. Tests: corpus 8721 (nested declaration shape from pi's rpc-mode: two instantiations over a reassigned counter and a Map, a hoisted call, recursion, sibling closures, `arguments.length` beside a trailing-parameter capture) and corpus 8724 (identity roots across two signatures, `F | undefined` operands, instantiation expressions, Map keys, `indexOf`/`includes`/`lastIndexOf`, `Object.is`, module namespaces), both differential against Node; diagnostics fixtures for the two refusals that remain; the three pin-shape refusal fixtures and their snapshots removed. The diagnostics corpus snapshot for tests/diagnostics/generics.ts drops the nested-declaration SC1090 clause (its join refusal stays), and generic-specialization-boundaries' per-module-location test now pins the refusal that remains — the declaration taken as a value — where it used to pin the declaration itself, which now compiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Proxy is a checked-dynamic value. A slot typed as a class holds native
instances only, so `show(new Proxy(...))` with `show(p: Palette)` built
without a diagnostic and threw "expected Palette at $, got object" when
the statement ran. A slot typed as an interface or object type took a
copy of the properties, read through the traps once: later writes to the
target and writes through the slot were lost, silently.
The lowering now refuses a Proxy it can see entering either kind of slot
(as an argument, a field, a return value, an element, an assertion, or
through a `const` binding that holds one) with SC1090 and a hint that
says what to write. A binding a Proxy initializes whose type is a class
is refused at its declaration, because a class method the trap returns
runs with the Proxy as `this` and fails its receiver check. A Proxy whose
handler is the empty literal adds nothing to its target, so a record
slot converts it as it converts the target, as before.
A binding typed as an interface keeps holding the Proxy itself. That was
true for module-level bindings and `var`; function-local `const` and
`let` copied, and now hold the Proxy too. A target written as an
asserted object literal (`{} as Config`) is an untyped object, as in
Node, instead of a typed record whose traps the runtime refuses or, for
a class, an assertion that throws.
The limitations page gets a Proxy entry.
Co-Authored-By: Claude Code <noreply@anthropic.com>
`if (options?.transform) headers = await options.transform(headers)` was refused with SC1090 "method calls like 'options.transform'" when `options` is a union of records: a generic function's options parameter bound to the union of every per-kind option record, each declaring the member as the same optional function. The record-field call answers a union receiver only when every arm holds the member as a bare function, and the dynamic dispatch declines these records, so the call fell through to the fence. When every arm is a record that declares the member with one and the same `F | undefined` type, and the checker narrowed the member to `F` at the call, the call now reads the member with the shared-field union read, extracts the function arm the narrowing proved, and calls through the function value, as the single-record receiver's optional callback does. It is tried last, after every other lowering declined, so no call that compiles today takes it. Arms that disagree on the member keep the fence. Corpus: tests/corpus/union-receiver-optional-callable.ts. Diagnostics: tests/diagnostics/union-receiver-callable-members.ts. Co-Authored-By: Claude Code <noreply@anthropic.com>
`const { _meta, ...rest } = item` inside `listed<T extends Base>(item: T)`
copied only the constraint's fields: with `T` bound to a wider record,
`JSON.stringify(rest)` printed `{"name":"w"}` where Node prints
`{"name":"w","extra":1}`. The checker types the rest as `Omit<T, K>` and
lists that alias's members through the type parameter's constraint, so the
packed record missed every field the binding adds. When the constraint
declares nothing but the consumed keys the type had no mapping at all, and
the binding was refused with SC1031 ("rest bindings whose packed type has
no static mapping").
The standard library's `Omit<X, K>` over one bound type parameter, alone
or intersected with object types, with literal keys, now maps to the
binding's record without K. A rest copy therefore holds what JavaScript's
CopyDataProperties copies, and agrees with the `Omit<T, K>` the call site
resolves for the function's result. An intersected type that would add a
member of its own to the result has no slot in the binding's layout: the
type stays unmapped when the member walk would also lose a field of the
binding, which refuses the rest binding instead of dropping fields, and is
left to that walk when it names them all.
Corpus: tests/corpus/generic-rest-wider-binding.ts.
Diagnostics: tests/diagnostics/generic-rest-asserted-member.ts.
Co-Authored-By: Claude Code <noreply@anthropic.com>
`spawnSync(command, args, options)` with a typed options value was refused
when `args` could be non-empty, and so was an inline `shell: true` beside
separate arguments: Node joins the list onto the command unescaped and
reports DEP0190, and the refusal assumed a native build could not report
it. It can. Node 24's normalizeSpawnArguments emits the warning exactly
when the shell option is on and the list is non-empty, once per process,
through process.emitWarning, whose tail the runtime already implements
(scr_warning_emit_deferred: the next tick, noDeprecation, throwDeprecation,
the 'warning' listeners and the default report).
`cp.shellLine(file, args)` answers the line Node runs: the file for an
empty list, otherwise `file + " " + args.join(" ")`, reporting DEP0190 for
the first non-empty list of the thread. A typed options value reads its
`shell` member at the call and passes the line with an empty argument
vector when it is on, the command and the list as they are when it is
off; an inline `shell: true` passes the line. The existing shell entry
points run it unchanged.
The entry lives in scr_child_shell.c, a unit of its own selected by the
childShellLine feature, so scr_child.c keeps its bytes (its comments
change with the same line count) and a program that never joins a line
links what it linked before. spawn and execFile keep their refusals; their
hint no longer says the warning cannot be reported.
Corpus: tests/corpus/spawnsync-shell-args.ts and
spawnsync-shell-args-listener.ts. tests/diagnostics/child-process.ts loses
the two spawnSync refusals.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The shell line read each argument with the accessor that traps on a hole,
so a sparse list passed with the shell enabled ended the program (exit
134, "array index 1 is a hole"). Node builds the line with
args.join(" "), which reads a hole as the empty string: the hole leaves
its separator in the line and nothing else, and the list still counts as
non-empty for DEP0190. The join now borrows each element and skips a
missing one.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Two cases from review.
A function-local binding that a closure captures before its declaration,
or that its own handler captures, is declared early as a TDZ box with
the declared record type. The Proxy was then converted into that record
slot, which the previous commit refuses, so a valid program got SC1090.
The early declaration now gives the binding checked-dynamic storage, as
its own declaration does.
Both give that storage only where the declared type is a slot that
would refuse or copy the Proxy. A binding typed as a function keeps its
slot, which wraps a callable Proxy, so 7445-crypto-key-pair-options.mjs
is unchanged.
A `const` binding that holds a Proxy, read through a shorthand property
(`{ view }`), was not recognized as that Proxy and was still copied into
the record field. Reads are now recorded by the expression they lower
to, for identifier and shorthand reads alike, and the object literal's
own field conversion asks the same question as the two conversion
funnels.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Two conflicts. The generated IR codec's layout digest: the codec is regenerated from the merged ir.ts (pnpm generate:ir-codec). function-references.ts, where main added the walk that drops lifted functions nothing reaches: an instance names its identity root for itself, like a closure its jsval entry, so the walk reaches the root when it reaches an instance that adopts it, and an unreached instance does not keep its root. Co-Authored-By: Claude Code <noreply@anthropic.com>
…declared In Node process.stdin is a Readable and process.stdout and process.stderr are Writables, so a program may hand them to anything that takes one. A natively compiled program represents process.stdin as a handle with its own members, and passing it where `stream.Readable` is declared failed the checked conversion at run time: `expected Readable at $, got ProcessStream`. The handle now answers its one stream, as process.stdout and process.stderr already answer theirs under the general stream representation and as a cipher handle answers its Transform: - `stream.stdioReadable` is the thread's Readable for descriptor 0, created by the first conversion. The stdin unit stays the only reader of the descriptor: the stream registers with it when it is first read and pushes what arrives, so a chunk reaches a listener on process.stdin and a listener on the stream once each. The code is a unit of its own (scr_stream_stdin.c, link feature streamStdin), linked only by programs that convert. - process.stdin flowing into a Readable slot (a parameter, a field, an annotated binding, a record member) is that stream. - process.stdout and process.stderr flowing into a Writable slot ask for the general stream representation when nothing else in the program did; the flow was refused before (SC1090). - A value of the stream class compared with the process stream it was read from compares the two streams, so `held === process.stdout` is true as in Node. Listeners registered on process.stdin itself and on the stream are two tables; the limitations page says what follows from that. A process stream held as an untyped value still fails the conversion. Co-Authored-By: Claude Code <noreply@anthropic.com>
… across objects, threads and library instances The address of a thread-local is not a constant. LLVM accepts `ptr @tls` in an initializer anyway, and the link then refuses it (GNU ld, when the two globals are in different objects), or produces a value that is no thread's copy: a crash at start or a silent wrong answer. The identity roots of thread-local closures were such constants until the previous commit; the emitter now refuses any module whose initializer names a thread-local (thread-local-constants.ts, run on every module's final text), so the next one is an internal error at compile time on a small program. The walk that drops lifted functions nothing reaches follows an instance's identity root, tested beside it. Evidence for the cases a one-declaration program does not reach: - native-worker-generic-identity-objects: twelve declarations, each held at two signatures by two functions, so the closures and their roots are in different objects of the divided build (the refused link). - native-worker-generic-function-identity: instantiation expressions, union operands, unknown, Object.is, Map and Set keys, searches, a callback slot, name and length, in the main thread and in a worker, and a function that cannot be posted. - the thread-instanced library fixture: a generic held at two signatures answers as one function in each of the four instances. Co-Authored-By: Claude Code <noreply@anthropic.com>
The one conflict is lowerer.ts: main added refuseSourceFieldDeletes beside the two calls of checkGenericIdentityPins, which this branch deletes. The new calls stay, the deleted check stays deleted. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Each is a rule the neighbouring shape already had.
Promise.all over an optional entry that carries a value. The tuple form
took `Promise<void> | undefined` entries and refused
`Promise<T | undefined> | undefined` (`this.opening?.catch(() =>
undefined)`). The undefined side of such an entry now joins as a promise
of the same type fulfilled with undefined, so the builder reads the
position at one type whichever side settled. An optional entry whose
promise's value cannot be undefined would fulfill two layouts at one
position; it keeps a refusal, now with a name and the spelling that
compiles.
reduce and reduceRight on a checked-dynamic receiver. A TypeScript source
dispatched map, filter, find, some and every on the value's kind and
refused reduce with "method calls like 'xs.reduce'". The runtime already
runs both.
Object.keys and Object.entries over what `typeof v === "object"` leaves of
a JSON value. The value keeps its stored union (`Json[] | boolean | number
| string | { [key: string]: Json }`), and the union enumeration took record
arms only. An array or primitive arm beside the records now answers through
the checked-dynamic walk a lone array or primitive argument takes, which is
Node's own for each (an array's indices, a string's indices, nothing for a
number or a boolean), and an index signature's value boxes into an `any`
element as a declared field does. Object.values of the same value is typed
any[] and keeps its SC2020. So does every member in a program that gives
the dictionary type identity storage (it converts a fixed record into the
type and writes through it): there the narrowed type has only the
checked-dynamic form, and pi's own statement is in that case.
`left ?? right` where the right side is wider. After every existing rule
declines, the present value converts into the result with the conversion a
slot of that type applies, after the original nullish test; the default
stays lazy. `c ? left : right` already compiled this way.
A function created above `let x: T;`. The dead-zone box a forward-captured
`const` uses now also serves a `let` without an initializer when its slot
can hold the unassigned state (an undefined-armed union, a checked-dynamic
binding); the declaration is the store that ends the dead zone. A type
that cannot hold undefined keeps a refusal that says so (SC1030), the twin
of the forward `var` one.
Corpus: promise-all-optional-value-entries, reduce-checked-dynamic-array,
object-entries-narrowed-json-union, nullish-default-widens-array,
forward-captured-uninitialized-let. Diagnostics:
promise-all-optional-entry-value, object-values-narrowed-json-union,
forward-let-unassigned-state.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The runtime has had the live Headers iterator since untyped values got it (sf_headers_step: Fetch's sort-and-combine list, re-read on every step, Set-Cookie values one by one). A typed Headers was still refused: the profile listed keys, values, entries and [Symbol.iterator] as unsupported, and iteration syntax over a Headers had a fence of its own. The four profile rows are static now, each with an operation and the static fixture as evidence. A direct call dispatches on the handle as forEach does and answers the runtime's iterator; for...of, spread, array destructuring, Object.fromEntries, new Map, new Set, Array.from and next() then take the paths they take for any checked-dynamic iterator. The bundled declaration gains the three methods. A method read as a value stays fenced. No runtime change. tests/corpus/headers-typed-iteration.ts pins the forms against Node; the fetch fixture gains the iterator lines the profile's evidence names; the surface manifest and the compatibility ledger are regenerated (the Headers row gains the corpus program as evidence). Co-Authored-By: Claude Code <noreply@anthropic.com>
…ing is set After process.stdin.setEncoding Node delivers strings to 'data' listeners. A listener is compiled for the type its parameter has, and a parameter with no annotation takes the declaration's type, which is bytes. The stdin unit handed such a listener the raw chunk although an encoding was set, so `data += chunk` appended the bytes as decimal numbers: a piped prompt read that way reached its consumer as "83,97,121,...", with no error. The delivery is now a TypeError with code SC2020 that names the fix: declare the parameter as string, which receives the decoded text (split multi-byte sequences kept whole). It fires only where Node would deliver a string. The runtime's own byte consumers (readline, the stdin stream) keep the raw chunk. The bundled declarations gain the string listener overloads, so the fix can be written without @types/node. Making an unannotated listener receive the string needs its parameter compiled at a type the checker does not give it; that is not in this change. process.stdin.read() and async iteration still answer bytes after setEncoding; the limitations page says so. Co-Authored-By: Claude Code <noreply@anthropic.com>
… is set After process.stdin.setEncoding Node's read() and async iteration hand out strings. Both are compiled for bytes and have no string form, and they handed out a Buffer (measured: typeof "object" where Node's is "string"), silently. With an encoding set they now throw a TypeError with code SC2020 that names the way out: leave the encoding unset and decode the Buffers. The iteration step answers a promise rejected with it, which the loop's await throws. Co-Authored-By: Claude Code <noreply@anthropic.com>
`type Json = null | boolean | number | string | Json[] | JsonObject` with
`JsonObject = { [key: string]: Json }`: once a record enters `JsonObject`
and is written through it, the dictionary keeps checked identity and is
the checked-dynamic value. Every object held in `Json` was then empty:
`JSON.stringify(JSON.parse('{"b":1}') as Json)` printed `{}`, and so did
a literal and the dictionary itself.
The mapper walks a dictionary's value type before it answers that the
dictionary is checked-dynamic. The value type led back to the dictionary,
that back-reference got a placeholder record, and the union closed over
the placeholder, which nothing ever completed. A dictionary with a
symbol-keyed member had the same hole without any identity.
A back-reference to such a dictionary now reads the checked-dynamic value,
and so does a back-reference to a union that holds one: the union then
takes the existing `string | object` collapse, in either mapping order,
or stays unmapped when an arm cannot ride it (a Map beside the
dictionary), and the refusal names that arm.
Co-Authored-By: Claude Code <noreply@anthropic.com>
A union of records has a discriminant only when every literal of one
member belongs to one layout. Two members with one literal and different
layouts (`{ type: "retry"; source: "a" } | { type: "retry"; source: "b";
reason: string }`) cost the whole union its discriminant, and its values
were then placed by layout: a record that fits two arms by width was
refused (SC2003), threw at run time behind an assertion, and from JSON or
`unknown` entered the wrong arm.
The record types of a declared union that share a literal of its deciding
member are now a class, and every type of a class maps to one record over
the members of all of them, wherever the type is used. The union keeps one
arm per class and the member is a total discriminant. A value of one
member's type is the class's record, so it enters and leaves the union as
the same object.
- union-classes.ts finds the classes from the union types the program
writes down, before any type is mapped (a whole-program fact, registered
in program-facts.ts), and builds a class's record.
- The type mapper maps a class member to its class's record; a class whose
members cannot be one record keeps its layouts.
- A member a class member requires that the record holds as optional is
read through the checked extraction: a value asserted past its members
throws a TypeError.
- A value whose type only has the layout of a class member would be copied
into the class's record; that entry is refused by name (SC1090) unless it
is a literal written at the slot.
- A conversion of `x as T` traps the arms the checker proved away at `x`,
as a conversion of `x` does.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Review of the previous commit: the entries of a JSON value's object arm are typed `[string, any][]`, and the index signature's values were boxed into the `any` element by copy. `const [, list] = Object.entries(o)[0]; list.push(x)` then wrote to a copy, and `list === o.list` was false, where Node hands back the object's own array. The value is boxed by reference now (dynFrom's liveRef, the capsule form a checked cast unwraps to the same object), so a write through an array or record read this way reaches the object's own, as it already did for the array arm's elements. tests/corpus/object-entries-narrowed-json-union.ts pins the identity and the writes for both arms. Co-Authored-By: Claude Code <noreply@anthropic.com>
One small program per cell (what tells a union's arms apart, how many arms, which value, how the value reaches the union, what is done with it), run under Node and as a native binary, with a verdict per cell. It is the instrument behind union-classes.ts: a cell is right, or refused at build time. Co-Authored-By: Claude Code <noreply@anthropic.com>
…s a union arm A record with only an index signature that is converted to an untyped value checks whether its key map has a null prototype. That read took the map from the second field of the record, which is where it sits when the record has a one-word header. A record that is an arm of a pointer-represented union has a two-word header, so the read took the tag word as a pointer: a segmentation fault for a dictionary that travels as `unknown` into a union that holds it, and for a union value asserted to its other arm. The map is read at the field after the header, as the two other reads in this helper already do. Co-Authored-By: Claude Code <noreply@anthropic.com>
The stdin unit hands the stream raw byte chunks, and the stream's async
iteration yields a chunk as it was pushed, so `Buffer.isBuffer(chunk)`
was false in `for await (const chunk of stream)` where Node's stdin
yields Buffers ('data' listeners and read() already answered Buffers).
The chunk is marked a Buffer before it is pushed, as the unit marks the
chunks of its own iteration. Found by the review bot.
Co-Authored-By: Claude Code <noreply@anthropic.com>
…ss' into fix/stdin-encoding-bytes-listener # Conflicts: # tests/harness/event-loop-cases.ts
Review of the previous commit: it boxed every index-signature value with
a live reference, which is only valid for a record, an array or a union
that holds one. `Object.entries` of `string[] | { [key: string]: number }`
then failed IR validation (SC9001, "live dynFrom of unsupported type
f64"), and so did an index signature of `boolean | string`.
The value now takes coerceToExpected, the conversion any value takes into
an untyped position: it hands a record or an array back by reference, also
through a union, and boxes a primitive as a primitive.
tests/corpus/object-entries-narrowed-json-union.ts gains index signatures
of primitives beside an array arm.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
The lowerer's journal inventory test lists every collection of the type registries as journaled or as mapping scratch. The union registry's back-reference answers unwind with the union's frame, before mapType returns, like `unions.inProgress`. Co-Authored-By: Claude Code <noreply@anthropic.com>
After process.stdin.setEncoding, read() and for await over process.stdin were refused by name on entry, before it was known whether they hand out a chunk. read(0), which Node uses to trigger a refill, and a read() with nothing held hand out nothing, so there is nothing to mistype: they answer null again, and the string 'data' listener behind them runs. An iteration that gets no input ends instead of rejecting. The refusal now sits where a chunk would reach the bytes consumer: in read() after the size is known to be satisfiable, and in the delivery that settles the iteration's parked step. Co-Authored-By: Claude Code <noreply@anthropic.com>
A library build's sidecar names every occurrence of a composed union's repeated arm and tracks an integer obligation per occurrence, which needs a layout per occurrence: with the members of such a union stored as one record, four obligations that must refuse (SC4022) were no longer seen. Library builds pass `separateUnionMembers`, and the shared lowering facts then hold no classes. The classes are read from the round's module order only, as the other shared facts are; a Lowerer built over a stub program has none. Co-Authored-By: Claude Code <noreply@anthropic.com>
Main's refusal lint (#3137) lists every refusal site of the runtime. The baseline is generated: it gains the two stdin refusals of this branch (a Buffer 'data' listener and read() after setEncoding) and the seven sites main's own later merges added without it, and drops four sites that are gone. Co-Authored-By: Claude Code <noreply@anthropic.com>
…ys after #3135 and its refusal baseline) into train/pi-1 Co-Authored-By: Claude Code <noreply@anthropic.com>
… fix One entry: the weak-collection refusal is raised in scr_weak_set now. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…vent names One entry: #2766 reworded the event emitter's refusal for a symbol event name. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
New head Compile time, second round (the head before this push, What the push adds:
Checked on this tree in a Sandbox before the push, since One of those guard tests is red, and it is main's: The zero-cost gate's identity on this head runs beside CI and follows here. |
|
The zero-cost gate's identity on the pushed head ( Compile time in this round, at one fixture run and one pair per self-compile shard: benchmarks +0.45% [95% CI -1.11%, +2.12%] and fixtures +1.32% [+0.11%, +2.52%], both called inconclusive at that run count; self-compile +0.35% [-2.01%, +1.45%], within tolerance. The round with five fixture runs on the head before this push (same members; the push added merges of main and a generated list) is the conclusive one: benchmarks +0.65%, self-compile +0.00%, fixtures -0.05%, all within tolerance. |
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. Measured with one binary on one 8-vCPU machine: 105.0 to 105.3 MB or 109.5 to 110.7 MB, for 200,000 keys and for 2,000,000 alike; 110.5 to 113.0 MB with eight or twenty-four processes at once. On CI's runner and in a 24-vCPU lane box the smaller run peaked at 96.4 and 93.5 MB beside 111.3 and 110.7 MB for the larger one, and the bound was missed by 1.9 and 4.4 MB with nothing leaked: the fixture's LLVM text is the same from main's compiler and from this branch's, and both pass 6 of 6 in one sandbox. What a leak costs, measured with the cross-heap collector off (no entry is released): 258, 431, 778 and 1,454 MB at 200,000, 400,000, 800,000 and 1,600,000 keys, 865 to 889 bytes per key; 964 to 970 between 50,000 and 150,000 keys. 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: 50 MB over the 1,600,000 additional keys, where the levels can account for 18 MB (12 bytes per key) and the leak above for 1.3 GB. 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. Runs of the changed test: 20 of 20 pass (5 on this branch, 5 on train pi-2, 10 on plain main, two sandboxes, two at once). The fixture's pairs directly: 20 pairs with eight processes at once differ by at most 0.6 MB (0 bytes per key), 24 pairs with twenty-four at once by at most 1.7 MB (1 byte per key). Co-Authored-By: Claude Code <noreply@anthropic.com>
|
New head CI on
The weak-key memory bound. The test compared the peak resident set of two runs of one binary (800,000 and 1,600,000 keys) with
The test now measures bytes per additional key over a longer lever at the same cost (400,000 against 2,000,000 keys, the collection cadence fixed): the bound is 32 bytes per key, which is 50 MB over the 1,600,000 additional keys, where the levels account for at most 18 MB (12 bytes per key) and the leak above for 1.3 GB. It also runs the leaking pair (50,000 and 150,000 keys, collector off: 964 to 970 bytes per key measured) and requires more than ten times the bound from it, so the measure is shown to see a leak. The changed test: 20 of 20 pass (5 on this branch, 5 on train pi-2, 10 on plain main; two Sandboxes, two runs at once). The fixture's pairs directly: 20 pairs with eight processes at once differ by at most 0.6 MB, 24 pairs with twenty-four at once by at most 1.7 MB (0 and 1 bytes per key). |
The PR's CI skips the island conformance lane (it touches no island path), but main runs it on every push, and three island builds change with this branch's lowering fixes: - npm/types-boundary (island:rowparse) and npm/zod3-boundary (island:zod) were refused at build time with SC1090; both build and match Node now, so their entries leave the list. - npm/zod4-boundary (island:zod) loses one of its two refusals: "SC1090,SC8010" becomes "SC8010". Regenerated by the lane's update mode over the whole lane on this tree (419 programs ran); nothing else changes. The three and their neighbours pass again without the update mode. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
New head
Checked on this tree in a Sandbox before the push: the tsc build; The zero-cost gate on the head before this one ( |
…rain/pi-1 expressions/binary.ts, the import block: main reformats the class-to-primitive import beside the generic-functions import from which #3101 removes the two identity-pin names. Main's first import and #3101's one-name second import stand. ir/codec.generated.ts is regenerated from the merged ir.ts. Co-Authored-By: Claude Code <noreply@anthropic.com>
One entry of startup-1's changes with this branch beside it: npm/hono-zod-validator with the validator in an island stopped at the refusal of Headers iteration in a static build (SC2020). #3274 compiles that iteration, so the program goes on and stops later, at a TypeError of the Headers constructor for its first argument. Still exit 1 where Node exits 0, still loud; the entry now names the new stop. This branch's own three entries (npm/types-boundary, npm/zod3-boundary, npm/zod4-boundary) are as in the previous regeneration. Update mode over the whole lane on this tree (427 programs ran); the entries that moved pass in check mode. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
New head
|
|
Native bootstraps on this head ( CI on this head so far: every Linux job is green. One red job, "test (macOS arm64 LLVM artifacts, no clang, 4/5)": The PR conflicts with main since islands-2 landed, in one test file ( |
|
Hold: please do not merge this PR. Several of its test programs are being replaced with simpler ones that pin the same compiler behaviour; the train returns as a new PR, and this one is closed as superseded then. 🤖 Generated with Claude Code |
One rollup of the lowering fixes on pi's path that were open as separate PRs. Each member is merged at its PR head with its own signed commits; nothing was rebased. The members stay open until this merges.
Members
f021e5605e,f9003c8c6a: routes by properties exist only among arms that keep each property at its own type). Owner's proof: gate passes with no allow list on the fixed head (5,314 identical), both lanes and both bootstraps pass on the first head.Not here: #3338 and #3179 were on the seed list and merged to main by themselves before the branch was cut.
What the train itself changes
planUnionRetagtakes Compile three union conversions pi needs: Extract member patterns, partial discriminants, asserted subtypes #3179's site routes and Choose a union arm by the value's properties where several arms hold a record of optional members #3339's routes by properties, in that order: an arm no rule places takes its site route first. This is the resolution Choose a union arm by the value's properties where several arms hold a record of optional members #3339's owner prescribed.cp.shellLine(Compile three pi fences: a callable member of a record union, a generic rest binding, and spawnSync shell arguments #3180) andstream.stdioReadable(Answer a process stream's one runtime stream where a stream class is declared #3279) get their rows inNEVER_THROW_LIB_FNS: main's builtin table guard (Hold every list of builtin names to the builtin table #3227) landed after both PRs were cut. Neither runtime function raises an error or runs program code.ir/codec.generated.tsis regenerated from the mergedir.ts.Readablethatprocess.stdinconverts to, so"aéb€c😀"arrived as61c3a962ac63f0800a. In theSCR_STDIO_EXTENDEDbuild only; pinned by a scripted stdin case with reads that end inside characters.lowerVarDecl: Compile six pi shapes: Headers iteration, optional Promise.all entries, dynamic reduce, JSON-union enumeration, a wider ?? default, a forward-captured let #3274'suninitializedLetSlotmeets main's Read a let or a field as undefined before its assignment #3169, which widens a binding a nested function can read before its first store. The helper stays and takes the widening as an opt-in argument that only the declaration passes.scr_events.conce per flavor and drops theSCR_STDIO_EXTENDEDbuild. Refuse bytes consumers of process.stdin by name once an encoding is set #3296's checks and the stdin fix above were written for that build: on this branch they are unconditional now, on main's one-call decoder, and every program that links the stdin delivery path carries them (the gate's rows are in a comment below).array-callback-rest-parameters.tsrefuses a rest-parameter callback passed to an array method overObject.valuesof a dictionary. Compile six pi shapes: Headers iteration, optional Promise.all entries, dynamic reduce, JSON-union enumeration, a wider ?? default, a forward-captured let #3274 lowersreduceandreduceRightover a checked-dynamic array through the dynamic call, which passes JavaScript's four arguments, so those two lines compile here and match Node. Their two diagnostics left the snapshot, and the lines are the corpus programreduce-rest-callback-dictionary-values.ts.scr_child_shell.c(Compile three pi fences: a callable member of a record union, a generic rest binding, and spawnSync shell arguments #3180) names the dyn-async unit's warning functions; main's runtime unit reference check (Link the http unit with node:http2 session and stream types #3321) wants that said. Onlycp.shellLinereaches them and that call selects the unit, so the reference is listed as known, asscr_pki.chas it.union-shared-discriminant-ambiguous.tscompiles once Store union members that share a discriminant literal as one record (pi's tool calls) #3311 is beside it: its two arms per discriminant value are one record now, so each value has one owner. The program matches Node (run with more values,in,Object.keysand an explicitundefinedmember), so it moves to the corpus asunion-shared-discriminant-merged-owners.ts, and the fixture keeps the SC2003 refusal pinned with two owners that give one member different types.packages/runtime/refusal-sites.json, the baseline of main's refusal lint (Name a diagnostic code on every runtime refusal #3137), is regenerated on the merged sources: it lists this branch's two stdin refusals, and whatever refusal sites main's own merges left unlisted at the time (after Fix main: island objects as weak keys after #3135, and the runtime refusal baseline after #3137 #3371, one: node:events: symbol event names and the max-listeners warning's fields #2766's symbol event name).lower-stmts.ts,divergences.ts,lowerer.ts,type-mapper.ts,program-facts.ts, and the limitations partialconsole-and-process-streams.mdx, where main's Project process streams into records of methods #3102 and Answer a process stream's one runtime stream where a stream class is declared #3279 add sentences at one place), and one function Compile six pi shapes: Headers iteration, optional Promise.all entries, dynamic reduce, JSON-union enumeration, a wider ?? default, a forward-captured let #3274 deletes that main had edited (lower-stdlib.ts).Validation of this tree
All in Vercel Sandboxes.
33653725e2(the members on main875ed8ec56), before the two test commits and the later merges of main:packages/runtime/test/regex.test.ts(Remove four causes of lane test failures: a stray packages/node_modules, serial fixture builds, vendor build budgets #3299's known flake); alone on the final tree it passes.dynamic-properties(+30.7% [+1.0%, +46.8%]), has an executable byte-identical to the base's and bimodal samples on both sides: noise.--gate-complete:3096f0e60fagainst main23f1b176f3: 5,330 programs identical, 119 fail to build on both sides (Node island programs; the gate has no libnode pack), 16 differ and are listed, nothing else differs. Compile time: benchmarks +0.39% [95% CI -0.73%, +1.26%], self-compile -0.21% [-1.28%, +1.89%], fixtures +0.21% [-1.10%, +1.51%], all within tolerance; the candidate compiler builds its own source.7977ad7fe4against main8f47512e31, the first round with main's new gate tool (Train ci-tests 1: lane flake fixes, CI shard balance and Effect sample, gate and CLI tooling, lifted-function index #3349), which also compares a program the change adds when the base compiler can build it: 5,331 identical, 119 fail on both sides, the same 16 listed, 24 added programs the base cannot build, and 13 added programs the base builds differently (below). Compile time: benchmarks -0.07% [-1.30%, +1.21%], self-compile -0.35% [-1.47%, +0.84%], fixtures +0.09% [-0.80%, +0.97%], all within tolerance; the compiler's own source costs 147 bytes of memory per source byte at the end of a lowering pass against a budget of 146 (+0.3%, reported, not failed).8954-extract-member-pattern-generic.tsgrew 2.17%), the second on the 13 added programs, which were not on the list yet.48c8bc942bagainst mainef8d82ef7f, with all 29 listed: PASS. 5,357 identical, 119 fail on both sides, 29 listed, 24 added programs the base cannot build, nothing else. Compile time: benchmarks +0.28% [-0.40%, +0.86%], self-compile -0.04% [-2.01%, +0.95%], fixtures +1.20% [+0.93%, +1.46%], within tolerance. The rounds on later heads are in the comments below: on473616d7dbagainst main246041da22the gate passes with 157 listed (the 30 of this section and the 127 that main's Build scr_events.c once per flavor #2801 adds, next item), and compile time is within tolerance on all three measures.1950-generic-fn-values.ts,1955-generic-fn-value-erasure.ts,2020-generic-value-bindings.ts,2551-generics-value-aliases.ts,2794-array-generic-callback-values.ts,generic-erased-value-tdz.ts,generic-identity-rewritten-pins.ts,generic-identity-unknown-scalars.ts.union-spread-distribution.ts(-0.16%),7160-json-parse-deep-unions.ts(+0.28%), and Compile three union conversions pi needs: Extract member patterns, partial discriminants, asserted subtypes #3179's two programs, which reached main after Store union members that share a discriminant literal as one record (pi's tool calls) #3311 was gated:8957-union-partial-discriminant-routes.ts(-0.29%) and8954-extract-member-pattern-generic.ts(+2.17%, 5,352 bytes).checked-dictionary-member-append.ts(-0.02%).SCR_STDIO_EXTENDEDbuild of the stdin unit (+0.06% to +0.12%; its recorded exception):6236-stdin-encoding-tty-probes,child-stream-set-encoding-untyped-listener.cjs,evolving-let-child-process.cjs.union-class-unmerged-member.ts(+17.70%, 74,632 bytes of class records in a 421 KB program),union-class-record-identity.ts(+2.46%),union-shared-literal-classes.ts(-1.52%).8932-proxy-record-bindings.ts(-0.45%),8933-proxy-record-export/main.ts(-0.92%).dictionary-spelled-twice-spellings.ts(-5.76%),dictionary-spelled-twice-modules/main.ts(-3.67%),dictionary-spelled-twice-distinct.ts(-1.04%),dictionary-spelled-twice-value-types.ts(+4.85%; it now links the dynamic object model, because a dictionary that keeps identity is a checked-dynamic view).identity-view-union-neighbours.ts(-5.83%). Fix a crash converting a dictionary record that is a union arm to an untyped value #3314:union-dictionary-arm-untyped.ts(one LLVM line). Choose a union arm by the value's properties where several arms hold a record of optional members #3339:asserted-method-param-union-arm.ts(-50.74%: the asserted entry takes the native method adapter). Compile nested generic function declarations and root generic function identity #3101:native-worker-generic-identity-objects.ts(+0.02%).pnpm node-compat:check,pnpm lint,pnpm format:check,pnpm test:ts7 --baselines-only, the guard and unit tests (the builtin table, runtime unit references, executable cache rules, program facts, the peel guard, the unit journal, the lifted-function index, the whole diagnostics corpus), the piped-stdin suite, and the members' corpus programs, plain. The current head has main through1dc939974b; on it the same list ran again with CI's policy contracts, the generated AST check and the weak-key tests of main's Fix main: island objects as weak keys after #3135, and the runtime refusal baseline after #3137 #3371.pi
Unedited pi (
e770f5efe), no forced islands, each build alone on one 32-vCPU Sandbox (Intel Xeon 2.90GHz):875ed8ec5633653725e2)Ten positions leave:
ai/src/providers/all.ts:141(#3339),ai/src/utils/headers.ts:5(#3274),mcp/resources.ts:54and:55andmcp/runtime.ts:449(#3274),external-editor.ts:28andutils/child-process.ts:35(#3180),rpc-mode.ts:91and:138(#3101), andtheme.ts:653:39, where #3167's refusal gives way to the next one on the same line (:653:52, the one new position). pi still stops at the end of lowering on this tree: the other 26 positions belong to other areas (computedimport()specifiers,Object.assignforms, library functions as values,FormData,WeakRef, Worker entry points, aWebSocketsubclass) and to the two refusals the project keeps. A pi binary and its suite therefore come from the integration tree, not from this PR.Not run
23f1b176f3,8f47512e31,04b7d6b0bb,ef8d82ef7f); the focused runs above cover what those touched.tests/harness/coverage.test.ts's whole-corpus case ran in the full round's lanes; on the later head, alone on an 8-vCPU Sandbox, it ran into its 600 s limit (a timeout, no failed assertion) and was not repeated on a larger machine.🤖 Generated with Claude Code