Commit Graph
6750 Commits
Author SHA1 Message Date
Malte Ubl 8e65aa86e9 Merge branch 'ni/i9-recorded-deferral' into ni/i10-js-island-requires 2026-10-28 09:38:32 -07:00
Malte Ubl d5dab9243b Merge branch 'ni/i8-exit' into ni/i9-recorded-deferral 2026-10-28 09:38:28 -07:00
Malte Ubl 54ca2938f5 Merge branch 'ni/i7-routing' into ni/i8-exit 2026-10-28 09:38:27 -07:00
Malte Ubl 00a3ab3ee0 Merge branch 'ni/i6-subclasses' into ni/i7-routing 2026-10-28 09:38:27 -07:00
Malte UblandClaude Opus 5.5 a989612ce7 Record the island case's unresolved import() fence as a divergence
The stale i6 branch marked lazy-traps island-only, which held while the
whole package was refused and ran in a Node island. main compiles the
package statically with a throwing stub per blocked edge, so its require()
edges still match Node through an island while an import() whose literal
specifier resolves nowhere is a deferred SC2012 fence. Record that
remaining gap the way main does instead of claiming Node parity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:38:22 -07:00
Malte UblandClaude Opus 5.5 ac2323fb2a Retire the spread diagnostics fixtures their corpus programs replace
`tests/diagnostics/spread-fences.ts` pinned the conditional-spread fence
(`...(c ? { x: 1, y: 2 } : { x: 3, y: 4 })`) and
`spread-runtime-optional.ts` pinned the `{ ...value }` spread over a
callback's runtime-optional record. Both shapes compile now, so neither
file can stay in a suite whose contract is that every program in it fails;
the differential corpus owns them instead (`8501-object-spread-optional-source`,
`8503-object-spread-conditional-shapes`), where the programs run under Node
and as a compiled binary rather than pinning a refusal.

The rest of `spread-fences.ts` already compiled before this branch, and the
fences it named that remain (an element-type mismatch, a non-tuple call
spread) are ordinary type errors, so the file goes with the fence it pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:13 -07:00
Malte UblandClaude Opus 5.5 c86b3a153b Keep the new lowering paths off already-typed code
The conditional-spread, asserted-probe, and asserted-element-probe commits
each answered a shape that previously refused, but three paths also
re-lowered code that already compiled:

- A checked-dynamic literal's conditional source is now lowered arm by arm
  only when the literal is built for a declared record target, so a literal
  already built as a plain dynamic object keeps its established lowering.
- An asserted member probe counts arms the way the extraction threshold
  always has, plus getter-backed arms beside an arm with no read at all: a
  union whose arms all declare the member keeps the assertion's extraction.
- An asserted element probe checks the stored value's checker type before
  speculatively lowering the receiver, so a non-union receiver leaves no
  synthesized helpers behind.

The zero-cost gate reports no difference for corpus/7020-conditional-
spread-fields.ts, corpus/6474-index-record-spread-literals.ts,
corpus/6475-dynamic-spread-literals.ts, corpus/asserted-arm-probe.ts, and
corpus/8014-foreign-element-callbacks.ts; the new corpus programs
(8501-8504, 8510-8513, tuple-dynamic-index, record-subset-keyed-read,
union-mixed-field-write) pin the behavior both ways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:12 -07:00
Malte UblandClaude Opus 5.5 378901df95 Convert every arm of a mixed union into the array width
`["one"] | string[]` flowing into a `string[]` slot compiled to the
checked extraction against the array arm alone, so a tuple value threw at
runtime. Node accepts it: the value is an array, and nothing about a
`string[]` position distinguishes which arm produced it.

Let the union-width plan cover a mixed union that holds the destination
arm beside a convertible one, and place that plan before the
checked-extraction rule, so each arm converts on its tag (a tuple rebuilds,
an array copies). A union whose only non-unit arm is the destination
(`T[] | undefined`) still has nothing to convert and keeps its tested
extraction.

tests/corpus/8513-tuple-union-array.ts pins the binding, call-argument,
readonly, and single-tuple shapes against Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:11 -07:00
Malte UblandClaude Opus 5.5 bed8c9dd42 Extend asserted probes to the bracket spelling and union arms
`(x as T)["m"]` — the element-access spelling of a member probe — was not
recognized as a probe, so it went through the assertion's checked
extraction and trapped exactly like the property spelling did before
#2443. Recognize a literal-key element access as the same probe, and
answer it per arm there too.

