The stale i6 branch marked lazy-traps island-only, which held while the
whole package was refused and ran in a Node island. main compiles the
package statically with a throwing stub per blocked edge, so its require()
edges still match Node through an island while an import() whose literal
specifier resolves nowhere is a deferred SC2012 fence. Record that
remaining gap the way main does instead of claiming Node parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`tests/diagnostics/spread-fences.ts` pinned the conditional-spread fence
(`...(c ? { x: 1, y: 2 } : { x: 3, y: 4 })`) and
`spread-runtime-optional.ts` pinned the `{ ...value }` spread over a
callback's runtime-optional record. Both shapes compile now, so neither
file can stay in a suite whose contract is that every program in it fails;
the differential corpus owns them instead (`8501-object-spread-optional-source`,
`8503-object-spread-conditional-shapes`), where the programs run under Node
and as a compiled binary rather than pinning a refusal.
The rest of `spread-fences.ts` already compiled before this branch, and the
fences it named that remain (an element-type mismatch, a non-tuple call
spread) are ordinary type errors, so the file goes with the fence it pinned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The conditional-spread, asserted-probe, and asserted-element-probe commits
each answered a shape that previously refused, but three paths also
re-lowered code that already compiled:
- A checked-dynamic literal's conditional source is now lowered arm by arm
only when the literal is built for a declared record target, so a literal
already built as a plain dynamic object keeps its established lowering.
- An asserted member probe counts arms the way the extraction threshold
always has, plus getter-backed arms beside an arm with no read at all: a
union whose arms all declare the member keeps the assertion's extraction.
- An asserted element probe checks the stored value's checker type before
speculatively lowering the receiver, so a non-union receiver leaves no
synthesized helpers behind.
The zero-cost gate reports no difference for corpus/7020-conditional-
spread-fields.ts, corpus/6474-index-record-spread-literals.ts,
corpus/6475-dynamic-spread-literals.ts, corpus/asserted-arm-probe.ts, and
corpus/8014-foreign-element-callbacks.ts; the new corpus programs
(8501-8504, 8510-8513, tuple-dynamic-index, record-subset-keyed-read,
union-mixed-field-write) pin the behavior both ways.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`["one"] | string[]` flowing into a `string[]` slot compiled to the
checked extraction against the array arm alone, so a tuple value threw at
runtime. Node accepts it: the value is an array, and nothing about a
`string[]` position distinguishes which arm produced it.
Let the union-width plan cover a mixed union that holds the destination
arm beside a convertible one, and place that plan before the
checked-extraction rule, so each arm converts on its tag (a tuple rebuilds,
an array copies). A union whose only non-unit arm is the destination
(`T[] | undefined`) still has nothing to convert and keeps its tested
extraction.
tests/corpus/8513-tuple-union-array.ts pins the binding, call-argument,
readonly, and single-tuple shapes against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`(x as T)["m"]` — the element-access spelling of a member probe — was not
recognized as a probe, so it went through the assertion's checked
extraction and trapped exactly like the property spelling did before
#2443. Recognize a literal-key element access as the same probe, and
answer it per arm there too.
`"m" in (x as T)` is likewise a probe of the asserted value, not a use of
the assertion: accept a union whose arms are all records or units when the
per-arm `in` answers every arm, so the optional-arm shape compiles instead
of refusing with SC1090.
A real use of the asserted member keeps the checked extraction and still
throws on a lying assertion.
tests/corpus/8512-asserted-probe-element-and-optional.ts pins the bracket
spelling, the member probe, and the throwing real use against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`(x as T).m` where T's arms disagree about `m` — one declares it as a data
field, another declares only a setter, or only a getter — fell back to the
assertion's checked extraction, which traps on the arm with no data field.
Node reads `undefined` from the setter-only arm (a read never invokes a
setter) and the getter's result from the getter-only arm.
Count an arm as declaring the member when it carries the data field or a
getter, and let a setter-only arm answer its own read as undefined instead
of bailing out.
tests/corpus/8511-asserted-probe-setter-getter-arms.ts pins both shapes
against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`"k" in make().api` folded to `true` (or refused with SC1090) as soon as
the receiver's shape decided the answer, dropping the receiver call. Node
always evaluates both operands of `in`, so the receiver still has to run,
exactly once.
Fold the static answer through a `seqExpr` for any receiver that is not
safe to discard, for the declared-field, accessor, and inherited-name
answers alike. Side-effect-free receivers keep their previous IR.
tests/corpus/8510-in-probe-calling-receiver.ts pins the once-only receiver
evaluation against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`[...maybe]` over a source the checker typed wider than the iterable it
holds at run time (`T | undefined` from an indexed read under
indexed-access strictness, an optional local) and `push(...value)` over
an untyped or widened source were SC1090 refusals. They now drain through
the checked iterator walk, validating each element into the target slot,
and throw Node's not-iterable TypeError for null/undefined.
`widenedIterableType` recognises a union of exactly one iterable arm plus
unit arms; `checkedIteratorPack` is exported so the array-method spread
path reuses the same walk.
Corpus 8504 covers `[...digit]` from a `string | undefined`, a `[...any]`
array literal, a `push(...any)`, and
`push(...(provider.x ?? []))`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A conditional spread whose arms are both populated, nested, or collide
with an earlier contributor (or a required field, or an index-signature
slot) was an SC1090 refusal. Those shapes now retry the whole literal as
a checked-dynamic own-key copy validated into the target record: the
one-entry-per-name desugar cannot express which arm runs at runtime.
The syntax fence defers so the checked-dynamic build gets the first
chance, and fires only where that declines. Inside a checked-dynamic
literal a conditional source lowers arm by arm as the dyn value whose
keys the copy reads, so a fresh arm literal is not reshaped against the
enclosing record's contextual type.
Corpus 8503 covers both arms populated with different shapes, a nested
ternary, a conditional over an earlier contributor, an index-signature
base with a conditional, and a `Headers | undefined` spread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`{ ...item }` over `unknown`, `any`, or a `Record<string, unknown>` source
was an SC1090 refusal: the record desugar cannot enumerate keys that are
runtime state. These sources now build a checked-dynamic object and
validate it into the target record, copying own enumerable keys in Node's
insertion order. Keys the target does not declare drop, matching the
existing spread stance.
Corpus 8502 covers a `Record<string, unknown>` into a typed record, an
`unknown` clone, and a merge with an explicit later field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`{ ...maybeRecord }` where the source is `T | undefined` was an SC1090
refusal when the target field had no earlier contributor. Node treats an
absent source as a no-op, so a present source now copies field by field
through the checked extraction (`x!`): the record arm copies the value,
and the absent unit arm throws the catchable TypeError instead of leaving
a required slot unset. An optional target field keeps its no-copy arm.
Corpus 8501 pins the present-source copy and the optional destination; a
new errors-harness case covers both arms of the required-field path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A union with one arm declaring `details: T | undefined` and another
declaring `details: unknown` (pi's ToolResultEvent, the runner.ts:1208
site) has no single static type every arm converts into, so reads and
writes of that field were refused with "assignment to non-variables" /
"reading 'details' on a union-typed value". TypeScript accepts the write
because the writable type of a property on a union is the UNION of the
arms' property types, so the value is legal there.
Reads now project every arm through the checked-dynamic representation:
boxing a typed value into it cannot fail, so isWidening accepts dynFrom
and the dynamic type becomes the shared read type when an arm declares
it. Writes lower the value once at its own type and let each arm convert
it — a free widening where the arm's slot holds it exactly, a checked
conversion where the slot is narrower, so a value that does not fit the
selected arm throws the catchable TypeError of the dyn->static edge where
JS would store it (the divergence a dyn value written into a typed slot
already documents).
Both paths are reached only where lowering refused before, so programs
that compile today keep their IR unchanged.
Corpus: union-mixed-field-write.ts covers the pi shape (an `unknown` arm
beside specific arms), a payload from each arm's own view, assignment in
expression position, an `undefined` payload, and a receiver that is
already the union.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`filter[resourceType]` where the receiver is a record with several
optional members and the key type is a finite union of literal keys
(`"extensions" | "skills" | ...`) refused with SC1090 whenever the
checker's answer type did not cover every declared field. That is the
common settings-record shape: the key domain names four of the members
and the rest have unrelated types.
The key type is a finite domain, so the read now dispatches over exactly
the fields it can name, comparing the key against each canonical
property spelling. The answer is the join of the reachable fields' types
— precisely the checker's answer type — and a key outside the domain
reads undefined, as a missing property does. Nothing is emitted for a
field the key type cannot name, so no widening to the full record leaks
into downstream lowering.
The dispatch is gated on the shape having no index signature, on the
access not being a call (`o[k]()` keeps its fence for inherited
methods), on the reachable fields being a proper subset, and on the join
holding an undefined arm so a smuggled key has an answer. A key domain
that names every field keeps the single keyed-read helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`t[i]` on a fixed-shape tuple where `i` is not a literal refused with
SC1090. A tuple is a positional record, so the read now dispatches over
its arity: the element at an integer index in [0, arity), and undefined
for anything else — a fraction, a negative, NaN, Infinity, or one past
the end, exactly Node's array-like read.
The answer type is the join of the element types plus undefined, the
same policy the array index read uses, so a consumer observes a miss
like any other optional element read: `t[i]!` narrows through the
existing checked-narrow path and arithmetic on a miss is NaN. The
receiver and the index are hoisted into hidden locals when they cannot
be repeated (receiver first, since JavaScript evaluates it before the
key). The chain stays inline because the arity is a compile-time
constant, unlike the lifted array helper whose length is not.
Writes keep their fence: a stored position must be a literal index.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The corpus fixture gained `mod-default.ts`, so the recorded module
evaluation order for `main.ts` must list it after `mod.ts` and before
`main.ts`. Re-recorded from the native frontend; the order-parity sweep
(1959 entries) passes against it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- child_process: the exit/close/error dispatch loops read the live
listener registry in their loop condition, so a listener that called
off/removeListener on a sibling cut the in-flight emit short. Node
copies the listener array before it emits; detach the registry
(scr_child_detach_exit/_close/_err), deliver that snapshot, release
each closure, and free the array in both platform arms.
- process: a first-class alias of stdout/stderr (`const out =
process.stdout`) keeps the stream's type but read `writableLength`
through the keyed dynamic get, which answered undefined. Fold that
read to the same synchronous-write 0 the direct lowering answers,
leaving the runtime untouched.
- fs: a `const` write-options record was inlined at the write site even
when it was mutable, so `OPTS.mode = 0o644` before the call diverged
from Node, which reads the record when the call runs. Accept only a
record whose properties are all readonly (`as const`, `Readonly<>`);
a mutable one keeps the SC2020 fence.
- Refresh the node-types-fenced snapshot: `path.win32` sub-namespaces
lower now, so the PlatformPath.sep fence is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`export default <expr>` and the anonymous default function/class have no
named binding for the dynamic namespace getter to read, so a namespace
used as a dynamic value fenced its `default` property with SC1090. They
do have storage: the module registers a `default` global keyed by its
default export symbol, which is what a default import reads. The getter
keeps that symbol per export and answers the snapshot coerced to `dyn`,
so the boxed namespace's `default` matches Node; a missing registration
still fences SC1090 and a value with no checked-dynamic form still
fences SC1101.
Corpus: 8412-module-namespace-untyped gains an `export default 42`
module and reads its boxed and typed `default`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve tests/harness/npm.test.ts by keeping main's npm-suite split and
porting this branch's island KNOWN entry and stderr frame normalisation
into tests/harness/npm-suite.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`.join(sep)` over a checked-dynamic separator fenced with SC2020. JS
substitutes the default comma only for the undefined VALUE; every other
value goes through ToString, which prints "null" for null, the digits for
a number or boolean, and throws the catchable TypeError for a symbol.
Evaluate the separator once into a hidden local, test it against the
undefined dyn kind, and answer either the comma literal or
dyn.toStringCoerce of the value.
Unlike the statically typed arms (string, and a union with an undefined
arm), a dynamic separator cannot be resolved statically, so this is the
runtime test the dynamic tier exists for.
Fixes the 2 pi sites (@anthropic-ai/sdk's node.mjs, pi's
interactive-mode.ts).
Evidence: tests/corpus/array-join-dynamic-separator.ts, run under Node and
natively in the plain and sanitized differential lanes, plus its
order-parity baseline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
analyzeJsWidenedBindings computes jsWidenedBindings for the whole program
before lowering begins, so the journal test classifies it as unjournaled
rather than a unit collection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve tests/harness/npm.test.ts by keeping main's npm-suite split and
porting this branch's island KNOWN entry and stderr frame normalisation
into tests/harness/npm-suite.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lowerer journal test requires every collection to be journaled,
checked or left out by name. islandHandleBindings is a memo keyed by
symbol and islandInstanceofHandled is unwound scratch of the lowering in
progress, so both are unjournaled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve tests/harness/npm.test.ts by taking main's npm-suite split and
porting the island path normalisation and the two island KNOWN entries
into tests/harness/npm-suite.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Object.getOwnPropertyNames over a statically typed record fenced with
SC2020 even though Object.keys over the same record lowers. Every own
property a scriptc record carries is an enumerable declared field (or an
index signature's overflow), so both surfaces answer the same key walk:
the interned %obj.keys helper for closed shapes, the declared-then-
overflow walk for index signatures, and the live checked view for a
record that kept undeclared properties of its checked source.
Fixes ~26 pi sites in typebox's guard walk (and the AST validators built
on it) plus gaxios/js-yaml reflection.
Keep the fences where the walk is not the answer: tuples and arrays (their
own names include "length" and the indices), accessor-carrying shapes
(the static walk cannot list accessor names), performance entries, X509,
Maps and class instances.
Evidence: tests/corpus/object-get-own-property-names-records.ts, run under
Node and natively in the plain and sanitized differential lanes, plus its
order-parity baseline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A direct call to an untyped method that returns `this` on every path,
used as a statement (`builder.add(part);`), boxed its receiver into a
capsule that nothing read. Such a call now runs the method's copy that
returns nothing, as the class dispatch helpers already do.
Response.clone() and Request.clone() on a value typed Response or Request
were refused: in TypeScript the fallback declarations had no clone, and
the fetch profile listed both as unsupported, so the call compiled to an
SC2020 refusal, also in JavaScript where the checker types the value. The
checked-dynamic handles already implement them (the body tees, and the
clone gets a copy of the headers with the same guard), so typed calls now
dispatch to those implementations, as text() and json() do.
The declarations gain clone(), and the fetch profile lists both as static
methods with the new static-clone fetch fixture as their differential
evidence. The surface manifest is regenerated, and the surface-manifest
probes that kept clone fenced now fence blob() and formData().
Adds the static-clone fixture and corpus programs fetch-clone.ts and
fetch-clone-untyped.mjs, with their corpus costs and preflight/order
baselines.