Commit Graph
4375 Commits
Author SHA1 Message Date
Malte UblandClaude Opus 5.5 b200bde558 Read a generic constraint member the instantiation omits
`options.sessionId` inside a generic function whose type parameter's
constraint declares `sessionId?: string` fenced with SC1090 when the call
site's options record has no such slot:

  function applyAuth<TModel, TOptions extends ProviderRequestOptions<TModel> & Transforms>(
    model: TModel, options: TOptions | undefined) {
    const sessionId =
      options && "sessionId" in options && typeof options.sessionId === "string"
        ? options.sessionId : undefined;
  }
  applyAuth(model, { apiKey, azureApiVersion });

The checker admits the read through the constraint (and, with the `in`
guard, through a synthetic `T & Record<"sessionId", unknown>`), while the
instantiated record — the value's actual layout — stores no such field.
`fieldTarget` found no record slot and the read fell through to the
generic unsupported-property fence, even though Node simply reads the
property off the object it has: undefined when the caller omitted it, the
extra property when a JavaScript caller supplied one.

fieldTarget now recognizes a generic receiver (a type parameter, or an
`in`-narrowed intersection that contains one) whose stored record lacks
the member, and lowers the read as the checked-dynamic keyed read of that
record value, entering the static code at the type the checker gave the
read. Reads that keep a slot take their record get as before, and the
programs this path opens did not compile at all before, so typed code
keeps its representation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:14 -07:00
Malte UblandClaude Opus 5.5 04409081f9 Lower chain-guarded union reads from the stored union
Inside an optional-chain body the checker types the guarded receiver by its
non-nullish narrowing, often one record of a discriminated union, while the
bound chain value still carries the whole present union: a runtime-optional
local that crossed an `await` and a prior `stored?.type === "oauth"` return
holds `ApiKeyCredential | OAuthCredential` at runtime.

