Commit Graph
4679 Commits
Author SHA1 Message Date
Malte UblandClaude Opus 5.5 a148177432 Check recursive records coinductively, not by refusing the back-edge
canDynCheckTo answered false at every recursion back-edge in a record or
array, so a type whose checked graph re-enters itself had no checked
extraction at all. That is ordinary in a callback-options API: a context's
method takes an options object whose callback receives the same context, so
passing an unknown value into the record raised SC1100 even though every
leaf is checkable — pi's LoadExtensionsResult graph (extension commands
whose handlers take the extension context) is the census shape.

Answer the back-edge coinductively, as the composite dynFrom walk already
does for recursive shapes: mark the in-progress key (the wrapper's own
success key) for the duration of the frame and verify every reachable field
at its first visit. A field that fails anywhere still fails the frame, and
the existing failure path keeps dropping dependent conversion results, so a
cycle cannot hide an unsupported sibling. The `unsafe` unit fixture now
carries a genuinely unsupported sibling; its former Map sibling is inside
the check domain, so the back-edge refusal was what made it look unsafe.

Only the static conversion domain widens: a program that compiled before
compiles to the same code, and the runtime extraction still walks the
actual value, throwing a catchable TypeError on a mismatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:17:19 -07:00
Malte UblandClaude Opus 5.5 d4041d6b25 Box promises nested in records, arrays and unions as dyn values
canBoxDynCompositeAt, the composite extension of the dynFrom domain, had no
promise case: it fell through to the runtime-handle test and answered false,
so a typed record with a Promise-typed field could not convert into an
unknown slot (SC1101). That is the shape of a model store's read state
(`reload?: { readers: number; promise: Promise<StoredModels> }`), and the
same fence blocked a promise in an array element or a union arm.

The backend already knew how to box one: toDynHelper emits
scr_dyn_new_promise for a dyn payload and scr_dyn_new_promise_adapting for
any other inner type, exactly as it does for a bare promise. Mirror that
conversion domain in the predicate — dyn and void inners, or an inner that
converts to dyn — so the promise boxes by REFERENCE wherever the walker
meets it and identity survives the round trip, as in Node.