`"m" in (x as T)` is likewise a probe of the asserted value, not a use of
the assertion: accept a union whose arms are all records or units when the
per-arm `in` answers every arm, so the optional-arm shape compiles instead
of refusing with SC1090.

A real use of the asserted member keeps the checked extraction and still
throws on a lying assertion.

tests/corpus/8512-asserted-probe-element-and-optional.ts pins the bracket
spelling, the member probe, and the throwing real use against Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:10 -07:00
Malte UblandClaude Opus 5.5 ce5562b3e7 Read setter-only and getter-only arms of an asserted probe
`(x as T).m` where T's arms disagree about `m` — one declares it as a data
field, another declares only a setter, or only a getter — fell back to the
assertion's checked extraction, which traps on the arm with no data field.
Node reads `undefined` from the setter-only arm (a read never invokes a
setter) and the getter's result from the getter-only arm.

Count an arm as declaring the member when it carries the data field or a
getter, and let a setter-only arm answer its own read as undefined instead
of bailing out.

tests/corpus/8511-asserted-probe-setter-getter-arms.ts pins both shapes
against Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:10 -07:00
Malte UblandClaude Opus 5.5 c423d640ea Evaluate a calling receiver of a statically-decided in
`"k" in make().api` folded to `true` (or refused with SC1090) as soon as
the receiver's shape decided the answer, dropping the receiver call. Node
always evaluates both operands of `in`, so the receiver still has to run,
exactly once.

Fold the static answer through a `seqExpr` for any receiver that is not
safe to discard, for the declared-field, accessor, and inherited-name
answers alike. Side-effect-free receivers keep their previous IR.

tests/corpus/8510-in-probe-calling-receiver.ts pins the once-only receiver
evaluation against Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:09 -07:00
Malte UblandClaude Opus 5.5 41364d9c08 Spread wider-than-runtime iterables in array literals and push/unshift
`[...maybe]` over a source the checker typed wider than the iterable it
holds at run time (`T | undefined` from an indexed read under
indexed-access strictness, an optional local) and `push(...value)` over
an untyped or widened source were SC1090 refusals. They now drain through
the checked iterator walk, validating each element into the target slot,
and throw Node's not-iterable TypeError for null/undefined.

`widenedIterableType` recognises a union of exactly one iterable arm plus
unit arms; `checkedIteratorPack` is exported so the array-method spread
path reuses the same walk.

Corpus 8504 covers `[...digit]` from a `string | undefined`, a `[...any]`
array literal, a `push(...any)`, and
`push(...(provider.x ?? []))`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:08 -07:00
Malte UblandClaude Opus 5.5 d9e8655eeb Lower conditional spread shapes beyond one empty arm
A conditional spread whose arms are both populated, nested, or collide
with an earlier contributor (or a required field, or an index-signature
slot) was an SC1090 refusal. Those shapes now retry the whole literal as
a checked-dynamic own-key copy validated into the target record: the
one-entry-per-name desugar cannot express which arm runs at runtime.

The syntax fence defers so the checked-dynamic build gets the first
chance, and fires only where that declines. Inside a checked-dynamic
literal a conditional source lowers arm by arm as the dyn value whose
keys the copy reads, so a fresh arm literal is not reshaped against the
enclosing record's contextual type.

Corpus 8503 covers both arms populated with different shapes, a nested
ternary, a conditional over an earlier contributor, an index-signature
base with a conditional, and a `Headers | undefined` spread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:07 -07:00
Malte UblandClaude Opus 5.5 f9f6d4d23a Copy untyped and index-signature spread sources through a checked dynamic object
`{ ...item }` over `unknown`, `any`, or a `Record<string, unknown>` source
was an SC1090 refusal: the record desugar cannot enumerate keys that are
runtime state. These sources now build a checked-dynamic object and
validate it into the target record, copying own enumerable keys in Node's
insertion order. Keys the target does not declare drop, matching the
existing spread stance.

Corpus 8502 covers a `Record<string, unknown>` into a typed record, an
`unknown` clone, and a merge with an explicit later field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:06 -07:00
Malte UblandClaude Opus 5.5 61ab4fbc25 Lower an object spread of a maybe-absent union source
`{ ...maybeRecord }` where the source is `T | undefined` was an SC1090
refusal when the target field had no earlier contributor. Node treats an
absent source as a no-op, so a present source now copies field by field
through the checked extraction (`x!`): the record arm copies the value,
and the absent unit arm throws the catchable TypeError instead of leaving
a required slot unset. An optional target field keeps its no-copy arm.

