Commit Graph
3982 Commits
Author SHA1 Message Date
Malte UblandClaude Opus 5.5 f09e02255d Guard the X509 dynamic-value fence against recursive types
refuseX509DynValues walks a converted value's type looking for an
X509Certificate record, and it memoized only records (by shape id) and
unions (by union id). A type that reaches itself through an array, set,
map or promise has no id to memoize by, so `type Rec = string | Rec[]`
in a program that also holds a certificate recursed until the compiler
ran out of stack — a crash, not a diagnostic.

The walk now keeps an identity-based visited set over those kinds: a type
met again while it is still being visited answers false, the least fixed
point of reachability, since nothing outside the cycle makes a
certificate reachable. Unions keep their memo, which now also covers a
union cycle that names one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 05:38:06 -07:00
Chris Tate 9bb4692959 Convert more structural values: class views, promise arms, index-signature records, tuple unions (#2442) 2026-10-27 21:29:07 -05:00
Chris Tate 37bdea8162 Pin constrained generic members and probe through assertions (#2443) 2026-10-27 21:27:21 -05:00
Chris Tate d4d7995d68 Treat a call that never returns as an exit in presence proofs (#2445) 2026-10-27 21:17:38 -05:00
Chris Tate b1e3c46976 Convert unions with typed array, date and regexp arms to strings
String(), template literals and `+` with a string refused a union with an
object arm other than a program class or URL (SC1090). classUnionToString
now also takes typed array, Date and RegExp arms and converts each arm as
ensureString converts it alone: a typed array through its join (a Buffer
through its own toString), a date through toString, a regular expression
through its source and flags.

The npm any-chaining fixture compiles with this PR's index-signature
record conversions and then reached String() of a `URL | Uint8Array`
option in its package. It now matches Node, and its KNOWN entry is
removed.

Adds corpus program union-object-arms-to-string.ts, its corpus cost and
its preflight/order baseline.
2026-10-27 20:49:51 -05:00
Chris Tate 6758fabe80 Evaluate computed in receivers and read getter arms in asserted probes
A statically decided `in` over a union of fixed record shapes treated
every field read as a pure receiver, so `"k" in (make().stored as R)`
folded to a constant without calling make(), and the several-arm answer
re-read the receiver once per tag test. A field read is now pure only
over a pure object (isSafeToRepeat). Any other receiver rides one tag test
when the answer is constant, and the several-arm answer evaluates it once
into a hidden local instead of refusing.

A presence probe through an assertion (`typeof (u as R).member`) answered
an arm with a getter of that name as if it lacked the member: undefined,
without running the getter. The per-arm read now calls the getter on that
arm, as Node's read does, and an arm with only a setter keeps the
assertion's checked extraction.

asserted-arm-probe.ts covers both: a receiver whose call is counted, and a
getter arm beside a data arm.
2026-10-27 20:44:33 -05:00
Chris Tate a0494cdfa3 Keep optional-chain calls out of presence exits
A call whose checker type is never exits the statement only when the call
is made. In an optional chain (`maybe?.fail()`, `this.helper?.fail()`,
`fail?.()`), a missing receiver or callee ends the chain without making
the call, and the checker still types the call never when the chain's
base has a non-nullable declared type, as an array element read does.
Such a statement completes, so a read after it is not proven present.

exitsAbruptly now skips a call whose callee chain uses `?.`.
2026-10-27 20:38:14 -05:00
Chris Tate 1856713700 Represent unions of classes as the instance pointer itself
A union of two or more class arms and at most two unit arms (`A | B`,
`A | B | undefined`, sum types over classes) now holds the instance
pointer instead of a reference-counted %ScrUnion box, as unions of
records already do. NULL and the null sentinel encode the unit arms.

The arm of a value is the arm whose preorder interval holds the
instance's class id. On 64-bit targets, hierarchy members already keep
their class id in the high half of the rc word, or in the 32-bit header
of ProgramLifetime classes. A standalone class that is an arm of such a
union now keeps its id the same way and counts in the low half; the
count reaches the immortal all-ones value instead of wrapping, as for
hierarchy members.

Each union has a tag helper that narrowing checks inline. Unions over
the same classes share one retain, release and trace: retain and
release use the shared 32-bit count, and only a release of the last
reference dispatches on the arm to that class's release. Unions over
ProgramLifetime classes retain and release nothing. When one arm
carries a collector header, every class of every arm's hierarchy does.

Arms may not extend one another, and must all be compact or all keep
the rc word, and all be ProgramLifetime classes or none. 32-bit
targets, library builds, lifetime-region programs, debug builds, and
unions used as Map keys or Set elements keep boxes.
2026-10-27 20:30:01 -05:00
Chris Tate a4a844f446 Revert "Keep frozen checked records frozen, and give sloppy-mode this the global object" (#2444) 2026-10-27 20:08:07 -05:00
Chris Tate e00c08d4af Treat a call that never returns as an exit in presence proofs
presentAtUse proves that a binding read cannot be undefined when an
earlier `if` leaves only for an absent value, as in
`if (x === undefined) return;` or `throw`. A branch that calls a function
declared to return `never`, such as `if (x === undefined) fail();` or
`this.fail()`, also never completes. It did not count, so the read stayed
possibly undefined.

A function returning such a read as `string` was then taken to return
undefined too. A string[] it filled counted as holding undefined
elements, which applies to every string[] in the program, so an element
of another string[] passed to process.stdout.write failed to compile
(SC2020, "process.stdout.write of non-string data").

exitsAbruptly now counts an expression statement whose call has type
`never`. The lowerer answers that from the checker
(PresenceBindings.neverReturns).
2026-10-27 20:06:19 -05:00
Chris Tate b16c739d34 Classify zero-cost gate differences instead of failing every runtime change (#2271) 2026-10-27 19:27:25 -05:00
Chris Tate 919b79b62a Revert "Keep frozen checked records frozen, and give sloppy-mode this the global object" 2026-10-27 19:20:37 -05:00
Chris Tate dfb7afcdd7 Load Node.js in-process on first use: libnode bridge and runtime loader (#1685) 2026-10-27 18:48:02 -05:00
Malte UblandClaude Opus 5.5 4c2adcf123 Convert a union of tuples into an array slot
A value typed as a union of tuples or arrays, such as the `as const`
key-table read `SEQUENCES[functionKey]` in pi, was refused with SC2003
when it flowed into a `string[]` or `readonly string[]` slot. A single
tuple already converted through the width family's tuple-to-array
rebuild; a union of tuples had no plan at all.

widthLiftPlan gains the unionArr step for an array destination: every
non-unit arm must lift on its own tag (a tuple rebuilds through
tupleArr, an array copies or lifts through arr), and a unit arm takes
the stranded TypeError, since the slot has no representation for it.
%union.arr dispatches on the runtime tag and applies each arm's lift;
applyWidthLift and one coerceToExpected hook wire it into the top-level
edges that the nested slots already reach through the plan.

The step is planned only after the union-into-one-of-its-arms rule and
only when no arm of the source is already the destination, so every
union that converted before - `T[] | undefined` into `T[]`, optional
array fields, `??=` - keeps byte-identical IR; only sources with no
conversion at all gain one.

Adds the differential corpus program, its cost file, an IR unit test,
and the order-parity baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:44:46 -07:00
Chris Tate e941d5d659 Run JavaScript WritableStream, TransformStream and pipes natively (#2437) 2026-10-27 18:43:35 -05:00
Chris Tate ccd87f44fc Keep the scr_map and three-library tests inside their timeouts (#2441) 2026-10-27 18:35:52 -05:00
Chris Tate ea8c1e6a34 Keep frozen checked records frozen, and give sloppy-mode this the global object (#2436) 2026-10-27 18:33:05 -05:00
Chris Tate e6a26e8e40 Keep the borrowed dispatch path through the inherited-method fallback (#2280) 2026-10-27 18:09:26 -05:00
Chris Tate 08fbecb7e7 Excuse only reproduced differences as nondeterministic, and read vendor archive symbols
A program is nondeterministic only when every artifact that differs also
differs between two baseline builds; a deterministic difference in another
artifact still fails the gate. The runtime-only check now reads the symbols
of changed vendor archives as well as runtime objects.
2026-10-27 17:55:44 -05:00
Chris Tate 90471a180d Read a proxy's integrity from its target, and give sloppy this the program's global object
Records checked from a proxy in a program that freezes untyped values no
longer fail on the proxy's integrity query: a proxy is frozen exactly when
its target is. A sloppy-mode function called with undefined or null
receives the global object that globalThis reads, so its builtin members
such as console answer before anything else reads the global object. The
new corpus programs are named instead of numbered and record their CI
costs.
2026-10-27 17:54:43 -05:00
Chris Tate 4e98e5e9bd Call web stream methods past own overrides, and await a transformer's thenable start
A method value of a web stream class, such as a saved
WritableStream.prototype.close, called on an instance whose own property of
the same name wraps it, ran the own property again, so the wrapper recursed.
The classes' method values now call the class's method directly
(ScrDynHandleClass.method_invoke); member calls still look at own properties
first.

TransformStream resolved its start promise with a transformer start's
result without adopting a thenable, so both sides started at once and a
rejection was lost. The start promise now resolves as a promise's resolve
function does (scr_dyn_promise_resolve_with): a thenable adopts through its
resolve job, a throwing `then` read rejects, and the stream starts or errors
when it settles, in Node's order.

Lowerer.namesWebStreamClass, a cache made when first needed, is listed in
Lowerer.unjournaled.
2026-10-27 17:53:47 -05:00
Malte UblandClaude Opus 5.5 458fe7d54a Give the three-library release and sanitized cases 1200s timeouts
The test compiles the three npm package twice, dev and release, and both
cases shared one 600s budget through test.each. Release compiles the whole
library into one LLVM module that scriptc-llvm-codegen optimizes and
codegens in a single process on a single core: 426s of the 477s total run
in the native-archive phase alone. Across sandbox lanes release measured
331, 335, 339, 345, 348, 350, 358, 428, 450, 511, 521 and 581s, so whole-
box speed varies about 1.7x and the slowest lanes sat within 20s of the
budget; the sanitized dev case hit 600s twice. There is no regression, the
work is fixed and single-threaded.

Generate the two cases in a loop so each gets its own timeout: 1200_000ms
for release and for dev under SCRIPTC_SAN, and the unchanged 600_000ms else.
Test names are byte-identical to the .each %s expansion, so
skipUnownedTest keeps assigning each case to the same shard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:27:08 -07:00
Malte UblandClaude Opus 5.5 3a0bef8db7 Answer member presence probes off the value an assertion actually holds
Node erases a type assertion, so `typeof (stored as R).member` reads the
property off whichever value is stored: when `stored` is
`R | Record<K, R>` and the record arm is holding, the read answers
undefined and the probe is false. Two earlier decisions made this throw
instead. lowerAsExpression applied the checked union extraction for
`stored as R` eagerly, trapping on exactly the arm the probe exists to
detect; and lowerFieldRead answered the read from the ASSERTED checker
type, which also folded `typeof` to a constant.

Defer the assertion's extraction for a member PRESENCE PROBE - the
operand of `typeof`, or an operand of `===`/`!==`/`==`/`!=` against
undefined, null or `void 0` - when the stored union's record arms declare
the member unevenly. The read then answers per arm off the runtime tag
(assertedMemberRead), each declaring arm reading its slot and the rest
answering undefined; a read with no per-arm answer still applies the
extraction the assertion would have taken. `"member" in (stored as R)`
over the same shape likewise probes the stored value.

Every real use - the member called, compared by identity, read into a
typed slot - keeps the checked extraction, so a lying assertion still
throws at its use. Types don't lie; a probe simply isn't the use.

Corpus program asserted-arm-probe.ts covers typeof, undefined and
nullish probes on the optional and required shapes, `in` on the required
shape, a probe guarding the real call, and a real use that throws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:22:07 -07:00
Malte UblandClaude Opus 5.5 ec973e7221 Pin generic method members constrained by the enclosing type parameter
An interface's generic method whose own type parameter is constrained by
the enclosing type's parameter (run<T extends TApi>(item: Item<T>)),
implemented by a plain contextually typed lambda, compiles since #2274:
the lambda takes the contextual signature's type parameters and lowers
once, erased. The merged corpus only covers unconstrained contextual
generics, so this program pins the constrained shape with a conditional
member on the argument type, explicit and inferred call type arguments,
the class-body form, and record-held receivers.

Verified against the 2026-10-23 base checkout, where the same shape
refused with SC2009 (Item<T>'s member 'api' has type 'T'), and the pi
models.ts:1159 Model<T> 'compat' shape refused the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:22:06 -07:00
Malte UblandClaude Opus 5.5 0f54ec7e6c Route index-signature record arms through the overflow capture reshape
widthLiftPlan had no arm for a record into a record whose destination
declares an index signature, so every nested position that plans through
it (a union arm, a field, an array element) refused the pair with SC2003
even though widthCoerce already compiles the same pair at top level.

Split lowerRecordOvfCaptureHelper into a pure recordOvfCapturePlan (the
same gates, no interning, no observable state, recursion-guarded like
recordWidthPlan) and the existing builder, return { how: "ovf" } from the
width relation for those pairs, and build the capture call in
applyWidthLift. The destination is still a fresh copy, the stance every
width conversion already takes, and no pair that compiled before changes
shape, since recordWidthPlan returns null for index-signature targets.

Adds corpus program record-ovf-union-arm.ts (a tagged index-signature
record narrowing into an index-signature union arm), its corpus cost and
its preflight/order baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:10:14 -07:00
Malte UblandClaude Opus 5.5 a5f2be64a9 Widen a union's promise arm when its payload widens
A union-to-union conversion refused a source arm whose promise payload
widens (`Promise<void> | undefined` into `Promise<unknown> | undefined`)
with SC2003, although TypeScript accepts the assignment and Node runs
it. planUnionRetag found no route: widthLiftPlan knew the copy-reshape
family but had no promise pair, and its union branch did not list
promise as a same-family arm. The arm-level conversion already existed
(coerceToExpected's checked payload widening, promisePayloadWidens); it
was just not part of the width relation.

A promise pair now plans as promiseWiden and builds through
coerceToExpected, so planUnionRetag's unique-arm route narrows the arm
and converts its payload before wrapping into the destination tag. The
promise keeps its identity and settlement turn; the destination promise
arm must still be unique (two promise arms keep the fence), and a
payload outside the dyn domain (a module namespace) keeps it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:09:52 -07:00
Malte UblandClaude Opus 5.5 1064dca751 Keep a live view for structural types a class implements by methods
A class instance stored in a slot typed as an anonymous structural object
type failed with SC2003 (in a union) or SC2002 (a plain slot). The type is
marked by the identity-flow scan, but needsView dropped the mark when
nothing compared the value by identity: pi's
`{ handleInput?(data: string): void } | undefined` field is only ever
called through. The type then mapped to a record, and the record copy
cannot carry a prototype method. The same dropped mark also cost named
interfaces with an OPTIONAL method, whose type is `T | undefined` and so
has no call signature where protocol() looks.

A structural type that exposes a program class method can never be a
record copy: the copy would lose the method, its receiver and subclass
overrides. needsView now keeps the mark for such targets, reusing
methodViewSources. The mark exists only because an instance already flows
into the type, so programs without that flow keep their representation.

The new corpus program covers pi's optional-method union, a required
method in a plain slot, a subclass override, identity, and the data-only
width-copy path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:09:42 -07:00
Chris Tate 6e5f21c580 Reuse keep-alive connections across fetch() requests (#2379) 2026-10-27 16:45:35 -05:00
Chris Tate 1a86f94239 Merge main 2026-10-27 16:45:14 -05:00
Chris Tate 38bc4ed67b Pad ASCII strings without indexing their UTF-16 units (#2392) 2026-10-27 16:44:40 -05:00
Chris Tate f945efecee Run JavaScript WritableStream, TransformStream and pipes natively
In JavaScript sources, WritableStream with its default writer and
controller, TransformStream with its controller, pipeTo and pipeThrough,
and TextEncoderStream and TextDecoderStream join the native web stream
classes, following Node 24's webstreams step by step: sinks and
transformers, backpressure and the writers' ready and closed promises,
abort with the controller's signal, the pipe's shared write requests and
its abort signal, and UTF-8 encoding and decoding across chunks settle in
Node's order. The streams take own properties, which a pipe does not use
for its own reads and writes, and the global object answers the classes
by computed name in programs that name them.

A JavaScript class that extends one of these classes keeps the SC1090
refusal, and TextDecoderStream refuses encodings other than UTF-8.

The node24-web-stream lifecycle, errors, queues, transforms, pipes,
pipe-errors, pipe-abort, tee and encoding cases now match Node.
2026-10-27 15:55:31 -05:00
Chris Tate 62d4cecd3a Keep frozen checked records frozen, and give sloppy-mode this the global object 2026-10-27 15:22:08 -05:00
Chris Tate c5aab2833b Use an exact integer loop limit as the counted loop's endpoint (#2432) 2026-10-27 14:51:49 -05:00
Chris Tate 3c7f47e6aa Lower function values with extra optional parameters into narrower slots (#2433) 2026-10-27 14:31:51 -05:00
Chris Tate cc6ee85fd7 Run JavaScript web streams on a native ReadableStream (#2411) 2026-10-27 14:20:02 -05:00
Chris Tate a75cff7ab4 Use an exact integer loop limit as the counted loop's endpoint
A counted for loop converts its limit to an i64 endpoint. A limit that is
not a literal went through the double every time: converted to double,
clamped to 2^53, rounded with llvm.floor or llvm.ceil and converted back.
At the default target CPU the rounding is a libm call, which a loop such as
`for (let i = values.length - 1; i > at; i--)` paid on every entry even
when `at` was an int32 local. A limit that is an exact integer (an int32
local, an integer view, or a proven range) is its own floor and ceiling and
within ±2^53, so it is now the endpoint directly; other limits keep the
double path.
2026-10-27 13:49:48 -05:00
Chris Tate 349feb5e47 Capture stack traces with Error.captureStackTrace as Node does (#1809) 2026-10-27 13:49:06 -05:00
Chris Tate 1feed2a3ec Answer cross-layout record casts with views of the original record (#2419) 2026-10-27 13:47:06 -05:00
Chris Tate 2f636fa93d Keep class instances that stay in their frame in frame storage (#2426) 2026-10-27 13:46:18 -05:00
Malte UblandClaude Opus 5.5 76d08ef73e Lower function values with extra optional parameters into narrower slots
A function value with more parameters than the slot it flows into is
assignable in TypeScript exactly when the extra trailing parameters are
optional, and a call through the slot passes no value for them, so the
callee sees undefined as it does in JavaScript. funcCoerceAdapter refused
every such pair, so a callback parameter forwarded into a narrower slot
fell through to requireExactShape and failed with SC2003 ("union
conversion has no unambiguous layout mapping") even though the checker
accepted the program.

The adapter now supplies each omitted extra parameter with omittedArgFor
(the undefined arm of the parameter's ABI union, or the checked-dynamic
undefined). A pair whose extra parameter cannot hold undefined stays
refused, and an extra required parameter remains a checker error.

Only pairs that previously returned null reach the new code, so programs
that compiled before are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:45:13 -07:00
Chris Tate 4bd4c43efd Check Uint8Array chunks in clones of JavaScript ReadableStream bodies
Response.clone and Request.clone tee the body. The tee branches of a body
read from a JavaScript ReadableStream did not keep that body's chunk check,
so consuming the original or the clone took string chunks as text. The
check is now a flag on the body's stream that its tee branches inherit, and
consuming either branch rejects with Node's TypeError "Received
non-Uint8Array chunk".
2026-10-27 13:28:33 -05:00
Chris Tate a897079be1 Capture onto the target evaluated before a constructorOpt that reassigns it
When Error.captureStackTrace evaluates a JavaScript constructorOpt (a getter,
a call, or this.constructor with a constructor getter), V8 has already
evaluated the target. The lowering kept the target in a local only when its
expression was not a plain read, so a constructorOpt that assigned the target
variable made the capture install the stack on the new value. The target is
now always kept in a local before the constructorOpt runs.
2026-10-27 13:00:48 -05:00
Chris Tate 9bdb36ed46 Report published values as frozen, sealed and not extensible
Under Node.js, publish() deep-freezes everything it reaches, so
Object.isFrozen and Object.isSealed return true and Object.isExtensible
returns false for a published object, array, Map or Set. Native builds
already refused writes into published values, but answered these queries
as for an unpublished value: publication marks an object immortal and sets
no frozen bit.

- record.isFrozen, array.isFrozen and the extensibility test of Maps and
  Sets also read an immortal rc word as published, the test of the
  published write guards, for the types the program may publish
  (PublishedTypes). Other types emit the code they did.
- scr_dyn_is_frozen, scr_dyn_is_sealed and scr_dyn_is_extensible answer
  for a published object, array, Map or Set, or a checked view of one,
  before materializing the view.
2026-10-27 12:57:41 -05:00
Chris Tate 82cf32d1cd Convert number | undefined timer delays with ToNumber (#2427) 2026-10-27 12:42:02 -05:00
Chris Tate 3b330b850a Release a frame object's nullable class field as the class pointer it holds
A nullable class field (Tag | null) stores the class pointer itself, whatever form the union takes elsewhere. When the same union was also a Set element or a Map key, it was a boxed union outside class fields, and the frame's cleanup of a frame class released the field's raw pointer with scr_union_release. classReferenceFields now gives each field its storage type (NullableRefFields.storageType), as the heap teardown does, so the scope exit calls the class's release.
2026-10-27 12:21:27 -05:00
Chris Tate 5431a8c142 Keep class instances that stay in their frame in frame storage
A class instance could live in its function's frame only when its class
had scalar fields only and CallLifetimes proved the binding was only read.
Per-call state objects whose fields own arrays, maps and other objects,
and whose methods and helpers store into them, stayed on the heap.

frame-escapes.ts proves which locals never leave their frame: every use
keeps the value in the frame (region-ir.ts keepsInFrame), a const binding
of it in the same function shares its fate, and a closure that captures it
stays in the frame and keeps the captured binding there. Parameter and
capture facts are solved optimistically over the call graph with a
worklist; async functions, generators and module code get none.

A frame class is a standalone class whose constructor does not let this
escape. Its instance now lives in frame storage when it is bound to such a
local, when it is passed in place to a parameter that stays in the
callee's frame, or when closures that stay in the frame capture its
binding. The frame owns the reference fields: they start empty, union and
dyn fields start at undefined, and the binding's scope (or the call's
statement frame) releases them on every exit. A cycle-capable class keeps
its collector header in front of the object, never a candidate.
2026-10-27 11:56:10 -05:00
Chris Tate a3ae97c097 Specialize calls that omit a constant default and drop unreached originals (#2423) 2026-10-27 11:37:19 -05:00
Chris Tate 096bf9a364 Map a failing type exactly from its canonical form where localized failure would degrade it (#2338) 2026-10-27 11:32:18 -05:00
Malte UblandClaude Opus 5.5 cdd121405b Convert number | undefined timer delays with ToNumber
setTimeout and setInterval passed their delay to the F64 timer slot
unconverted, so a delay typed number | undefined failed with SC2003. Such
a delay is an optional `ms?: number` passed on, or a number[] element
read, whose storage keeps the out-of-range undefined. Node converts the
delay with ToNumber, so undefined is NaN, the omitted delay. A union
delay now goes through lowerOptionalNumber, as Math arguments already
do. Other unions still reach the lib boundary's refusal, and number
delays are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:11:51 -07:00
Chris Tate fa65a3b385 Specialize calls that omit a constant default and drop unreached originals 2026-10-27 10:49:22 -05:00