Commit Graph
4543 Commits
Author SHA1 Message Date
Chris Tate b8a6d3805a Islands: CommonJS island requires, builtin-class handles, JavaScript widened bindings (#1907) 2026-10-29 15:27:09 -05:00
Chris Tate 0117f39da0 pi wave 5: union layouts and reads, provider and tool records (#2547) 2026-10-29 15:25:45 -05:00
Chris Tate 87c8d14510 Test262: String/Number/Boolean wrapper objects in JavaScript sources (#2550) 2026-10-29 15:24:04 -05:00
Chris Tate d972a6bc13 Defer island modules whose recorded load is deterministic (NI-7b/7d/7e/7f) (#1906) 2026-10-29 15:18:28 -05:00
Chris Tate 43bafa67d1 Test262 group C: early errors, undeclared reads, async-iterator relaxation, compareArray harness (#2500) 2026-10-29 15:17:08 -05:00
Chris Tate ffa0753a82 Generic arrows: enclosing-frame captures (incl. mutable, boxed), optional-call arity, declared-arity refusal (#2506) 2026-10-29 15:14:11 -05:00
Chris Tate 145f8fcd5e fs.watch names the changed entry; fs.watchFile polls in its own gated unit (#2507) 2026-10-29 15:13:12 -05:00
Chris Tate 2de17f95df Pass every argument to emitter listeners that read arguments, and deliver the leak warning at exit (#2544) 2026-10-29 15:12:50 -05:00
Chris Tate 03b26a7e8e Stdio write hook: console lines pass (line, errorHandler), byte chunks pass a Buffer (#2511) 2026-10-29 15:12:31 -05:00
Chris Tate dfa940a82d Return freed memory sooner: chunked shared writes, a bounded string spare, idle trims (#2518) 2026-10-29 15:12:10 -05:00
Chris Tate f2e82c68e1 Pack jammed saturating loops one copy per lane and test their exit every fourth iteration (#2522) 2026-10-29 15:11:29 -05:00
Chris Tate 16f3c16752 Match Node's node:path behavior from Node 24's path tests (#2495) 2026-10-29 15:10:50 -05:00
Chris Tate 2d2cd25d26 Count the arguments a replaced stdio write receives (#2530) 2026-10-29 15:10:03 -05:00
Chris Tate 3c9d162a99 Test the direct index once in number-key lookups, and inline the probe in loops (#2525) 2026-10-29 15:09:43 -05:00
Chris Tate 7eda6fa880 Trim the per-chunk work of compiled object-mode stream callbacks (#2523) 2026-10-29 15:09:24 -05:00
Chris Tate 59eced7a6c Lower ReadableStream tee, pipeTo and pipeThrough in TypeScript (#2513) 2026-10-29 15:09:05 -05:00
Chris Tate 771b9a96ba Throw Node's TypeError when a nullable destructuring source is null (#2537) 2026-10-29 15:08:26 -05:00
Chris Tate a9e2cb994f Deliver the lines a chunk split after a line listener closes the interface (#2533) 2026-10-29 15:07:08 -05:00
Chris Tate a391eef92a Read and write cached regular files on the loop thread in fs streams (#2540) 2026-10-29 15:06:47 -05:00
Chris Tate 08e2b4596a Run for-in over a receiver that may be null or undefined (#2534) 2026-10-29 15:06:00 -05:00
Chris Tate 7d283cbda3 Read at the exact bigint position in fs.readSync (#2535) 2026-10-29 15:05:39 -05:00
Chris Tate 5dc4bfd6d8 Read builtins into untyped TypeScript slots as Node's values (#2556) 2026-10-29 15:05:19 -05:00
Chris Tate d19fad3999 Match Node's emitWarning, kill, chdir and userInfo behavior (#2532) 2026-10-29 15:04:59 -05:00
Chris Tate e6f6cd4b98 Release the web view of an abandoned fetch body while its transfer runs (#2570) 2026-10-29 15:04:15 -05:00
Chris Tate 42e57e4ef3 Deliver a line to a single line listener without the listener snapshot (#2531) 2026-10-29 15:03:53 -05:00
Chris Tate ce6d9d0f1a Skip the receiver bracket for adapters of receiver-free callbacks (#2564) 2026-10-29 15:03:34 -05:00
Chris Tate d67f378a50 Release the web view of an abandoned fetch body while its transfer runs
A Response body's stream gets a web ReadableStream view when a member the
fetch stream lacks is read (for example `body.tee`). The view's source holds
the stream, and the stream released the view only when that source was its
last holder. While the transfer runs it holds the stream too, so a dropped
body with a view kept a reference count of 2: the unobserved-response
discard never ran, the paused socket was never resumed, and a compressed
response dropped before HTTP EOF kept the process alive.

The stream now releases the view when only the view's source and the live
transfer hold it, and then takes the unobserved-response discard, as a
body without a view does.

The static-abandon-view fixture reads `body.tee` and `body.locked` of a
4 MiB incompressible gzip response from the new /gzip-incompressible route
and drops it. It hung until the test's timeout before this change and now
exits as Node does.
2026-10-29 13:44:32 -05:00
Chris Tate f9b024dbab Count ArrayBuffer storage in process.memoryUsage()
process.memoryUsage() reported external and arrayBuffers as 0. arrayBuffers
is now the storage of the ArrayBuffers, typed arrays, Buffers, DataViews
and SharedArrayBuffers the program holds, and external the same storage
without SharedArrayBuffers, as Node splits them. A view over existing
storage adds nothing. v8.getHeapStatistics().external_memory reads the
same figure as external.

Each ScrBytes owner records the bytes it counts in the struct's padding,
so ScrBytes stays 64 bytes and the offsets compiled code reads hold; a
static assertion pins them. Owners are counted in a per-instance counter,
because their reference counts are not atomic and they are released on
their instance's thread. Shared storage moves its owner's count to a
process-wide atomic counter, since any thread may release it.
2026-10-29 13:36:41 -05:00
Chris Tate 62943d7460 Search wrapper graphs once, brand the wrapper prototypes, read call members once
- The wrapper check before JSON, util.inspect, structuredClone and deep
  equality searched the value's graph recursively with only a depth
  limit, so an object with two references to itself took exponentially
  many calls, and a wrapper more than 64 levels deep was not found. The
  search now keeps a set of the objects it reached and an explicit stack:
  each object and array is searched once, at any depth.
- String.prototype, Number.prototype and Boolean.prototype are wrappers of
  "", 0 and false, as in Node, so their valueOf and toString answer the
  primitive instead of throwing the brand TypeError. String.prototype has
  its own `length` of 0.
- A string-named method call on a dynamic receiver in a program with
  wrappers read the member twice: once to check for the builtin
  String.prototype method and once for the call. The call now reads it
  once, and wrapper.isStringMethod checks that value.
- createsWrappers is listed with the lowerer's first-use caches that the
  unit journal leaves out.

The wrapper corpus program covers the prototypes' values, a getter read
once per call and console.log of a 40-level shared graph. The refusal
test covers a wrapper 100 levels deep, a wrapper under a shared graph and
util.inspect of a cyclic value.
2026-10-29 13:31:39 -05:00
Chris Tate 51c9e31533 Skip the receiver bracket for adapters of receiver-free callbacks
A signature adapter forwards the ambient receiver to its original, and
its closure carries the original's RECEIVER_FREE bit. Calls through the
closure's own pointer tested that bit, but the direct entry of a known
adapter target bracketed every call, because the adapter itself counts
as a receiver reader. The direct entry now tests the bit too.

The receiver readers analysis also admits more node kinds that run no
user code: optional chains, record presence and absent fields, dynamic
boxing, array for-of loops, typed array writes, typed array methods and
constructors over typed operands, the module binding check and region
intrinsics, performance.now, Atomics operations, string and number
library calls over primitives, and new Error(message, options) without
an options object.
2026-10-29 10:42:30 -05:00
Chris Tate 7aaf98b53c Check nullable destructuring sources through their unit arm
The TypeError check for a null or undefined source now reads the unit arm the value holds as a dyn null or undefined, instead of converting the whole union to dyn. A union whose data arm has no dyn form, such as a record holding a generator, compiles again instead of failing with SC9001.
2026-10-29 10:24:12 -05:00
Chris Tate 1ace0b4e88 Expect the map and record-width results the PR's lowering now gives 2026-10-29 09:14:55 -05:00
Chris Tate dbc448efe2 Merge main 2026-10-29 09:14:05 -05:00
Chris Tate 3ee8c41cbd Append long flatMap tuple arms in index order, and read omitted generic members
A flatMap callback's tuple arm appended its elements in the stored field
order, which sorts the names as strings, so a tuple of eleven or more
elements came out as 0, 1, 10, 2, …. The elements now append in index
order.

A generic receiver's member that the instantiation does not store reads
through the checked keyed read, but only for an access that is not a
write. The write test took any binary expression's left operand and any
unary operand for a write, so `options.key ?? fallback`,
`options.key === undefined` and `!options.key` still failed with SC1090.
Only an assignment's target, a delete, and `++`/`--` count as writes now.
2026-10-29 09:12:40 -05:00
Chris Tate 352842f020 Scope early errors to function bodies and class strictness, and match the upstream compareArray reads
- A function body or class static block treats its top-level function
  declarations as var-scoped: duplicates are legal in strict code, and a
  let/const/class of the same name is SC0006.
- Strictness is the code around a node: a function declaration's own
  "use strict" no longer makes its declaring context strict.
- Every part of a class is strict; object literal methods and accessors
  are strict only by their own prologue or their surroundings, and an
  arrow function's block body can carry "use strict".
- The bare compareArray reads b.length, then a.length, and a.length again
  before every element pair, converting it as the upstream `i < a.length`
  does, so a string length compares its elements.
- Tests that relied on the old handling of undeclared names now use
  constructs that are still refused or still compile: the installed-entry
  diagnostics fixture reports a syntax error (a read of an undeclared name
  is now Node's run-time ReferenceError), and static-objects-order's
  loops.js stores from its for-in head into `undefined` instead of an
  undeclared name (a store into an undeclared name refuses its package with
  SC1091).
2026-10-29 08:37:25 -05:00
Chris Tate dd927301fc Read builtins into untyped TypeScript slots as Node's values
In a TypeScript source, a builtin global used as an object literal member
(`const o: any = { j: JSON }`) failed to lower, and the member silently
became a closure that threw when called. Node reads the global itself, so
`typeof o.j` answered "function" instead of "object", two reads were never
equal, and `o.j.stringify(x)` threw "is not a function". There was no
diagnostic.

A builtin read into a slot the checker types as any or unknown now holds
the value a JavaScript source reads: the JSON, Math and Reflect namespace
objects, Buffer, and functions such as structuredClone, atob and
queueMicrotask. This covers object and array
literal members, arguments, initializers and return values. A member of a
literal whose own slot is any takes the literal's slot. Array, setTimeout
and setImmediate keep their refusal: their values lack Array's statics and
the timers' util.promisify.custom getter.

Object literal members of TypeScript sources no longer defer a failed
lowering to a runtime closure. A builtin with no value (Intl, Proxy,
parseInt, process, a typed slot) reports SC2020 at compile time.

util.inspect now prints a plain object's Symbol.toStringTag as Node does:
`Object [Math] {}`, `[Object [Y]]` past the depth limit, and
`[Object: null prototype] [N] {}`. The tag is read through the prototype
chain and getters, and it is skipped when it is empty, not a string, the
name "Object", listed as an own enumerable key, or its getter throws.
2026-10-29 07:57:38 -05:00
Chris Tate 9021a03133 Pass a shared view whole to a replaced stdout or stderr write
A shared view written to stdout or stderr went out in 64 KiB pieces through
scr_stdio_write, so a replaced process.stdout.write received the first
piece alone, and a false return, which only reports backpressure, ended the
loop: a view larger than 64 KiB lost the rest. With a write hook, the view
is now copied whole and passed in one call, as Node passes it; the bounded
pieces go to the stream itself through scr_stdio_write_raw, which never
stops early.

tests/corpus/shared-bytes-stdio-write.ts replaces process.stdout.write with
a function that records each chunk's length and returns false, and writes a
150,000-byte shared view through it.
2026-10-29 07:50:05 -05:00
Malte UblandClaude Opus 5.5 a32db356b6 See zero-argument wrapper constructions as wrapper sites
`new String()`, `new Number()` and `new Boolean()` build a wrapper over
"", 0 and false with no argument, but the program fact required an
argument, so a program whose only wrapper was one of those compiled the
wrapper and kept the primitive mapping for its slot: `s.length` or
`s.valueOf()` then failed at runtime with `expected string at $, got
object` where Node answers 0 and "". Before this slice that shape was a
compile-time refusal, so the fact has to cover it. Count a `new` of the
boxed constructors regardless of argument count; `Object()` without an
argument is a fresh plain object and keeps requiring one.

Adds the zero-argument corpus program, its order-parity baseline and its
cost entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:48 -07:00
Malte UblandClaude Opus 5.5 ad449b53ea Compile primitive wrapper objects as ordinary dynamic objects
new String/Number/Boolean and Object(primitive) refused in JavaScript.
A wrapper is now an ordinary dictionary-mode object whose prototype is
the new gated unit's String, Number or Boolean prototype, with the
[[PrimitiveValue]] slot on the object's source identity. Coercions,
property reads, method calls, key order, identity and typeof reuse the
dynamic tier; the prototype methods read the slot. Once a program
creates wrappers, the boxed types map to the dynamic value, instanceof
tests the prototype chain, Object.prototype.toString and util.types read
the slot, and the consumers that cannot read it yet (JSON, inspect,
structuredClone, deep equality) refuse a wrapper loudly. Programs without
a wrapper site are unchanged.

The npm suite's node24-text-utilities fixture builds wrappers, so that
case now runs instead of refusing with SC2020; it records the divergence
it now shows (the error formatter names a wrapper's class Object, and
function values print without their source) instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:47 -07:00
Malte UblandClaude Opus 5.5 e41fee0c4a Fill an async result from a returned Promise<never>
`return fatal(...)` where `fatal` is declared `Promise<never>` (a throw-only
async helper) is well-typed — `never` converts into anything — but the returned
value flattens through the returned-promise await into a void, which then
fails the async function's declared record result with SC2002 (`expected
'{ cancelled: boolean }', got 'void'`). The IR keeps `Promise<never>` as a
promise with a void payload, exactly like `Promise<void>`, so the two are
indistinguishable by type alone.

The return now asks the TS payload type: a `never` payload means the promise
never settles, so the awaited value is unreachable and fills the declared
result through divergentValue — the same unreachable-value path a
never-returning call already takes in a value position.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:15 -07:00
Malte UblandClaude Opus 5.5 3c5b9eadd2 Lift a record field whose function value narrows its parameters
A record width conversion copies a function-valued field when the field's
signature adapts (cleanFuncAdaptable). That gate asked coercibleValue of each
parameter pair, which excludes the checked-dynamic extraction and the
structural width the parameter conversion actually performs, so a definition
whose `execute`/`renderCall` take a concrete parameter record could not
complete a slot declaring `unknown` members — the SC2002 `field 'execute'` /
`field 'renderCall'` shape that dominates the census's tool-definition sites.

The gate now measures what funcCoerceAdapter converts: cleanParamPair for
parameters (so a callback parameter recurses through the same rule one
function type down) and cleanResult for the result (a structural width, which
carries the promise payload widening, and the void-into-unknown drop). A
recursion guard keyed in coercions.planning keeps structurally recursive
function aliases from looping, the coinductive answer recordWidthPlan already
gives its own back-edges.

A parameter that narrows a dynamic member to a record is the checked-dynamic
extraction, the same conversion a cast performs: an argument of the wrong
shape throws from the adapter rather than reading undefined, as with any
checked conversion in the language.

The gate widens admission only: a pair the old gate refused was an SC2002
error, so programs that already compiled keep byte-identical IR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:14 -07:00
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