Corpus 8501 pins the present-source copy and the optional destination; a
new errors-harness case covers both arms of the required-field path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:06 -07:00
Malte UblandClaude Opus 5.5 d9e61976b0 Read and write union fields whose arms store different types
A union with one arm declaring `details: T | undefined` and another
declaring `details: unknown` (pi's ToolResultEvent, the runner.ts:1208
site) has no single static type every arm converts into, so reads and
writes of that field were refused with "assignment to non-variables" /
"reading 'details' on a union-typed value". TypeScript accepts the write
because the writable type of a property on a union is the UNION of the
arms' property types, so the value is legal there.

Reads now project every arm through the checked-dynamic representation:
boxing a typed value into it cannot fail, so isWidening accepts dynFrom
and the dynamic type becomes the shared read type when an arm declares
it. Writes lower the value once at its own type and let each arm convert
it — a free widening where the arm's slot holds it exactly, a checked
conversion where the slot is narrower, so a value that does not fit the
selected arm throws the catchable TypeError of the dyn->static edge where
JS would store it (the divergence a dyn value written into a typed slot
already documents).

Both paths are reached only where lowering refused before, so programs
that compile today keep their IR unchanged.

Corpus: union-mixed-field-write.ts covers the pi shape (an `unknown` arm
beside specific arms), a payload from each arm's own view, assignment in
expression position, an `undefined` payload, and a receiver that is
already the union.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:05 -07:00
Malte UblandClaude Opus 5.5 5bcabf9dd5 Lower a finite-literal keyed read over a subset of the declared fields
`filter[resourceType]` where the receiver is a record with several
optional members and the key type is a finite union of literal keys
(`"extensions" | "skills" | ...`) refused with SC1090 whenever the
checker's answer type did not cover every declared field. That is the
common settings-record shape: the key domain names four of the members
and the rest have unrelated types.

The key type is a finite domain, so the read now dispatches over exactly
the fields it can name, comparing the key against each canonical
property spelling. The answer is the join of the reachable fields' types
— precisely the checker's answer type — and a key outside the domain
reads undefined, as a missing property does. Nothing is emitted for a
field the key type cannot name, so no widening to the full record leaks
into downstream lowering.

The dispatch is gated on the shape having no index signature, on the
access not being a call (`o[k]()` keeps its fence for inherited
methods), on the reachable fields being a proper subset, and on the join
holding an undefined arm so a smuggled key has an answer. A key domain
that names every field keeps the single keyed-read helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:03 -07:00
Malte UblandClaude Opus 5.5 91deb8b5a0 Lower a tuple read with a runtime index as a positional dispatch
`t[i]` on a fixed-shape tuple where `i` is not a literal refused with
SC1090. A tuple is a positional record, so the read now dispatches over
its arity: the element at an integer index in [0, arity), and undefined
for anything else — a fraction, a negative, NaN, Infinity, or one past
the end, exactly Node's array-like read.

The answer type is the join of the element types plus undefined, the
same policy the array index read uses, so a consumer observes a miss
like any other optional element read: `t[i]!` narrows through the
existing checked-narrow path and arithmetic on a miss is NaN. The
receiver and the index are hoisted into hidden locals when they cannot
be repeated (receiver first, since JavaScript evaluates it before the
key). The chain stays inline because the arity is a compile-time
constant, unlike the lifted array helper whose length is not.

Writes keep their fence: a stored position must be a literal index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:37:03 -07:00
Malte UblandClaude Opus 5.5 626e9021a3 Record the new import in the module-order baseline
The corpus fixture gained `mod-default.ts`, so the recorded module
evaluation order for `main.ts` must list it after `mod.ts` and before
`main.ts`. Re-recorded from the native frontend; the order-parity sweep
(1959 entries) passes against it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:33:10 -07:00
Malte UblandClaude Opus 5.5 5d8d6b35a1 Address the child-lifecycle, stream-alias and fs write-options reviews
- child_process: the exit/close/error dispatch loops read the live
  listener registry in their loop condition, so a listener that called
  off/removeListener on a sibling cut the in-flight emit short. Node
  copies the listener array before it emits; detach the registry
  (scr_child_detach_exit/_close/_err), deliver that snapshot, release
  each closure, and free the array in both platform arms.
- process: a first-class alias of stdout/stderr (`const out =
  process.stdout`) keeps the stream's type but read `writableLength`
  through the keyed dynamic get, which answered undefined. Fold that
  read to the same synchronous-write 0 the direct lowering answers,
  leaving the runtime untouched.