The corpus program pins the round trip both ways: the payload, the promise
identity, and the absent-field case, under Node and natively.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:17:19 -07:00
Malte UblandClaude Opus 5.5 54cd19e52a Walk a for..of over an iterable beside never[]
`for (const name of registry.get(key) ?? [])` (mcp's server-tool registry)
types the head `Set<string> | never[]` and had no lowering: the iterable
union rule knows Iterable/generator unions and the indexed numeric arms, so
the loop fell through to the generic SC2001 fence on the union type.

`never[]` is the empty array literal's own type: its only value is the empty
array, which yields no iterations. `T[] | never[]` reduces in the checker
(never is assignable to T), so the reachable shape is exactly one inhabited
non-array iterable beside `never[]`. That loop is the existing container walk
on the arm the runtime tag selects:

    if (unionIsTag(head, setArm)) { <lowerForOfSet on unionNarrow(head)> }

for Set, Map and string — no new representation, and the empty-array arm is
an empty `else`. A union of two inhabited iterables
(`Set<string> | string[]`) still fences: both arms hold values and their
walks yield different element kinds through different protocols.

Corpus 8416-iterable-never-array-arm pins Set/Map/string registries with
present and missing keys, a live set mutated between loops, and an explicit
`Set<string> | never[]` annotation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:17:18 -07:00
Malte UblandClaude Opus 5.5 c3954d4dd4 Give a .map callback that answers a closure an ordinary function element
`entries.map((entry) => async () => …)` (package-manager's concurrency task
lists, `(() => Promise<…>)[]`) was refused: `.map` rejected a `func` return
with the same badType as a `void` return.

A closure is a supported array element. The closure array literal `[() => 1]`
already lowers to a `func`-element array, where the value is one ScrClosure
for its whole life, stored by reference with the element kind's `_v` adapter
and `scr_closure_trace_v`. The map helper builds the same array, so the
`func` half of the fence goes at both `.map` sites (plain receiver and
union-of-arrays receiver).

The `void` fence stays: `void[]` has no element, because a void return value
is erased before it could be written.

Corpus 8415-map-closure-elements pins the `.map`-produced arrays of closures:
`Array<() => Promise<string>>` and `Array<() => string>`, closure identity
through `indexOf`/`includes`, an inline `.map`, and
`Promise.all(tasks.map((task) => task()))`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:17:13 -07:00
Malte UblandClaude Opus 5.5 ea23feeb21 Merge a union's promise arms into one promise of their payload union
`void | Promise<A> | Promise<void> | Decision` (the agent-loop `finishTurn`
hook, codemode's remote client) had no representation: a union arm's index is
its runtime tag and a promise arm's tag IS the `instanceof Promise` test, so a
second promise arm had no tag. The union was refused (SC2009) and every use
site inherited the blocker.

JavaScript cannot tell the arms apart: `instanceof Promise` answers true for
both and `await` settles to either payload. The union therefore maps to one
promise arm `Promise<A | undefined>`: payloads join like union arms (`void`
becomes the undefined unit, an already-union payload flattens, identical
payloads dedupe, a checked-dynamic payload absorbs the arms the dynamic
representation holds faithfully). A payload with no union arm home (a RegExp
payload, a promise-of-a-promise) keeps the whole union fenced, and the
refusal now says why.

A `Promise<A>` value flowing into the merged arm converts with the existing
checked promise-payload widening (promisePayloadWidens / promiseWiden): the
promise keeps its identity and its settlement turn, and the settled payload
is checked against the payload slot.

Cost: the merge only runs for a union that already has two promise arms,
which is refused today. Every other union interns the same arms in the same
order, so a fully typed program without such a union compiles to identical
IR.

Corpus 8414-promise-union-arms pins identity through the union slot,
`instanceof Promise` narrowing, `await`, `.then`, the void arm, the rejection
path and a non-promise data arm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:17:05 -07:00
Malte UblandClaude Opus 5.5 ab5b603e1f Keep the error itself when an interface extends Error
`interface HttpError extends Error { status: number }` receives `new Error(...)`
asserted to it. The error's fields are writable and later writes must show
through the view, so the view is the error object, not a record built from it.

A lib Error, TypeError, ... instance now carries identity into the structural
view, the same way a program class instance does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:16:21 -07:00
Malte UblandClaude Opus 5.5 b716775730 Keep a Map or Set handed to a record view as the collection
A Map or Set is a live collection: a record copy answers the members it copied
once and misses every later write, so `function freeId(seen: { has(k: string):
boolean })` called with a Map must answer through the Map.

The collection now carries identity into the record view, and the view is
registered as a collection view: it keeps checked identity without protocol
dispatch, because there is no stored member to read. Its method calls dispatch
on the collection at runtime (`scr_native_map_invoke`), and `size` reads the
collection's own member.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:14:47 -07:00
Malte UblandClaude Opus 5.5 8e495686e1 Resolve a named Pick or Omit alias to the class it views
`type Readable = Pick<Session, "read">` is the same live view of a class
instance as the inline `Pick<Session, "read">` spelling. Only the inline form
was recognized, so a view named through an alias became a record copy: it
dropped the class's methods and lost the instance's identity.

The resolved type of a program alias is the alias itself, so the reference is
read from the alias's declaration instead: `Pick`/`Omit` keep their first type
argument, and another alias is followed through its own declaration, bounded
in depth. `originsOf` resolves the view the same way, so the class family
behind a named view is found too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:14:12 -07:00
Chris Tate ee30477142 Keep small listener snapshots of readline and EventEmitter emits on the stack
readline's emit and EventEmitter's emit copy the listener list before
running it, so that listeners added or removed during the emit don't
change who runs. The copy was a malloc and a free per emitted event: per
line for a readline 'line' listener. A list of at most four listeners is
now copied into a stack buffer; longer lists still allocate.

A new corpus program pins the snapshot semantics for lists below and above
four listeners: a listener added during an emit runs from the next emit, a
removed one still runs in that emit, and a once listener runs once.
2026-10-28 15:07:25 -05:00
Chris Tate d83a5dc929 Fix census WP-2 stdlib/@types sites: void-slot lambda ABI, ChildProcess.off/signalCode, writeFileSync const options, path sub-namespaces, stdout writableLength (#2468) 2026-10-28 15:04:48 -05:00
Chris Tate e80b5f1447 Compile .length of computed tuple expressions
`.length` of a tuple is its arity, and the lowering folded it to that
constant only when the receiver was rooted at a binding or `this`. Any
other receiver (a call result, a ternary, an `as` assertion, an `as
const` literal) was refused with SC1090.

The receiver is now evaluated once, for its side effects and exceptions,
and its value discarded; the expression is the arity. A binding, `this`,
or a chain of plain property reads from one still folds. A chain through
an accessor is evaluated, so an accessor that returns a tuple runs, as in
Node.
2026-10-28 14:57:21 -05:00
Chris Tate 1edb46545a Lower more pi constructs: tuple and keyed reads, mixed union fields, spreads, assertion probes (#2476) 2026-10-28 14:51:13 -05:00
Chris Tate 4066cf1d1a Lower Object.getOwnPropertyNames over records and Array.prototype.join with a dynamic separator (#2475) 2026-10-28 14:44:07 -05:00
Chris Tate 7e2e47aa8d Report unhandled rejections in programs without async work (#2479) 2026-10-28 14:13:38 -05:00
Malte UblandClaude Opus 5.5 cbd26d3a11 Merge remote-tracking branch 'origin/main' into fix/pi-lowering-constructs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:01:49 -07:00
Malte UblandClaude Opus 5.5 d25f3ba44b Evaluate effectful receivers and keys once in the new lowerings
The keyed and tuple-index dispatches read the receiver and key inside a
discard statement, which re-evaluated an effectful receiver expression
(an `in` test over a call result) and ignored a key call that rebinds the
receiver binding. Both now settle the receiver into a hidden local before
the key evaluates, and the inherited-key probe returns its answer
directly instead of wrapping it in a discard statement.

Narrow the union field write's no-static-supertype path to unions that
have a dynamic arm, so a union of unrelated static storage types keeps
its refusal, matching the read path; a runtime-keyed spread into a record
is covered by the checked extraction test instead of the refusal table.

Review threads:
- 4403396631 inherited-key probe evaluated an effectful receiver twice
- 4403396650 key/index calls that reassign the receiver binding

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:01:33 -07:00
Malte UblandClaude Opus 5.5 f88019a8d1 Merge remote-tracking branch 'origin/ni/i10-js-island-requires' into ni/i11-cross-heap-gc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:58:05 -07:00
Malte UblandClaude Opus 5.5 b1ff156eaa Merge remote-tracking branch 'origin/ni/i9-recorded-deferral' into ni/i10-js-island-requires
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:57:50 -07:00
Malte UblandClaude Opus 5.5 afa84f5c5f Merge remote-tracking branch 'origin/ni/i8-exit' into ni/i9-recorded-deferral
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:57:37 -07:00
Malte UblandClaude Opus 5.5 c3e4f2ced8 Merge remote-tracking branch 'origin/ni/i7-routing' into ni/i8-exit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:57:23 -07:00
Malte UblandClaude Opus 5.5 d4445ec628 Merge remote-tracking branch 'origin/ni/i6-subclasses' into ni/i7-routing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:57:10 -07:00
Malte UblandClaude Opus 5.5 de7f40e87a Merge remote-tracking branch 'origin/main' into fix/pi-wp6b-stdlib-tail
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:57:03 -07:00
Malte UblandClaude Opus 5.5 81580f44ad Merge remote-tracking branch 'origin/main' into ni/i6-subclasses
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:56:30 -07:00
Malte Ubl 7dcf1376b6 Merge remote-tracking branch 'origin/main' into fix/pi-lowering-constructs 2026-10-28 11:40:29 -07:00
Chris Tate ad368e353d Lower Response.clone and Request.clone on typed values (#2473) 2026-10-28 13:37:52 -05:00
Chris Tate f61c40e4e7 Space the timer delays in two order-sensitive tests and raise the static-spatial timeout (#2478) 2026-10-28 13:36:39 -05:00
Malte UblandClaude Opus 5.5 031b46d2b7 Evaluate the stream receiver before the writableLength constant
A procStream-typed receiver that is a call or an accessor read now runs
for its effects, and propagates its throw, before the fold answers 0: the
constant sits in a seqExpr behind an exprStmt receiver, the same
evaluate-for-effect shape objectIsDisjointFalse uses. A pure alias still
folds to the bare constant. The corpus program covers a call receiver, an
accessor receiver and a throwing receiver against Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:33:39 -07:00
Malte UblandClaude Opus 5.5 753532a3da Merge origin/main into fix/pi-wp2-stdlib-node
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:33:39 -07:00
Chris Tate 911857561e Read typed-array constructors, NaN and Infinity in shorthand properties (#2477) 2026-10-28 13:26:13 -05:00
Chris Tate 7bb87229c2 Report unhandled rejections in programs without async work
A program with no async functions, generators or timers never ran the
event loop, so a promise rejection nothing handled went unreported:
`Promise.reject(new Error('x'))` alone exited 0, where Node prints the
error and exits 1. The loop now also runs when the program makes a
promise (a Promise static, `new Promise` or Promise.withResolvers), and
its end reports the rejection, runs the 'exit' listeners with code 1 and
exits 1, as for async programs. Programs that make no promise keep their
code unchanged.
2026-10-28 13:23:10 -05:00
Chris Tate 44a4cd9db1 Merge main 2026-10-28 13:19:00 -05:00
Chris Tate 55a803c733 Read the array's length before converting a join separator
Array.prototype.join reads the length before it converts the separator.
The lowering converted the separator first and let the join read the
length afterwards, so a separator whose toString changed the array joined
the wrong number of elements. When ToString of the separator value can run
code of the program (a checked-dynamic value, an object or class instance
with toString), the join now reads the length first: an array that grew
joins its first elements, and the missing elements of one that shrank join
as empty strings. Primitive separators keep the direct join.
2026-10-28 13:07:14 -05:00
Malte Ubl 9fbcdc9f03 Merge remote-tracking branch 'origin/ni/i10-js-island-requires' into ni/i11-cross-heap-gc 2026-10-28 10:57:17 -07:00
Malte UblandClaude Opus 5.5 e7c83477dc Suppress OpenSSL's default-verify-path store in the sanitized lane
6883-node-island-builtin-handles builds a Headers inside an island, undici
loads node:tls for the builtin classes it backs, and OpenSSL parses the
system CA bundle through PEM/X509. That state is freed only by
OPENSSL_cleanup at exit, which NI-7c keeps off, and the stacks carry no
node::crypto frame, so the existing entries cannot match. Name OpenSSL's
allocator entry points, as the file already does for the rest of libnode's
process-lifetime state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 10:56:59 -07:00
Chris Tate 577f086658 Space the timer delays in two order-sensitive tests and raise the static-spatial timeout
mock-timers' "named and namespace imports keep the loop's timers" and
6811-node-island-uncaught-listener check the order in which real timers
fire, with delays 1 to 5 ms apart that are scheduled one after another.
On a busy runner more time than that can pass between scheduling two of
them, and the order flips in either lane: the sanitized node:test run saw
Node's reference print 'named promise' first, and the sanitized
llvm-differential run saw 'island timer 1' before 'native timer after'.
The delays are now 100 ms apart in mock-timers (100, 200, 300) and the
island's timers (150, 250) start 100 ms after the native ones (10, 50).
Node prints the same output as before.

static-spatial bundles three.js and builds it with the sanitizer, which
passed the default 300 s test timeout on a busy runner. Its cases take
three.test.ts's 1200 s timeout.
2026-10-28 12:49:21 -05:00
Chris Tate 5a1021dcd1 Fence Response.formData in the node-types fenced fixture
Response.clone lowers now, so the fixture's unlowered Response
operation is formData, which keeps the same fence and hint.
2026-10-28 12:42:13 -05:00
Chris Tate 2f3b926de8 Share modules between a program and its Node islands through loader hooks (NI-6) (#1902) 2026-10-28 12:41:52 -05:00
Chris Tate 68980c2414 Box user module namespaces as dynamic values (#2469) 2026-10-28 12:40:34 -05:00
Chris Tate 857b4ad3e0 Read typed-array constructors, NaN and Infinity in shorthand properties
A shorthand property's name carries the property's symbol, so the
identifier lowering's typed-array and NaN/Infinity cases, which checked
that symbol's provenance, did not match `{ Uint8Array }` or `{ NaN }`. A
JavaScript source stored the opaque identity token there, and TypeScript
stored a runtime fence; the long form `{ Uint8Array: Uint8Array }` read the
constructor. Both cases now resolve the name as the other builtin cases do
(isStdlibGlobal), which reads the shorthand's value symbol.

In JavaScript sources, `globalThis.NaN` and `globalThis.Infinity`, and
destructuring them from the global object, read the identity token too.
They now read the numbers.
2026-10-28 12:38:39 -05:00
Malte Ubl 02988d23df Merge branch 'ni/i10-js-island-requires' into ni/i11-cross-heap-gc 2026-10-28 09:38:33 -07:00
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 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