lowerUnionProperty gated on the spelled receiver type being a union, so the
read fell through to the generic SC1090 fence ("reading 'type' from a value
of type 'ApiKeyCredential'"). Take the union from the chain binding, which is
the value lowerExpr is about to read; unionDisc then dispatches on the stored
tag and maybeNarrow applies the checker's narrowing to the result.

Adds the differential corpus program union-read-after-chain-guard and its
emission test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:13 -07:00
Malte UblandClaude Opus 5.5 1ed5ba53aa Lower u || d where the checker narrowed u to one of its own arms
`return typeof options === "string" ? {...} : options || {}` refused:
flow narrowing ruled the string arm out, but the stored value keeps the
wider union, so the logical operator saw a union-typed left operand
outside a condition.

When the checker's result is one of the left value's own arms, test the
value in its own union and extract the result arm through the checked
single-arm narrowing: the falsy arm takes the default, which keeps its
lazy evaluation, and a value of a ruled-out arm traps rather than
masquerading as the result type.

Corpus 8526 pins the narrowed `||` default over an optional interface, a
union of records, and lazy default evaluation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:13 -07:00
Malte UblandClaude Opus 5.5 00467fdcf6 Append a flatMap callback's TUPLE result arms in order
A block-bodied callback whose enclosing call has a contextual array
type infers a tuple union rather than an array union: `return pair ?
[pair] : []` in a `f(): T[]` infers `[] | [pair]`. The flatten plan
classified only array arms, so such a callback refused.

The empty-arm detection now recognizes the empty tuple (a bare `[]` arm
under a contextual array type), and the plan gains a `tuple` arm for a
one-field-or-more tuple whose fields all contribute: each field is
appended in order, exactly as an array arm's elements are. `checker.
isArrayType` is false for tuples, so the arm scan accepts
`isTupleType` too.

Corpus 8525 pins the tuple arm, a two-field tuple arm, and an array arm
beside a scalar arm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:12 -07:00
Malte UblandClaude Opus 5.5 94036b088b Narrow .filter predicate results to structural types, not just arms
A written type predicate usually names FEWER members than the
receiver's element has — `(c): c is { text: string } => ...` over a
block that also carries a `textSignature` — and may name a structural
type that is no arm of the element union at all. Both shapes refused,
so `commitments.filter(pred)` over a union of records with differing
members did not compile.

Record targets now convert when their members are a SUBSET of the
source's: each target member is read and converted, the copy keeps the
source's keys in the source's order, and a member the source lacks
still has no slot to read and stays refused.

When no union arm matches the predicate's type, `structuralUnionConversion`
converts tag by tag: each arm the target can be read from returns its
converted value and an arm with no such read throws the same checked
TypeError a lying predicate throws, so the checked extraction still
traps rather than silently passing a foreign value through.

Corpus 8524 pins the subset-record and structural-union conversions;
`narrow-guard-traps.test.ts` pins the TypeError a predicate that
returns true for a foreign value raises.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:12 -07:00
Malte UblandClaude Opus 5.5 e198a9eec2 Dispatch a keyof read whose member is unknown per declared key
`settings[key]` over a settings record with an `unknown` member
(`type: unknown`) refuses even though every field reads through a
checked-dynamic slot: the checker's answer collapses to `unknown`
because the union of the field types absorbs into it, so there is no
typed join for the keyed read to dispatch into.

Recognize that shape — the read's type is `unknown`, the shape has no
index signature, and every declared field's value is checked-dynamic
representable — and dispatch per declared key answering a checked-
dynamic read. The key type is the record's own `keyof`, so the answer
is always present.

Corpus 8523 pins the read differentially.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:11 -07:00
Malte UblandClaude Opus 5.5 3328ed2aa7 Lower union member reads the checker narrowed away
Reading a member off a union-typed value is what most pi refusals at
`SC1090` came down to: `credential.type` on `ApiKeyCredential |
OAuthCredential`, `session.role`, `entry.timestamp`, `tool.operations`,
`arms.length`, `renderer.hasOverlayEntries`.

Three read shapes reach the same dispatch and now answer:

- A checker-NARROWED receiver keeps the wider union it stores, because
  lowering erases flow narrowing (`stored?.type` after the earlier arm
  returned, `(entry as { role: string }).role`). The dispatcher now reads
  the member off whichever arm the value holds — an arm that does not
  declare it answers undefined, exactly Node's get — and narrows the
  answer to the checker's type, which traps on an arm the narrowing
  ruled out (types do not lie). This fallback sits after every existing
  lowering, so only refusals gain one.

- A member the arms answer through an ACCESSOR (a getter inherited from
  a shared base class, `renderer.hasOverlayEntries`) has no field slot
  `unionDisc` could read, so the read dispatches on the tag and calls
  each arm's own getter. Data-field unions keep their `unionDisc`
  lowering.

- `.length` on a union of arrays and strings (`Model[] | ModelRef[]`,
  `string[] | Buffer[]`) dispatches on the tag and answers each arm's
  own length.

The member read moves out of `lowerUnionProperty` into
`lowerUnionValueRead`, which both entry points share, and
`missingFieldAsUndefined` threads the narrowed-receiver get semantics
through the keyed read.

Corpus 8520/8521/8522 pin the three shapes differentially.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:11 -07:00
Malte UblandClaude Opus 5.5 5afe93093f Accept a wider lambda whose own signature is already its completed ABI
A lambda declaring an optional or defaulted trailing parameter — the
`(a, b, loose?) => …` semver comparator, the sort callback whose surplus
position is a flag — was refused at its birth fence (SC1090) even though
every read of it spells that position as `T | undefined`, the completed
native ABI. The fence compared the slot's mapped type against the
contextual lambda's, which misses two shapes the checker accepts: a
lambda whose own `any` parameter mapType leaves unmapped, and a
*contextually* typed lambda whose OWN signature is wider than the slot
it is written under.

requireExactArityValue now admits a lambda whose own call signature maps
to the slot position for position, and cleanFuncAdaptable accepts a
source with extra trailing parameters when each of them completes with
the undefined value, the same absence a call through the slot passes.

`sort`/`toSorted` separately required a comparator of at most two
parameters; it now adapts the first two and lets the surplus ones
complete as undefined, so the imported three-parameter comparator sorts.

Corpus 8529 pins the surplus-parameter comparators (optional, defaulted,
`any`-typed), the direct calls through them, `toSorted`'s untouched
receiver, and remains byte-exact against Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:10 -07:00
Malte UblandClaude Opus 5.5 49b133751c Adopt a contextual union's map arm for a bare new Map()
`new Map()` has no type arguments, and the no-argument constructor gives
the checker nothing to infer them from: the expression types as
Map<unknown, unknown> however the destination spells it. Construction
adopted a contextual type only when it mapped to a map, so a nullable or
readonly annotation — `Map<K, V> | undefined`, `Map<K, V> | null`,
`ReadonlyMap<K, V> | undefined`, an optional record field — refused the
very value its slot was built for (SC2003; pi's extension registry, whose
`handlers?: Map<...>` fields start empty).

A Map value can only inhabit a union's map arm, so construction now
adopts that arm when the contextual union has exactly one: the remainder
of the annotation is the nullish absence the value never is. Two
different map arms stay refused (SC2003), because the layout would be
ambiguous.

Corpus 8528 pins the nullable, readonly and optional-field annotations,
a parameter slot, iteration after the adoption, and remains byte-exact
against Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:10 -07:00
Malte UblandClaude Opus 5.5 223973a56b Store a discarding mapper's answer as an undefined array element
`Array.from({ length: n }, () => undefined)` and `.map(() => undefined)`
type as `undefined[]`, which the type mapping represents as an array of
unit-union elements. Callback-driven producers take the element from the
lowered callback's return type, and a standalone `undefined` in return
position maps to the void convention — its answer is erased before it
could be stored — so every discarding callback was refused (SC2009; pi's
tui terminal-color palette).

A producer whose result element is the unit-only union now gives the
callback that union as its contextual return, exactly as the array
element mapping substitutes it for a `void` element: the arrow's
`undefined` answer becomes the present unit value. `[undefined, ...]`,
sparse literals and an explicit `undefined[]` annotation already had
that representation.

Corpus 8527 pins the palette field, the mapper's call order and index,
the `.map` and `Array.from(array, ...)` forms, `in`/JSON and the empty
case under Node and natively.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:09 -07:00
Chris Tate 686c34bd25 Store tty.isatty and process.cwd as function values (#2494) 2026-10-28 22:46:19 -05:00
Chris Tate 6f2a18d16c Test262 group B: generator method ABI, empty-pattern throws (#2505) 2026-10-28 22:41:17 -05:00
Chris Tate 3e7528ca86 pi wave 4: path values, union delete, NonNullable unions, BigInt stats, optional-param function values, live accessor literals, clearer stdlib-alias errors (#2497) 2026-10-28 22:33:16 -05:00
Chris Tate 91b5d267b9 Keep frozen checked records frozen, and give sloppy-mode this the global object (#2453) 2026-10-28 22:02:18 -05:00
Chris Tate 2ae08e4002 Route more packages and values through Node islands (NI-4c–4g) (#1904) 2026-10-28 21:53:40 -05:00
Malte UblandClaude 5.5 34c39e5443 Throw on empty patterns over checked-dynamic sources
An empty binding pattern still performs its one observable step:
RequireObjectCoercible for `{}` and GetIterator for `[]`. Over a statically
non-nullish source that step is invisible, which is why the empty-pattern
lowering emitted nothing; a checked-dynamic source can be null or undefined
at runtime, where V8 throws.

Emit the runtime null/undefined test for dyn sources in the empty-pattern
branch, and let a JS-file empty object pattern take the source-spelling
message dynDestrCheck already produces for non-empty ones.

Test262 (static-adapted-v6, --workers 8), affected chapters before -> after:
language/statements/class/dstr pass 1518 -> 1526, fail 424 -> 416;
language/expressions/class/dstr pass 1518 -> 1526, fail 424 -> 416. No
pass -> fail.

Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
2026-10-28 19:51:10 -07:00
Malte UblandClaude 5.5 460bd6fb38 Keep a generator method's generator ABI through the prototype-access widening
A class whose methods are reachable through `X.prototype` widens its method
signatures to checked values so a JS caller can hand anything across. The
widening replaced every non-void return with DYN, which erased a GENERATOR
method's generator type; the generator branch below then had no generator
channels to build its metadata from and refused the whole class (SC2001 in
`%init`, re-emitted as a runtime fence in a JS file). Every
`assert.sameValue(c.m, C.prototype.m)` variant of the campaign therefore
refused at runtime, 759 of them in the generator-method chapters alone.

A generator's return kind is already a checked-crossable ABI: dynFrom boxes a
generator by reference and dyn calls resume it through the dyn generator ops,
so only the arguments need the widening. Guard the return widening to
non-generator methods.

Test262, 1,110 affected variants, before/after on the candidate
(class/gen-method 52->76, class/async-gen-method 76->128,
class/scope-gen-meth 0->8); no pass->fail and no typed-path delta.

Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
2026-10-28 19:51:09 -07:00
Chris Tate e506d19cd6 Name destructured builtins, parenthesized functions and util.debuglog as Node does
Reading the name of a function stored in a const followed const aliases to
the definition, but stopped at destructuring: `const { cwd } = process`
gave the binding's name instead of Node's "wrappedCwd". A member
destructured from the process global or a builtin module's namespace now
gives the module table's name for it.

An anonymous function wrapped in parentheses (`const f = (function () {})`)
answered an empty name. As in Node, it takes the name of the binding or
property it initializes through the parentheses.

A builtin read as a property (`util.debuglog.name`) answered the name its
shared adapter was first created under, so after `util.debug` it answered
"debug". The name now comes from the property and the module table, and
util.debug's value is named "debuglog", as in Node.
2026-10-28 21:50:00 -05:00
Chris Tate 52e3dadc20 Match Node for BigIntStats dates, bigint option effects and path namespace spreads
- A BigIntStats Date member is built from its truncated millisecond member,
  as Node's getters do, where the Stats getters round the fractional one: a
  timestamp at least 0.5 ms past a millisecond no longer reads one
  millisecond later than Node.
- statSync, lstatSync and fstatSync evaluate a bigint option whose value can
  have an effect (`bigint: (count++, true)`), in member order after the path
  and before the stat; an exception it throws is the call's.
- An object spread of a node:path namespace in JavaScript copies the module's
  members, read through the module value so the copy holds the same
  functions: an ES module namespace with its keys sorted and `default`, and
  posix, win32 and the default import in Node's member order.
- scr_lib.h declares the BigIntStats readers, so every runtime name the
  backend can emit has a prototype in scr_runtime.h.
2026-10-28 21:16:57 -05:00
Chris Tate 3b520e4fb2 Merge main 2026-10-28 20:50:23 -05:00
Malte Ubl db617bd594 Merge remote-tracking branch 'origin/main' into fix/pi-conversions-layouts 2026-10-28 18:30:14 -07:00
Chris Tate e0c692593c Evaluate a for..of source beside never[] once, keep collection views through a second view, and combine typed promises in untyped JavaScript
- lowerForOfEmptyArrayArm binds the `Set | Map | string | never[]` source
  to a hidden local before the tag test, so a call or getter in the loop
  head runs once and the loop walks the value the test saw.
- markTarget marks a structural view as a collection view when its source
  is already one, and reruns the fixpoint when a view marked earlier
  becomes a collection view, so a Map or Set handed through two views keeps
  answering through the collection.
- Promise.all in a JavaScript source whose call types as Promise<any[]>
  (an unannotated parameter) runs the engine's Promise.all over the entries
  as checked-dynamic values when they are typed promises. A merged
  `Promise<number | string>[]` argument reaches that call since union
  promise arms merge.

Corpus 8419 and 6956 and the js-own-tostring npm fixture cover each case.
2026-10-28 20:22:14 -05:00
Chris Tate d1abf69149 Refresh the runtime profile and convert ASCII case eight bytes at a time (#2492) 2026-10-28 20:15:03 -05:00
Malte UblandClaude Opus 5.5 caf098eff9 Refuse a stdlib-global snapshot read as a value
`const proc = globalThis.process` lowers to alias plumbing: the declaration
emits no storage and no code, because member and call reads resolve through
the global it names. A read that needs the VALUE therefore failed at the use
with the generic "binding form with no lowering", which names neither the
declaration nor the cause.

Register every stdlib-global snapshot declaration (TypeScript sources only,
matching the elision) and report the cause at the declaration (SC2020),
naming the global and, when it has a short spelling, the type the program
asked the compiler to hold. The use site keeps the inherited-blocker cascade
(SC2004) instead of the generic text.

An unread snapshot stays elided: reading a global has no side effects, so
dropping an unread `const p = process` is observationally identical to Node,
and refusing it would break programs that compile harmlessly today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:04 -07:00
Malte UblandClaude Opus 5.5 83e26da25e Keep a literal's accessors live through a declared data member
An object literal whose get/set accessor satisfies a member the target type
declares as DATA was refused with SC1090: a record member is one storage
slot, so the accessor could only be snapshotted at construction. Node keeps
the accessor live, and the pi sites that hit this (agent.ts:90,
runner.ts:889, codemode/execution.ts:562) are all live getters — one throws
until the object observes later state — so a snapshot is disqualifying.

One static record layout cannot hold both member kinds at one name, so the
analysis marks the TARGET type instead of inventing a literal-only variant:
when a literal's member kind disagrees with the member the contextual type
declares at the same name, that type keeps checked identity, exactly as a
structural type that carries a class instance's identity does. The literal
then lowers through the checked-dynamic literal path, which defines accessor
members as live get/set properties and data members as plain properties, and
reads, writes, Object.keys, `in` and JSON.stringify on the declared type
dispatch dynamically — Node's own model.

The mirrored direction is closed too: a data property satisfying a declared
accessor member (`interface Live { get size(): number }`) used to be an
SC2002 missing-`%get:`-slot refusal; it now keeps the declared type's
identity as well.

Cost, the accepted trade: every value of a MARKED type becomes a checked
dynamic view — a property table and closure dispatch instead of a static
struct slot, so reads and writes on those types are slower (never wrong).
The cost is scoped to the marked type: a program with no such literal marks
nothing, and the frontend analysis itself adds only one array push per object
literal and one Set lookup per data member, with no checker query unless a
member name is an accessor somewhere in the program. IR is byte-identical
for 64 existing corpus programs compiled before and after. The whole-type
accessor-shape form (static reads, closure slots) remains available as the
faster representation if profiles ever show this matters; it needs the
accessor enumeration surfaces (keys, JSON, inspect, for-in, spread, rest)
that this route gets for free.

Only a single object type can be the literal's target: a multi-arm union
picks its arm from the value itself and an intersection merges members, so
neither is marked and both keep today's behavior. A value that reaches the
slot through a variable rather than a literal keeps today's refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:04 -07:00
Malte UblandClaude Opus 5.5 6e396ad721 Read a shared member off a receiver the checker ruled unreachable
An exhaustive switch's `default:` branch declares `const unreachable: never =
event`, and the warning reporter reads a member off that value through an
erasure (`(event as any).type`, `(event as unknown as { type: string }).type`) —
the shape pi's proxy warning takes. The union shared-field read refused it
because the checker types the receiver `never` (or the erasure), not the
union the value still holds.

`lowerUnionProperty` now proceeds when the receiver expression is a `never`
value, directly or through the parenthesized/parenthesized-assertion chain
that escapes it, and reads the field off what the lowered value actually is:
the shared-field read for the union it holds, the record read for a record,
the keyed read for checked-dynamic. `never` admits no reachable call site, so
no reachable program changes shape.

Corpus: tests/corpus/erased-never-member-read.ts (`as any`, `as unknown as`,
and a helper function taking the same shape). Node-identical stdout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:03 -07:00
Malte UblandClaude Opus 5.5 7245ec9a04 Admit function values whose completed parameters the slot coercion adapts
A method declaring `ctx?: Ctx` completes to `(id, Ctx | undefined) => string`;
a slot that requires the narrower `(id, ToolCtx) => string` was refused with
SC1090 at the value's birth, because the gate only compared the two
signatures for equality. The slot coercion (funcCoerceAdapter) already
converts every parameter through coercibleValue, the record width conversion
(which also selects a width-liftable arm of a union destination — the
`ToolCtx` into `Ctx | undefined` shape), the partial checked-dynamic
extraction, and the union arm preservation. `funcSlotAdaptable` now asks the
same question through `cleanParamPair`, so the gate admits exactly the pairs
the adapter converts without a trap-only path, and both call sites (the
object-literal method slot and the mapped value target) use it.

The gate also learns the two slot shapes it could not name: an object
literal contextualized by an INDEX SIGNATURE (`Record<string, Handler>`)
takes its member slot from the index signature's value type, and a literal
with no contextual type takes the member's own completed type — the type
every later read of the member spells.

Nested function DECLARATIONS are no longer judged at birth: lowering a
declaration builds the body, not a value, and each value use re-checks the
completed signature against that use's own target. An overloaded nested
declaration with an optional parameter, returned as a value (`return write`
into a one-parameter `Writer`), is now accepted instead of refused.

Corpus: tests/corpus/optional-param-function-values.ts (a required slot over
an optional parameter, an index-signature member with an extra trailing
optional parameter, an annotated value with a defaulted parameter, the nested
overloaded declaration returned as a value, direct-call controls, and a
union-typed slot). Node-identical stdout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:02 -07:00
Malte UblandClaude Opus 5.5 cf7814d19c Lower fs statSync/lstatSync/fstatSync with { bigint: true }
Node answers a BigIntStats when the stat call is passed `bigint: true`: the
same snapshot, read through a bigint member table. Every number member of
Stats reads as a bigint — the integers, the `*Ms` millisecond members (the
truncated nanosecond quotient, as the binding computes them) and the
BigIntStats-only `*Ns` nanosecond members (sec * 1e9 + nsec, exactly) —
while only the four time members stay Dates. scriptc refused the option
outright.

The representation is static, chosen by the option literal: the lowerer
validates `bigint` against the call's result type (so
`{ bigint: true, throwIfNoEntry: false }` still narrows
`BigIntStats | undefined`), refuses a runtime-valued option and an option
that disagrees with the destination, and keeps every other option key
fenced. fstatSync joins statSync/lstatSync on the same path, with the
descriptor evaluated before the options.

The IR gains a `bigintStats` handle kind beside `stats`: the same RC
family, the same non-traced leaf and non-dynamic handle treatment, so no
shape, tracing or dyn-check behavior moves for any program that never asks
for one. Its member table is a `bigstats.*` lib-call family whose Date
members reuse the existing stat reads.

The runtime support is its own unit, scr_stats_bigint.c: the snapshot
layout moved into scr_stats_private.h so the bigint getters can read the
same fields without scr_lib.c carrying their code. The unit is gated on the
existing `bigint` feature in both the runtime-pack matrix and the
compiler's from-source unit table (the sanitized lane links from that one),
and `-ffunction-sections` keeps a bigint program that never reads a member
from linking any of the getters. No typed program's IR or LLVM changes.

tests/corpus/7018-bigint-stats.ts pins the member table, the Date/bigint
split, ms/ns agreement, arithmetic on a member, lstat over a symlink,
fstatSync over a descriptor, the throwIfNoEntry union, and the plain Stats
answers of the un-optioned and `bigint: false` forms against Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:01 -07:00
Malte UblandClaude Opus 5.5 4c4a53d538 Name the arms a union-typed spread merge cannot pick between
A spread merge like `{ ...base, ...override }` whose sources are union
types has one field set per combination of source arms, so the merged value
can carry fields from several arms at once. No single arm of the
destination union can hold it — naming one would drop the other arms'
fields, which Node keeps — and no runtime tag would be honest. The merge
was refused with the generic "cannot be compiled yet" (SC2001), which says
nothing about the shape the program is building.

The new SC2003 names the destination union and lists the candidate arms,
and its hint gives the two repairs: merge ONE arm per path (narrow every
spread source to the same arm first), or declare one open record type whose
fields are all optional instead of a union of all-optional arms. The
diagnostic fires only for a union-typed destination; other non-record
destinations keep badType.

`NonNullable<T>` over a union already maps to the union value itself, so
tests/corpus/7017-nonnullable-union.ts pins the mapped-type path
(assertions, generic instantiations, narrowing, serialization) against
Node. tests/diagnostics/union-spread-merge.ts re-pins the refusal, and the
union-clone-override-width snapshot moves from SC2001's generic text to
the arm-naming SC2003.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:00 -07:00
Malte UblandClaude Opus 5.5 f7a22cb0c7 Lower delete u.k when every union arm declares k optional
A union of records whose every arm declares the same field optional has a
definite answer for `delete u.k`: the field becomes absent, exactly as the
monomorphic optional-field delete writes it. The delete statement used to
fall through to the receiver-agnostic path and refuse the union.

lowerUnionFieldDelete dispatches over the arms (one tag test per arm, the
last arm as the fall-through else) and writes each slot's absent value
through a recordSet on the narrowed arm; the union value is retained once,
so every arm deletes the object the program passed rather than a
projection. Sloppy-mode writes keep their frozen-record semantics.

Everything else keeps its fence: unit arms, class arms, required fields
(a slot with no absence to represent), accessors, index signatures,
optional chains and tuples all return null and reach the existing
diagnostics.

tests/corpus/7016-union-field-delete.ts pins the delete, the absence
afterwards (`in`, optional reads, JSON.stringify), the retained identity
the caller's alias sees, and deleting an already-absent field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:00 -07:00
Malte UblandClaude Opus 5.5 81cacab03d Lower node:path's platform namespaces as path module values
path.posix and path.win32 are module objects, not declarations: they are
the same handle `process.getBuiltinModule("node:path")` answers, so
`posix === path.posix` holds and a mutation through one spelling shows
through the other. Reading either namespace as a value mapped to a module
namespace type with no value lowering, and `import { posix } from
"node:path"` was refused outright.

The platform namespaces now lower to the checked-dynamic module value, as
a `path.posix` property read, as a named import, and as a receiver
(`win32.join(...)`); pathModuleValue answers the module handle instead of
building a fresh object literal, which is what makes the identity and the
cached members hold. Type mapping treats a path platform namespace and
@types/node's PlatformPath interface as the dynamic type they are, so a
union with a plain record arm collapses onto it instead of refusing.

tests/corpus/7015-path-platform-namespace-values.ts pins the identity,
cached members, joins and the import forms against Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:57:59 -07:00
Chris Tate 84644dfe9d Send the writeHead head with the response's first write (#2488) 2026-10-28 19:18:26 -05:00
Chris Tate db30118a0e Store tty.isatty and process.cwd as function values
Fixes #2408
Co-authored-by: MathurAditya724 <57684218+MathurAditya724@users.noreply.github.com>
Fixes #2402
2026-10-28 19:18:01 -05:00
Chris Tate 7f0c987140 Key Sets and Maps of identity-arm unions by their pointer representation (#2487) 2026-10-28 19:08:20 -05:00
Chris Tate 61c24c08e9 Refresh the runtime profile and convert ASCII case eight bytes at a time
scr_str_case_conv scans ASCII text for the first letter to convert eight
bytes at a time, then converts the rest eight bytes at a time; the tail
and big-endian targets take the byte loop. With a refreshed profile, the
byte-at-a-time scan compiled worse for short strings with nothing to
convert, which made validate-errors slower.

The runtime profile is collected again on this change with
scripts/refresh-runtime-profile.mjs (326 raw profiles). Changed
functions hold 0.0% of its counts, against 3.7% for the previous one.

tests/corpus/string-case-ascii-words.ts covers ASCII strings of every
length from 0 to 40, with letters at block boundaries and next to the
bytes just outside A-Z and a-z.
2026-10-28 18:43:37 -05:00
Chris Tate 18ab17848e Keep the held head across a cork() and uncork() with no write
An uncork() flushed a head writeHead stored even when nothing was
written while corked. Node keeps the stored head until write(), end() or
flushHeaders(), so the flush now sends it only when a write() or
flushHeaders() ran while corked. The http-held-head fixture covers the
three cases.
2026-10-28 18:23:49 -05:00
Chris Tate 94e921728c Island handle operations, native subclasses of island classes, arrays and collections by reference (NI-5b/5c/5d) (#1903) 2026-10-28 18:09:56 -05:00
Chris Tate 7409c85216 Compile .length of computed tuple expressions (#2482) 2026-10-28 17:51:45 -05:00
Chris Tate 3a1ba89194 Throw for .length of a missing tuple element
An unchecked element read of a tuple array (`rows[i].length` with
`rows: [number, string][]`) answers the element's optional value, and the
computed `.length` path discarded it, so a read past the end printed the
tuple's arity. It now holds the receiver and throws Node's TypeError,
"Cannot read properties of undefined (reading 'length')", when the element
is missing, before answering the arity.

The corpus program tuple-length-computed.ts covers a missing element read
directly and through a field holding the array.
2026-10-28 17:04:51 -05:00
Chris Tate 94215d6f0e Keep glibc's memset, memcpy, memcmp and bcmp in zig-linked executables
The BigInt reciprocal divided a two-limb value by one limb, which compiles
to a __udivti3 call. zig provides that builtin from its compiler-rt, a
single object that also defines memset, memcpy, memmove, memcmp, bcmp,
strlen and the _chk variants. Linking it for __udivti3 made those
definitions take precedence over libc's in every glibc executable built
with SCRIPTC_CC=zigcc, and zig's bcmp and memcmp compare one byte per
iteration and its memset stores one byte at a time.

The reciprocal now divides in 32-bit halves with one-limb operations, so
the runtime references no compiler-rt builtin and glibc executables import
the four functions from libc.
2026-10-28 17:01:19 -05:00
Chris Tate 2814b57e20 Send the head writeHead stores with the response's first write or end()
res.writeHead() builds the head and keeps it until the first write(),
end() or flushHeaders(), which sends it with that call's data in one
socket write, as Node's _storeHeader does. A response with an Expect
header sends the head at once, as in Node. The http-held-head server
fixture compares the wire timing with Node's.
2026-10-28 16:35:39 -05:00
Chris Tate 6cc5345a41 Key Sets and Maps of identity-arm unions by their pointer representation
A union that is a Set element or a Map key was a tagged box in the whole
program, because the C map hashes and compares union keys by tag and
payload. One `Set<Node | undefined>` made every `Node | undefined` value
of the program a box, at every read.

A union whose arms all compare by identity (records, class instances,
arrays, functions), beside at most one undefined and one null arm, now
keeps its pointer representation in collections too. The map keys such a
union by reference: the pointer carries the identity, and NULL and the
null sentinel are two more distinct keys. Unions with a string or another
value arm keep their boxes, and library builds keep the earlier rule.

The tests of the box paths keep their unions boxed through a new
`boxedUnions` emission option instead of a Set of each union.
2026-10-28 16:22:38 -05:00
Chris Tate 3a15562085 Follow a redirect whose chunked body has not ended (#2484) 2026-10-28 16:06:56 -05:00
Chris Tateandlinguofeng 46058c67a2 Report an aborted client response's error to its 'error' listeners only
A client response cut off mid-body emitted 'error' ("aborted") whether or
not it had an 'error' listener, so a response without one ended the
process with "Unhandled 'error' event". Node's IncomingMessage reports its
destroy error only to 'error' listeners; 'aborted' and 'close' still fire.

fetch leaves a redirect response by destroying its request, so following
a redirect whose chunked body had not ended crashed the process before
fetch settled. It now follows the redirect.

Fixes #2395

Co-authored-by: linguofeng <460328+linguofeng@users.noreply.github.com>
2026-10-28 15:19:07 -05:00
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