- fs: a `const` write-options record was inlined at the write site even
  when it was mutable, so `OPTS.mode = 0o644` before the call diverged
  from Node, which reads the record when the call runs. Accept only a
  record whose properties are all readonly (`as const`, `Readonly<>`);
  a mutable one keeps the SC2020 fence.
- Refresh the node-types-fenced snapshot: `path.win32` sub-namespaces
  lower now, so the PlatformPath.sep fence is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:15:37 -07:00
Malte Ubl 3a93979d10 Merge branch 'ni/i10-js-island-requires' into ni/i11-cross-heap-gc 2026-10-28 09:12:31 -07:00
Malte Ubl f2cee4d5f2 Merge branch 'ni/i9-recorded-deferral' into ni/i10-js-island-requires 2026-10-28 09:12:29 -07:00
Malte Ubl 5f29686b01 Merge branch 'ni/i8-exit' into ni/i9-recorded-deferral 2026-10-28 09:12:28 -07:00
Malte Ubl 1fd7d94cd3 Merge branch 'ni/i7-routing' into ni/i8-exit 2026-10-28 09:12:22 -07:00
Malte Ubl 363cdad97f Merge branch 'ni/i6-subclasses' into ni/i7-routing 2026-10-28 09:12:22 -07:00
Malte Ubl 6d74cbb104 Merge remote-tracking branch 'origin/ni/i5-shared-modules' into ni/i6-subclasses 2026-10-28 09:12:13 -07:00
Malte UblandClaude Opus 5.5 a0849484ab Read an expression default through a module namespace getter
`export default <expr>` and the anonymous default function/class have no
named binding for the dynamic namespace getter to read, so a namespace
used as a dynamic value fenced its `default` property with SC1090. They
do have storage: the module registers a `default` global keyed by its
default export symbol, which is what a default import reads. The getter
keeps that symbol per export and answers the snapshot coerced to `dyn`,
so the boxed namespace's `default` matches Node; a missing registration
still fences SC1090 and a value with no checked-dynamic form still
fences SC1101.

Corpus: 8412-module-namespace-untyped gains an `export default 42`
module and reads its boxed and typed `default`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:09:45 -07:00
Malte Ubl 5c79a40570 Merge branch 'ni/i10-js-island-requires' into ni/i11-cross-heap-gc 2026-10-28 09:06:47 -07:00
Malte Ubl 930e447a5b Merge branch 'ni/i9-recorded-deferral' into ni/i10-js-island-requires 2026-10-28 09:06:43 -07:00
Malte Ubl 88678fa9ac Merge branch 'ni/i8-exit' into ni/i9-recorded-deferral 2026-10-28 09:06:01 -07:00
Malte Ubl a7d8e3ed98 Merge branch 'ni/i7-routing' into ni/i8-exit
# Conflicts:
#	tests/harness/npm-suite.ts
2026-10-28 09:05:56 -07:00
Malte UblandClaude Opus 5.5 10f974aab9 Merge ni/i6-subclasses into ni/i7-routing
Resolve tests/harness/npm.test.ts by keeping main's npm-suite split and
porting this branch's island KNOWN entry and stderr frame normalisation
into tests/harness/npm-suite.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:59:57 -07:00
Malte UblandClaude Opus 5.5 3f91c43a44 Merge origin/main into fix/pi-wp6b-stdlib-tail
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:51:03 -07:00
Malte UblandClaude Opus 5.5 fc407d0751 Lower Array.prototype.join over a dynamic separator
`.join(sep)` over a checked-dynamic separator fenced with SC2020. JS
substitutes the default comma only for the undefined VALUE; every other
value goes through ToString, which prints "null" for null, the digits for
a number or boolean, and throws the catchable TypeError for a symbol.
Evaluate the separator once into a hidden local, test it against the
undefined dyn kind, and answer either the comma literal or
dyn.toStringCoerce of the value.

Unlike the statically typed arms (string, and a union with an undefined
arm), a dynamic separator cannot be resolved statically, so this is the
runtime test the dynamic tier exists for.

Fixes the 2 pi sites (@anthropic-ai/sdk's node.mjs, pi's
interactive-mode.ts).

