`child.on("close", async (code) => …)` lowers to a closure returning
Promise<void>, and an annotated `(): number => 5` returns a number. Node
drops every listener's return value: EventEmitter ignores it, and a
returned promise still surfaces its rejection through the runtime's
unhandled-rejection tracking, which fires at settlement, not at the drop.
The listener slot is void, so the closure now adopts it with
coerceToExpected(cb, funcOf(params, VOID)) — the adapter calls the
listener and discards the answer, the shape every other void callback
slot already takes. A returned value that cannot be discarded still
refuses with SC1090.
tests/diagnostics/child-process.ts drops the annotated-listener fence
(Node drops that value too) and its snapshot is re-recorded; the new
corpus program pins the inferred, annotated and async listener shapes
against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A spread of `R | undefined` (or several record arms with null or
undefined) beside a runtime-keyed source into a declared record was
refused: the declared merge took only record and checked-dynamic
sources. Each record arm now copies through its own shape's merge
helper behind its tag test, and a null or undefined arm copies
nothing, as CopyDataProperties skips nullish sources. The source
evaluates once, in order. Record and checked-dynamic sources emit the
same code as before.
Also name the real reason a `delete` of an OPTIONAL field typed
`unknown`/`any` is refused. Such a field rides the checked-dynamic
slot, which has no ABSENT state yet (an omitted one already reads as
present), so the diagnostic no longer calls the optional field
required, and a union receiver says this instead of blaming the
receiver. The refusal stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`catch ([a, b])` / `catch ({ name, message })` was refused with SC1062.
The exception cell keeps its hidden CAUGHT local and the pattern reads
its identity-preserving checked-dynamic view (caughtToDyn), so the
pattern takes the same per-read fences an `any` source does: GetIterator
for an array pattern (positions past the end bind undefined, an iterator
with values left closes), RequireObjectCoercible plus keyed reads for an
object one. V8 spells this parameter's source '.catch' ("Cannot
destructure property 'a' of '.catch' as it is null.") and reads the
first key first when that element carries a default; both reproduced.
The promise-handler parameter that SC1062 still names keeps its fence,
and the two diagnostics corpora that pinned the clause form now pin that
handler instead.
Corpus 8603 pins array/object/nested patterns, defaults, mutation, a
per-throw rebind in a loop, and the TypeError class for a value the
pattern cannot bind.
Test262 (statements/try, its dstr subtree included): 388 variants,
157 -> 261 passes, 26 -> 20 fails, 0 pass -> fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Object.entries(value).filter(([, item]) => ...)` inside a generic body
spells the entry element as a deferred conditional over the lib overload's
own type parameter (`[string, keyof T extends never ? unknown : T[keyof
T]]`), which no bound-type-parameter resolver can decide. The array-method
entry then found no mapping for the receiver type and fenced the call with
SC2020, even though the `Object.entries` lowering answers the concrete
`[string, <value union>][]` the value really has. When the checker's
receiver type is an array that the tables cannot map, fall back to the
lowered value's array type, as the checker-untyped probe above does
(re-lowering is pure IR construction). The stdlib member gate still runs,
because the checker does type this receiver, so only calls that were
previously refused can change.
A `.filter` type predicate whose record target declares fields the
receiver element type has no slot for stays refused, but the SC1090
message now names the cause instead of a generic missing conversion: each
element was narrowed when it was STORED, so those fields do not exist at
runtime and the predicate cannot make them real. The sound paths already
work and match Node: a checked-dynamic (`unknown[]`) receiver verifies
every target field at the narrowed element, and an index-signature
receiver reads the target's fields out of its overflow.
Corpus programs:
- 8741-mixed-tuple-array-slots: `["one"] | string[]` through a
`Record<string, Mixed>` read, a `string[][]` literal, spread, a
`readonly string[]` binding, a ternary, and parameter position — every
shape Node accepts stays an array and never traps.
- 8753-filter-predicate-superset-guard: the superset guard as an inline
annotation on a checked-dynamic receiver (honest per-field tests; a
lying predicate throws the catchable TypeError instead of handing out
invented fields) and as a guard function on an index-signature receiver.
- 8768-entries-filter-generic-instantiation: `entries().filter` inside
`compact<T>`, a `const` binding with `.map`/`.some`, and a written
predicate over the same receiver.
`tests/diagnostics/filter-predicate-superset-record.ts` pins the refusal
and its new message.
Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
SharedArrayBuffer, DataView, BigInt64Array, BigUint64Array and Proxy read
as values are interned constructor values with Node's name and length.
Calling one throws Node's TypeError "Constructor X requires 'new'".
instanceof through a stored value answers as Node does: SharedArrayBuffer
and DataView test the value's kind, BigInt64Array and BigUint64Array answer
false (neither is constructed), and Proxy answers false for a primitive and
throws Node's TypeError for an object.
Intl read as a value is a namespace object with Node's 12 members in
Node's order. Its members are function values that throw the SC1090 fence
when called; direct calls such as new Intl.NumberFormat("en-US") keep
their lowerings.
TypeScript sources read the same values into any and unknown slots, so
the diagnostics program uses Atomics for a builtin without a value.
A distributed union spread evaluates its source before the dispatch, so
the properties written before the spread run after it. When the source
may have effects (a call that changes a value an earlier property reads),
those properties must now be constants; otherwise the literal takes the
checked-dynamic merge, which keeps Node's order.
A listener registered under an unrestricted event name was not recorded
in the program's event scan. A prefixed or suffixed emit with no matching
pattern listener was then lowered to `false` without dispatching, so the
any-name listener never ran. The scan now records such registrations, and
computed-name emits dispatch whenever one exists.
The same registrations can place a listener of any signature in the
bucket of a literal event. listeners(), rawListeners() and
events.getEventListeners() of a literal event returned that bucket typed
by the event's tuple, so calling an element passed the listener arguments
of other types. These calls are now refused once the program registers a
listener under an unrestricted name, as they already are for pattern
registrations.
`{ ...base, ...override }` over union-typed sources has one field set per
combination of source arms, so the merged value can carry fields from
several arms at once. When the per-arm distribution (slice 1a) cannot pick
one arm, the merge was refused with SC2003 and the site did not compile
(provider-composer's all-optional compat unions, mcp's aggregate config).
Build the object Node builds instead: the checked-dynamic literal, whose
keys are written in source order through the existing dynamic-object path
(field chunks, spreads by copy-data-properties, computed keys by
propertyKey), then converted into the declared union by the existing
checked conversion. Reflection, JSON and inspection see the keys Node
keeps; a value the union cannot hold throws the catchable TypeError at the
conversion rather than being reinterpreted as one arm. The fallback stays
narrow: union-typed spread source, auto tier, no capture buffer, and a
union the checked extraction can validate — everything that compiles
today lowers exactly as before, and `--tier=static-only` keeps the refusal
that tests/diagnostics/union-spread-merge.ts pins.
Corpus union-spread-merge.ts pins key order, last-writer-wins, nested
identity, the accepting typed consumer and the snapshot semantics
differentially; dyncheck's "union spread merge" covers the refusing
single-arm reinterpretation scriptc-only, since Node never checks a cast.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An event bus that forwards caller-chosen channels (`emitter.emit(channel,
data)`, `emitter.on(channel, handler)`, `emitter.off(channel, safe)`) refused
every call with SC2020: the static design types each literal event with one
program-wide tuple, and `'error'` and the meta events have runtime-fixed
ABIs, so a computed name needed a constant prefix or suffix.
An unrestricted computed name on a non-stream emitter now takes the
checked-dynamic receiver's existing bridge: emit lowers through
lowerDynamicEmitterEmit when the program fixes a typed tuple for that name,
else scr_emitter_emit_any (which keeps Node's `'error'` contract and the
meta events); on/once/prepend register through lowerDynamicEmitterOn or
scr_emitter_on_any, which adds runtime typed adapters for those events; off
removes by the original closure. Node's semantics hold for every name,
including the reserved ones. The one checked boundary is a typed listener
registered under a literal name receiving a payload its parameter type
rejects: that conversion throws the catchable TypeError, the bridge's
existing "types don't lie" contract. Streams keep the refusal, since their
runtime events carry fixed per-base ABIs.
Cost: zero. Literal-name sites and the program-wide event table are
unchanged (an unrestricted site adds no computed pattern), and the bridge
helpers are emitted only at the new sites. No runtime change.
Pi sites: event-bus.ts:16 and :26. Site :27 (`return () => emitter.off(...)`
in a `() => void` slot) clears its emitter call but still depends on the
separate value-returning-arrow-through-a-void-slot fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`{ ...u, k: v }` where `u` is a union of records (`{ ...msg, content }` in
a message-mapping switch, `{ ...command, id } as Cmd`, `{ ...base, entries }
as A | B`) is typed by TypeScript as one spread per arm of `u`, but the
frontend mapped the literal to the union and refused with SC2003 ("a spread
merge over union-typed sources"). The only union-source spreads that lowered
were shared-field overrides and the single-record-arm present test.
lowerDistributedUnionSpread builds the literal the way the checker types it.
The source evaluates once into a hidden local; a tag dispatch builds the
running arm's literal with that arm as the spread source, at the destination
arm the discriminant's last writer picks, else the source arm itself, else
the one arm whose keys are exactly the keys the literal writes. Every key a
contributor may write must be a field of the destination, so a genuine merge
(`{ ...http, ...patch }` where the patch can carry a stdio-only key) keeps
the refusal instead of silently dropping keys. A stored source the checker
narrowed converts through the checked re-tag first, so an excluded arm throws
the catchable TypeError.
Cost: the function runs only where lowerObjectLiteral would have refused;
the new `exact` parameter defaults to false and the new
`spreadArmOverrides` map is empty outside the dispatch, so every existing
literal lowers to the same IR.
`fix/records-literals` changes the same field-desugar capture (it adds a
`checkedSpreadSource` block around it). This commit deliberately keeps the
per-arm source as the expression `(lowerer.spreadArmOverrides.get(srcNode)
?? lowerer.lowerExpr(srcNode))` inside that capture, so the merge keeps
records-literals' `let srcLowered` plus its checkedSource block and applies
the override inside the same expression, as the design specifies.
Pi sites: execution.ts:431, agent-session.ts:3795, runner.ts:1048,
model-resolver.ts:216, sdk.ts:324, session-manager.ts:538, rpc-client.ts:577.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A top-level return in a CommonJS module compiles when declarations follow
it. When the return is taken, only code that already exists can observe the
bindings it skips: functions and classes created before the return, and
function declarations after it. A module where one of them reads a binding
declared after the return, or that assigns exports after it, still stops
with SC1090.
assert.strictEqual and assert.notStrictEqual compare a nullable number,
string or boolean (`number | null`, `string | null`) with a scalar, null or
undefined through the checked-dynamic SameValue compare, which reports
Node's failure messages. notStrictEqual of another nullable value, such as
`child.stdin`, against null or undefined tests the value's tag.
An initializer-less JavaScript `let` whose writes all agree also takes
their type when the writes are calls declared in the standard library, such
as `spawn(...)`, and at file scope. A runtime handle that a closure
captures takes the handle or undefined. Inside a nested function, a member
read on it (`c.address()`, `child.stdin`) reads the handle and throws
Node's TypeError while the binding is still undefined; other reads there
take the checked-dynamic value.
A specifier that resolves ONLY to a declaration file has no runtime module
to link: Node throws ERR_MODULE_NOT_FOUND at it. Preflight already refused
the import form (SC1010), but the re-export arm and both require arms took
the declaration file as a module-order edge. Once a declaration file is a
program module, its own imports are lowered as code — which is how a
package's own `.d.mts` files emit SC2013 for every specifier inside them
(the pi/typebox shape: 133 errors, all at import statements inside
`node_modules/typebox/build/**/*.d.mts`).
Refuse the re-export form with the same SC1010 the import form uses, and
skip declaration files as edges in both require arms. `splitFiles` gains
the matching invariant: a declaration file is never lowered as a runtime
module even if one reaches the order by another path.
Test: tests/diagnostics/declaration-only-module (SC1010 snapshot) and the
recorded preflight/order baselines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 534ec68eb13f15812376aa5126f013da693a48d6)
An unpinned generic declaration compared with `===`/`!==` takes its
signature from the other operand (`held === ident`), which clears the pi
`agent-session.ts:590` shape.
That pin alone would answer `false` where JavaScript answers `true` when
the program also holds the declaration at another signature, so a
whole-program check refuses the comparison instead. It covers the direct
form and values compared through `unknown` (the g5 silent divergence:
two instances of one declaration compared unequal), resolving dynamic
operands' provenance only when some declaration is held at more than one
signature.
- generic-functions: `identityComparisonPin` and
`checkGenericIdentityPins` (runs once at the end of lowering, when every
value instance is known); `GenericFnInfo.valueInstances` records the
instances value references produced.
- Corpus: `generic-identity-pin.ts`, `generic-identity-unknown.ts`.
- Diagnostics: `generic-identity-multiple-pins.ts`,
`generic-identity-unknown.ts`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An identity comparison (===, ==, !==, !=) between process.env and values
held from it folds to equal: every bare use of a hold reads a fresh
snapshot, while Node compares the one environment object. A comparison
with a primitive, null or undefined keeps the ordinary lowering; one
with another object is refused with SC2014.
- A binding initialized from process.env that the program reassigns is no
longer a hold, so its member writes no longer reach the live table after
the reassignment. Its declaration is refused with SC2014, since its reads
and writes before the reassignment would miss the live environment.
- surface-manifest.json lists the SC2014 fence.
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.
`const env: { HOME?: string } = process.env` (and every other annotation
or cast that moves the hold off the env record) type-checks, but the hold
in scriptc is a snapshot record taken at the initializer, so its member
reads would observe the environment as it was then while Node's hold IS
the environment object and reads the live table. Writes through such a
hold were already refused with SC2014; reads still drifted silently.
Refuse the hold where it is created: refuseHeldEnvCopyHold runs on every
variable declaration whose initializer holds process.env and reports
SC2014 at the binding when its type is not the env record (a pure
index-signature record of string | undefined, which is what process.env
maps to).
tests/diagnostics/held-env-copy-hold.ts pins the refusal for a
fixed-field annotation and an `any` hold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under the synchronous output model nothing is ever queued: Node emits
'drain' only after a write() returned false, and these writes never
return false. The process streams therefore describe a stream whose
writes complete synchronously, and the members a flush idiom probes are
constants:
- writableHighWaterMark is 65536 (Node's default for a pipe, a file and
a TTY);
- writableNeedDrain is false;
- writableLength stays 0.
`on`/`once`/`addListener`/`off`/`removeListener`/`prependListener`/
`prependOnceListener` of "drain" evaluate their receiver and listener and
register nothing: the event cannot fire, and the listener table is not
observable (listenerCount/listeners/eventNames on the process streams
stay refused).
`events.once(process.stdout | process.stderr, "drain")` lowers to
`events.pendingForever`, a fresh promise that never settles, which is
Node's behavior for a stream no write asked to drain; it holds no loop
handle, so a program awaiting it still exits.
The fallback declarations gain writableHighWaterMark, writableNeedDrain
and the event methods, so typed programs can ask.
Corpus: `tests/corpus/process-stream-drain.ts`; the diagnostics fixture
`process-stream-listener-count.ts` pins that reading the listener table
stays refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading `process.stdout.write` as a value, `write.bind(stream)`, and
`process.stdout.write = fn` now reach the runtime stream object's write
slot in TypeScript, and a program that replaces an output write method
calls its typed writes through that slot (SC8020 refuses the combination
with Node islands until H2 shares the stream object with them).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A record width copy such as Provider<"beta"> into Provider<Api> failed
on method fields whose parameter record depends on the instantiation:
TypeScript admits them by method bivariance, but the adapter only took
mechanical parameter conversions. Inside a record's field plan, such a
field now takes a checked adapter: parameters convert with the record
width plans, and union arms without a home throw the catchable
TypeError. Top-level function conversions and every pair that planned
before are unchanged.
The safe direction lifts too: record-width's method field whose parameter
record widens now plans, so its SC2002 refusal gives way to the later
honest SC1090 (a function value has no inspect lowering).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node resolves a name nothing declares at run time. In sloppy mode a WRITE
creates a property on the process-wide global object, which a native binary
has no object for, so those programs belong on the Node island route (plan
D-2/D-5). Until that routing exists, an undeclared write in a `.js` source
is refused at build time under a dedicated code, SC1091, whose text names
the island route instead of failing with a generic no-lowering message.
Reaching that refusal at all means the checker has to stop treating an
undeclared name in a JavaScript source as a compile error, so 2304, its
did-you-mean spelling 2552, and shorthand position's 18004 join the JS
relaxation set. The relaxation stays `isJsSourceFileName`-gated: TypeScript
input keeps the compile error, where an undeclared name is a bug rather
than a runtime path.
The refusal is a preflight walk over every JavaScript file, so it reports
once per write, before any code is emitted, at the span the checker used.
`isJsWriteTarget7` is shared with the lowerer's unresolved-reference fence,
which supplies the same refusal for a write that reaches lowering by a path
preflight does not model. An empty identifier is skipped: it is a parse
recovery artifact, never a name a program writes.
The JS-read half of D-2 (the name lowers to Node's catchable
`ReferenceError`) is the next commit; this one only changes what the build
says about a write.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Static semantics reject a program before it runs. Until now the compiler
borrowed that rejection from whatever tsc happened to report, and Test262's
negative-parse suites show where the borrowing stops: programs the spec
makes SyntaxErrors compile and run. This adds the two largest families the
grab-bag misses (plan §7.7 D-4, option c), each under its own preflight
code, so a checker relaxation can never remove the only rejection a
parse-negative program had:
SC0005 a statement list declares one lexical name twice
(`{ class f {} async function f() {} }`)
SC0006 a lexical name is `var`-declared in the same statement list
(`switch (1) { case 1: let x; case 2: var x; }`)
SC0007 a declaration where the grammar admits only a Statement
(`while (false) function f() {}`, `l: class c {}`,
`if (x) async function f() {}`)
frontend/early-errors.ts is a preflight walk over JavaScript sources, so a
rejected program fails where a tsc parse error fails, before any lowering.
A case block is one lexical scope across its clauses; a list's
VarDeclaredNames reach into nested blocks but never into a function or a
class body; and Annex B's allowances are honored exactly: two plain
function declarations may repeat in a sloppy block (B.3.3.1), and a plain
function may be an if/else body or a labelled statement in sloppy code
(B.3.2) — an async function, a generator or a class never gets either
allowance, and neither does any function as a loop or 'with' body.
Strictness comes from Node's own module classification (isNodeEsmFile7),
not from the checker's external-module bit: moduleDetection=force gives
every file module scope, CommonJS included, and taking that bit as
strictness cost the Annex B exemption on the annexB
function-redeclaration tests. Typed sources are untouched — their checker
diagnostics (TS2451 and friends) remain the gate, and re-reporting a
collision under a second code would only duplicate them.
tests/test262/support.mjs counts one of these codes at a location inside
the test's own source as the parse-negative match, the same standard the
helper already applies to tsc's early errors. The diagnostics fixtures pin
both families, including the strict half of SC0007 where Node refuses the
two Annex B shapes as well.
The pass only reads the AST and only refuses programs, so no typed
program's IR changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A generic arrow declared inside a function compiles to shared module
instances that took the enclosing values they read as trailing
parameters: a copy, so any binding written in the file was refused,
including a counter the arrow itself increments. When one of its
captures is reassigned, the instance is now a lifted function that
receives every capture through a closure environment, and each call site
calls a closure over its own boxes, the representation ordinary closures
use. Writes from the instance, the frame and other closures meet in the
same box, as in Node. Arrows whose captures never change keep the
trailing parameters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A function value's declared type may name fewer parameters than the
function it holds, and a later widening call then passes arguments the
declared type never spelled. JavaScript's function object carries all of
its parameters, so nothing may be dropped on that path — that would be a
silent divergence from Node.
The narrowed store is already a named refusal (a compiled function value is
one concrete native ABI: "'(string, number) => number' values where
'(string) => number' is expected"), reported at the narrowing site for a
binding and for an object-literal member slot. Record both in the
diagnostics snapshot so the refusal cannot regress into a silent drop.
Pin the shapes that DO keep every argument: a class member whose overloads
spell fewer parameters than its implementation, the member read as a value
(the read keeps the implementation's JS arity), and a value that entered
through an untyped slot and is widened back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 94d44e947c48dc0184c941d526380ebd1b1b443d)
A generic arrow or function-expression binding declared inside a function
used to be refused (SC1090) whenever its body read anything from the
enclosing frame, because its monomorphized instances are module functions
with no closure.
Give each instance the frame it needs: every value the binding reads from
the enclosing function becomes a TRAILING parameter of the instance, and
every call site (which sits in the declaring frame or in a closure over it)
passes the current value after the declared arguments. The instance body
binds the same symbol, so it resolves the reference like any parameter.
Sound only while the original binding never changes, so the fence now
covers exactly what a copy cannot stand in for: `this`, `super`,
`new.target`, `arguments`, an enclosing type parameter, an enclosing
generic scope (per-instantiation types), and any binding with a write
anywhere in the file or declared after the binding (temporal dead zone).
Corpus: withClientIdentity/withIdentity, physicalModels (captured Set),
listTwo (captured local function), runTracked (async, call from a nested
closure) and twice (two instances, different capture values). The
generic-value-bindings diagnostics fixture now pins the fences that remain
(a capture the file reassigns, and an enclosing generic scope) instead of
the never-reassigned parameter it used to.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5b23e6ccf110c66e96fad472265d7b53db0f342a)