Evidence: tests/corpus/array-join-dynamic-separator.ts, run under Node and
natively in the plain and sanitized differential lanes, plus its
order-parity baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:50:54 -07:00
Malte Ubl 9e1db03e33 Merge branch 'ni/i10-js-island-requires' into ni/i11-cross-heap-gc 2026-10-28 08:49:44 -07:00
Malte UblandClaude Opus 5.5 538b78f59d Leave the JS-widened binding prepass set unjournaled
analyzeJsWidenedBindings computes jsWidenedBindings for the whole program
before lowering begins, so the journal test classifies it as unjournaled
rather than a unit collection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:49:39 -07:00
Malte Ubl d1566d2746 Merge branch 'ni/i10-js-island-requires' into ni/i11-cross-heap-gc 2026-10-28 08:48:18 -07:00
Malte Ubl a476e02b3a Merge branch 'ni/i9-recorded-deferral' into ni/i10-js-island-requires 2026-10-28 08:48:10 -07:00
Malte Ubl bda1292e5c Merge branch 'ni/i8-exit' into ni/i9-recorded-deferral 2026-10-28 08:47:58 -07:00
Malte Ubl 8a918e7aff Merge branch 'ni/i7-routing' into ni/i8-exit 2026-10-28 08:47:47 -07:00
Malte UblandClaude Opus 5.5 0fb9e25464 Merge ni/i6-subclasses into ni/i7-routing
Resolve tests/harness/npm.test.ts by keeping main's npm-suite split and
porting this branch's island KNOWN entry and stderr frame normalisation
into tests/harness/npm-suite.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:47:41 -07:00
Malte UblandClaude Opus 5.5 126154edc9 Classify the island handle collections as memos and scratch
The lowerer journal test requires every collection to be journaled,
checked or left out by name. islandHandleBindings is a memo keyed by
symbol and islandInstanceofHandled is unwound scratch of the lowering in
progress, so both are unjournaled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:46:58 -07:00
Malte UblandClaude Opus 5.5 3b86b83c45 Merge origin/main into ni/i6-subclasses
Resolve tests/harness/npm.test.ts by taking main's npm-suite split and
porting the island path normalisation and the two island KNOWN entries
into tests/harness/npm-suite.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:46:53 -07:00
Malte Ubl e5f57e2093 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-28 08:43:13 -07:00
Malte Ubl 29a31446d3 Merge remote-tracking branch 'origin/main' into HEAD 2026-10-28 08:43:06 -07:00
Malte UblandClaude Opus 5.5 1d91abadfa Lower Object.getOwnPropertyNames over fixed records
Object.getOwnPropertyNames over a statically typed record fenced with
SC2020 even though Object.keys over the same record lowers. Every own
property a scriptc record carries is an enumerable declared field (or an
index signature's overflow), so both surfaces answer the same key walk:
the interned %obj.keys helper for closed shapes, the declared-then-
overflow walk for index signatures, and the live checked view for a
record that kept undeclared properties of its checked source.

Fixes ~26 pi sites in typebox's guard walk (and the AST validators built
on it) plus gaxios/js-yaml reflection.

Keep the fences where the walk is not the answer: tuples and arrays (their
own names include "length" and the indices), accessor-carrying shapes
(the static walk cannot list accessor names), performance entries, X509,
Maps and class instances.

Evidence: tests/corpus/object-get-own-property-names-records.ts, run under
Node and natively in the plain and sanitized differential lanes, plus its
order-parity baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:38:16 -07:00
Chris Tate dea433dacd Call the receiver copy for direct calls whose result is discarded
A direct call to an untyped method that returns `this` on every path,
used as a statement (`builder.add(part);`), boxed its receiver into a
capsule that nothing read. Such a call now runs the method's copy that
returns nothing, as the class dispatch helpers already do.
2026-10-28 10:21:05 -05:00
Chris Tate a484390e07 Merge main 2026-10-28 09:50:05 -05:00
Chris Tate 3a476d5f32 Fix the X509 recursive-type crash and lower the refused array species forms (#2467) 2026-10-28 09:45:10 -05:00
Chris Tate 1cf59d42e4 Lower Response.clone and Request.clone on typed values
Response.clone() and Request.clone() on a value typed Response or Request
were refused: in TypeScript the fallback declarations had no clone, and
the fetch profile listed both as unsupported, so the call compiled to an
SC2020 refusal, also in JavaScript where the checker types the value. The
checked-dynamic handles already implement them (the body tees, and the
clone gets a copy of the headers with the same guard), so typed calls now
dispatch to those implementations, as text() and json() do.

The declarations gain clone(), and the fetch profile lists both as static
methods with the new static-clone fetch fixture as their differential
evidence. The surface manifest is regenerated, and the surface-manifest
probes that kept clone fenced now fence blob() and formData().

Adds the static-clone fixture and corpus programs fetch-clone.ts and
fetch-clone-untyped.mjs, with their corpus costs and preflight/order
baselines.
2026-10-28 09:33:31 -05:00