- 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).
Chunked emission replays every numbered helper a range creates on the
merging emitter. The jsval entry thunks (dyn.jsEntryThunk) were missing
from the replayed dyn helpers, so a range that boxed a closure with a
jsval body failed to merge.
The committed runtime profile named the runtime ABI marker
scr_runtime_abi_v47, which ABI 48 renames to scr_runtime_abi_v48. The
profile's record now carries the new name; its hash and counts are
unchanged.
The ICU unit's WTF-8 to UTF-16 conversion counted units from lead bytes
alone. A 4-byte-shaped encoding of a BMP value counted two units and wrote
one, so ICU received a length past the units written, and word segments
built from those ranges could return prior heap contents. The conversion now
decodes each maximal ill-formed subsequence to U+FFFD, as the WHATWG UTF-8
decoder does, keeps WTF-8's 3-byte lone surrogates as their unit, and
reports the units it wrote. The decoder lives in scr_intl_utf16.h.
Directory entry names (readdir, opendir) and process.argv entries now decode
as UTF-8 with U+FFFD for malformed bytes, as Node decodes them, instead of
entering strings as raw bytes.
test_intl_utf16.c checks the decoder against Node's TextDecoder over
malformed sequences. intl-malformed-utf8.test.ts compares a program that
segments malformed directory names and an argument against Node.
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.
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.
Adding scr_stats_of_path and scr_stats_zero to scr_lib.c moved every
program's executable: scr_lib.o is linked everywhere, so its new symbols
and strings shifted addresses even for programs that never watch a file.
The two entry points now live in an SCR_FS_WATCH_FILE build of scr_lib.c,
the matrix's variant for programs whose feature set has watchFile, and the
poll unit calls the renamed scr_stats_poll_path. Programs that do not poll
link the default object unchanged: compiled without the define, scr_lib.o
is byte-identical to the base (verified by comparing the two objects).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A record view's slot lookup answers NULL for a field a partial view lacks
(record-views.ts), but only a program that asserts a record literal to a
wider type ever asks for such a slot. Testing for the sentinel inside the
default scr_record_view_slot put a branch on a hot accessor of every
program that links a view: the zero-cost gate found fifteen typed
programs whose executables changed with byte-identical IR and LLVM.
Build the unit twice instead, as the fs.watchFile work does for its unit.
The default object is again byte-for-byte the one it was, and the
SCR_RECORD_VIEW_PARTIAL build — the one whose slot answers NULL — is
selected by the program's link features. `recordViewPartial` joins the
runtime features, set by a `partialView` dynCheck and carried through the
link features, the executable options and the early (cache-key) features,
so a cached executable can never link the other build. The runtime pack
gains the eight builds that pair the flag with the unit's existing
dynTier, webStreams and nodeIsland combinations; every program without a
partial assertion keeps the default object.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`{ truncated: true } as TruncationResult` now keeps the literal's own
layout: the assertion builds shape A from the literal's keys with the
asserted type's field types and hands the asserted type B a partial view of
it (record-views.ts). Fields the literal never wrote are absent from the
view, so `in`, `Object.keys` and `JSON.stringify` answer exactly what Node
answers, and a genuine value of the asserted type lowers as before.
Reads, writes, destructuring and `in` tests of a field some asserted literal
leaves unwritten are checked against the view, so they cannot reach a slot
the view lacks: a read throws the catchable TypeError (Node yields
undefined, a documented divergence), and a write throws before storing
(Node would add the property).
The site scan is one lazy whole-program pass (partial-assertions.ts) that
records only the targets an object-literal `as` produces, so a program
without one lowers exactly as it did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A record literal asserted to a wider record type (`{ truncated: true }
as TruncationResult`) is a checked cast whose source shape lacks some of
the target's fields. Slice C1 of the presence-narrowing design: teach the
record-view plan, the LLVM backend and the runtime about the PARTIAL view
such a cast answers.
- `dynCheck.partialView` marks the cast; the validator requires a
`dynFrom{liveRef}` record operand whose every field is a target field
with an equal type.
- `planRecordViews` records the (target, source) pairs it cannot plan as
full views and exposes `partial`, `partialTargets` and
`partialMissing`; the cycle analysis owns these views like the rest.
- `recordViewCall` emits the `SIZE_MAX` sentinel for a target field the
source does not declare.
- `scr_record_view_slot` answers NULL for a field the viewed record does
not declare (its offset table holds `SIZE_MAX`), and
`scr_record_view_field` passes that answer through. The emitted view
guard, the clone helper and a presence test (`recordHas`) ask for that
slot and stop with `scr_trap` and the program's own message when it is
absent, so the runtime gains no string constant and no function: a new
function in a linked unit shifts the pack for every program. A program
without a partial assertion compiles to the same bytes.
- `recordHas` asks the view when the field is missing from some partial
source, and still honors an undefined-armed field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
freezableTypes seeded its worklist with `stack.push(...live)`, one entry per
untyped conversion in the program. A spread argument list is capped by the
engine's argument limit, so a large program threw `RangeError: Maximum call
stack size exceeded` while lowering, which crashed the pi coverage build.
Seed the worklist with a loop push instead.
The same spread sat on other program-sized accumulators: the diagnostics and
runtime-fence ledgers (one entry per deferred statement), the lowered function
list, the static program roots, module units and early bindings, and the
dropped-package scan results. Those push with loops too. Spreads over a fixed
list or over one node's own children stay as they are.
The co-located test seeds a 200k-entry live list, which the spread throws on,
and pins that a live record type still marks its shape freezable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`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>
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>
scr_js_wrapper.c constructs `new String/Number/Boolean` wrappers and
ToObject of a primitive as ordinary dictionary-mode objects whose
prototype is the unit's String, Number or Boolean prototype, with the
[[PrimitiveValue]] slot on the object's source identity. The unit also
answers instanceof, Object.prototype.toString, the util.types brands,
the String method receiver, and the guard of the graph-walking consumers
that cannot read the slot yet. A new `primitiveWrapper` runtime feature
(any `wrapper.` libCall) links it, so a program that never creates a
wrapper links no byte of the unit and the zero-cost gate keeps fully
typed programs clean. The `wrapper.*` signatures, symbols and dispatch
case are registered here; no frontend emits them yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The corpus program runs under Node and natively with identical output:
a getter returning a private method, private generator and async
generator methods, private fields read and written through static
accessors, a private method called through `this`, every
subclass-receiver TypeError, receivers passed in as a value, the
anonymous-class message, the absence of "#x" string keys, and
source-order initialization of public and private static fields with a
static block. The IR unit test pins that `this.#x` in a runtime static
accessor calls the brand helper and that no private name becomes a
string literal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A JavaScript class with a public static accessor gets its constructor
object at run time, but a private static member kept the whole class off
that path, so the Test262 shape `static get m() { return this.#m; }`
refused. Defining the privates on the constructor object instead would
make them string keys, readable through any subclass. Private statics
now keep their native storage (the static function and module global),
initialized in source order with the public statics, and an access
through a receiver other than the class's own name first checks that the
receiver is the declaring class object, throwing V8's TypeError
otherwise. The class-name spelling is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every new runtime entry this wave added landed in a unit that baseline
programs link (scr_lib.c's write/copyFile flags, scr_bytes.c's
string_decoder dyn forms, scr_events.c's prepend registries and
stdin.isRaw, scr_async_dyn.c's uncaught prepend, scr_net.c's server.off).
A changed unit changes its object's build-id and symbol metadata even when
every retained function is byte-identical, so 1517 corpus programs
reported a runtime change for code they never call.
Each new entry now lives where only a program that uses it can reach it:
- writeFile's `flag` option and the fs/promises twins move to
scr_fs_flags.c (feature fsFlags); copyFile's mode form reuses
scr_fs_cp.c's reflink walk, so it is that unit's SCR_FS_FLAGS build.
- string_decoder's checked-dynamic chunk forms move to
scr_bytes_strdec_dyn.c (feature strdecDyn).
- the prepend registrations, stdin.isRaw and server.off are the
SCR_PROCESS_EXTRAS builds of scr_events.c and scr_async_dyn.c and the
SCR_NET_SERVER_OFF build of scr_net.c (feature processExtras).
The prepend entry points reuse the append registration and move the new
entry to the front, so no existing function changes and every default
object stays byte-identical. The Windows copyFile arm stays beside the
Win32 path helpers in scr_lib.c, inside its _WIN32 block.
The source build (the -O1 -fsanitize=address lane) compiles these units
from their sources rather than from the prebuilt pack, so RUNTIME_DEFINES
names the same three builds the pack matrix selects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Object.getOwnPropertyNames over a record lowered through the checked-dynamic
boxing this wave added for arrays and class instances, which linked the
dynamic tier into a fully typed record walk and changed a program's binary
for no semantic gain. The static key walk is exactly Object.keys'
construction, so a record keeps it; the descriptor and symbol forms have no
static walk and still take the dyn view, as do arrays and class instances.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A stream's "data" payload arrives as checked-dynamic, and decoder.write/
decoder.end fenced it (SC2020 'StringDecoder.write of 'unknown' data'). The
dyn route reads the chunk as Node does: a string returns unchanged without
touching the buffered state, an ArrayBufferView (Buffer, TypedArray,
DataView) decodes through the existing per-encoding helpers with the
packed-f64 pending state threaded through, and anything else throws
ERR_INVALID_ARG_TYPE for "buf" with Node's message.
The two runtime entries live in scr_bytes.c beside their static twins and
share the same `StringDecoder.write`/`end` lowering, so a program that only
ever passes typed chunks keeps the existing libCalls (zero-cost).
Corpus 8428 pins the string pass-through, the utf8/base64 state across an
interleaved string chunk, and both argument-type errors against Node.
This commit also carries this wave's compatibility evidence: the fs, url
and string_decoder corpus programs are recorded in static-support.json and
the ledger is regenerated with pnpm node-compat --local.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
copyFileSync/copyFile took exactly two arguments; the third is libuv's
copyfile flag bits (COPYFILE_EXCL 1, COPYFILE_FICLONE 2, FICLONE_FORCE 4 —
fs.constants.* reads bake to literals). The surface row now carries the
F64, and both forms lower to new flag-aware runtime entries that reuse the
existing recursive-copy helper (reflink via ioctl, EXCL through open) after
Node's own range check: a mode outside [0, 7] throws the RangeError
"mode is out of range: >= 0 && <= 7".
writeFileSync/writeFile fenced every `flag` option although "w" (the
default truncate-or-create) and "wx" (O_EXCL: EEXIST when the path exists)
write the bytes the lowered path already writes. Both literal flags now
lower; every other flag still fences by name because it writes different
bytes. The exclusive form is a separate runtime entry so the open flags
stay compile-time known.
Corpus 8427 pins the sync and promise forms of both against Node,
including the EEXIST and range errors, the mode-plus-flag combination and
the async twins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reflect.has had no lowering. It is [[HasProperty]] — own and inherited
names (Array.prototype's, Object.prototype's, a class's compiled methods),
proxy `has` traps, symbol and property-key conversion — which is exactly
the `in` operator's checked-dynamic probe, so it lowers to dyn.hasKeyComputed
over the target's dyn view (and the target is fenced when it has no such
view). A primitive target is Node's TypeError, which the probe answers
false for instead: fenced by name, never silently divergent.
Object.getOwnPropertyNames/Symbols/Descriptors only accepted values that
already were checked-dynamic; a record, array or class instance now boxes
into its own dyn view (own fields and elements, in property order) like
the function and error targets already did.
Corpus 8426 pins both against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
URLSearchParams crossing an implicit ToString (`${sp}`, "" + sp, String(sp))
had no lowering: ensureString now answers sp.toString, the serialized pair
list. Record inits also accept shorthand members (`new URLSearchParams({ model })`)
like the explicit `{ model: model }` spelling.
`re.exec(s)?.index` keeps the optional chain: the read is the match row's
index/input member (the chain lowering binds the guarded receiver first),
so the questionDotToken guard no longer diverts it.
Array.from(s) over a string whose binding the checker narrowed through a
guard read as the narrowed union and fenced; the site type now decides, and
the read is coerced to that string (code points, fresh array).
Corpus 8423-8425 pin all three against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The six new differential programs' plain and sanitized measurements, in
tests/harness/corpus-costs/<program>.json so the shard balancer weighs
them without touching the shared table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`spawn(cmd, args, { detached: process.platform !== "win32" })`, the
portable daemon idiom, no longer refuses: a non-literal detached value
lowers into scr_spawn_opts's bool slot, the same POSIX_SPAWN_SETSID flag
the literal forms reach, so the child leads its own process group.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`class Sink extends EventEmitter<Map>` now compiles: the type argument of
an EventEmitter base is the listener MAP, a pure typing aid (Node erases
it too), so the base is the same emitter class whatever the map says. A
stream base's argument IS the element layout and still refuses, as does
every other generic base.
The bundled fallback `events` module declares the class with an
unconstrained map so interfaces typecheck against it, and its members stay
the untyped `...args: any[]` surface either way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
process.stdin/stdout/stderr.fd answers Node's compile-time descriptor
(0/1/2, on a TTY, a pipe, and a redirected file alike), and
process.stdin.isRaw answers the TTY's raw-mode flag through
scr_process_stdin_is_raw (1/0), or Node's undefined on a pipe-backed
stdin through the module's interned `boolean | undefined` union.
The declared type of isRaw is `boolean` (as @types/node has it), so the
read is added to isOptionalProcessStreamProperty: without that entry
maybeNarrow bridges the union to the declared arm and the runtime guard
traps on the undefined a pipe actually answers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
process.prependListener now registers at the FRONT of the signal, 'exit',
and 'uncaughtException' listener lists instead of refusing: the runtime
gained scr_signal_prepend and scr_process_prepend_exit (both memmove the
existing entries down one slot), and the uncaught-exception registry
shares one prepend implementation with the append path (scr_rej_push_at).
Families with an append-only registry ('message', 'disconnect',
'unhandledRejection', 'rejectionHandled', 'warning') and
prependOnceListener still refuse rather than reordering silently.
Server.off/removeListener unregister the most recently added matching
listener from each of the server's lists (error, close/listening,
connection/secureConnection, timeout, drop) through scr_net_server_off,
releasing the callback and its once-marker; the emit forms keep their
existing lowering.
A child handle that is a `ChildProcess | undefined` field or parameter now
takes `child?.kill(...)` / `child?.on(...)`: both were gated on a
questionDotToken that the optional-chain lowering has already consumed, so
the chain-blocked check (lowerer.chainBlocked) replaces it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pi's compatibility shims patch host objects — `process.stdout.write = fn`
to capture output, `.bind()` on a native function, `Object.defineProperty`
on a module export. A compiled binary has no host object to patch: the
natives are direct calls, so these sites keep an explicit refusal instead
of compiling to something that silently diverges from Node.
tests/diagnostics/native-shim-monkeypatching.ts pins the three shapes
(SC1090 assignment to a non-variable, SC2007 on a valued overload set,
SC2020 for defineProperty on an import), so the boundary is visible and
any future process-object model has to update it deliberately.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Module resolution is a build-time artifact, but only a string literal
counted as a static specifier: `const FS = "node:" + "fs"; import(FS)`
and `import("node:" + "path")` fell into the computed-specifier refusals.
pi's `packages/ai` env loader writes exactly that shape.
`staticString` now folds a literal, a cast or parenthesized form of one,
a `+` concatenation of those, and a `const` binding whose initializer
folds to one (depth-bounded); anything only known at run time still
returns null, so genuinely computed specifiers keep their refusal.
`import()`, `import.meta.resolve` and `createRequire`'s `resolve` share
it. A folded specifier carries no module symbol on its argument node, so
the own-module test and the import target fall back to the checker's
program import resolution.
Corpus 8502 pins both folded forms and the builtins they name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`this.cache?.warm()` refused whenever the receiver lowered to a dynamic
value: the checked-dynamic receiver path treated any `?.` step as
unhandled chain work and bailed, even inside a chain body whose guard had
already proven the receiver present. Those steps are now gated on
`chainBlocked`, the canonical "is this `?.` still unhandled" test, so the
guarded body is the plain read-and-call it already is, with `dynCall`
throwing Node's catchable "x.y is not a function" for a present but
non-callable member. The string-only and primitive-method branches drop
their raw `questionDotToken` checks the same way.
A capture the checker itself types with an absent arm (`T | undefined`,
an optional property or parameter) also refused as a "complex" use in a
logical or conditional receiver, even though the union value flows into
the surrounding operator, which handles the absent arm itself. The use
now lowers to a plain read when both the checked type and the recorded
storage carry an undefined arm; a hidden absence keeps its refusal.
Corpus 8500 and 8501 pin optional method-call receivers and
`(entries ?? []).map(...)`-shaped capture receivers against Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`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>
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>
`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>
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>
`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>
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>
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>
`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>
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>
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>
`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>
`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>
Error-taking write completion callbacks lower now (W3), so the diagnostics
corpus no longer reports SC2020 for them. The end-callback refusal it pins is
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The typed write slot, the drain probes and the run-time-typed chunk write all
live in scr_events.c, a unit every program links. Unconditionally they changed
the default object, so executables of programs that never touch the write slot
changed byte for byte (three corpus programs differed runtime-only). Typed
programs must stay byte-identical.
Keep the default object exactly as it was and put the new code behind
SCR_STDIO_WRITE_SLOT, the way SCR_STREAM_WRITE_ERRORS gates the stream unit:
the shared write body, the run-time-typed chunk entry point and Node's
anonymous write function exist only in that build, and scr_stdio_method keeps
its own body everywhere else.
The lowering sites that reach the slot (the write method as a value, its
replacement, a patched program's typed writes) now emit a distinct marker lib
call, process.stdioSlot, which is the same scr_process_stdio at run time. The
feature scan turns it, together with process.stdioWriteDyn, into the
stdioWriteSlot runtime feature, which selects the matching runtime pack variant
and defines SCR_STDIO_WRITE_SLOT for the executable. The pack matrix enumerates
the combinations with stdioExtended and nodeIsland explicitly, so each existing
variant keeps its defines and its object.
Both new lib fns join USES_TIMERS_LIB_FNS: the write callbacks they queue run
from a nextTick, and a program whose only stdio use is the write slot would
otherwise drop the callback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`process.stdout.write(data)`/`process.stderr.write(data)` with a chunk
whose type is only known at run time (`any`, or a `string | Uint8Array`
union) was refused. Lower the one-argument form to the new
`process.stdioWriteDyn` libCall, and factor the existing stdio write
method's dispatch into `scr_stdio_write_call` so both share Node's
Writable validation: a string or a Uint8Array/Buffer writes its bytes,
null throws ERR_STREAM_NULL_VALUES, and any other value throws
ERR_INVALID_ARG_TYPE, all before anything is written.
`scr_stdio_write_call` keeps its exact statements, so it is only the
call sites that change; programs that contain a JS stdio value and link
the events unit may differ (their write method's bytes).
Corpus: `tests/corpus/process-write-dynamic-chunks.ts` covers strings,
Buffer, Uint8Array, a string | Uint8Array union, a JSON field, and the
four invalid chunks with their codes and messages.
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>
`writable.write(chunk, cb)` with `cb` passed as a function VALUE lost the
completion's error: `lowerWriteCb` checked only the callback's syntax and
lowered every other callback through `lowerExprExpecting(cbArg,
funcOf([], VOID))`, which adapts a one-parameter function down to zero
parameters. The native binary therefore printed `undefined` where Node
prints `null`, or the write's own error (`ERR_STREAM_WRITE_AFTER_END` /
`ERR_STREAM_DESTROYED` for late and destroyed writes).
Lower such callbacks through a new `writable.writeEcb` libCall
`(recv, mode, chunk, encoding, cb)`: a zero-parameter callback keeps the
existing synchronous path, and a callback that declares the error (a dyn
value, or `null | Error`) is wrapped by `scr_stream_ecb_wrap` with the
compiler-built adapter that delivers the error the write completion
carries. The stream unit publishes that error only in its
`SCR_STREAM_WRITE_ERRORS` build, selected by the new `streamWriteErrors`
runtime feature (rule: any `writable.writeEcb`). Every other build
compiles `SCR_WCB_CALL` to the same plain call, so their objects are
unchanged.
The destruction path also matched Node: `afterWrite` calls `cb(null)`
even for a stream destroyed after the write was queued (Node 24
`internal/streams/writable`), so the destroyed `SCR_ST_AFTER_WRITE` tick
passes NULL rather than the stream's stored error.
Corpus: `tests/corpus/writable-error-callbacks.ts` covers success, an
erroring write, a write after end, and a write on a destroyed stream.
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 built from the checked-dynamic tree keeps the own keys its shape
does not declare, and every JS copy carries them. `%rec.capture` (the
closed-to-index-signature width reshape) rebuilt the target from the
source's DECLARED fields only, so those keys vanished with no diagnostic.
The shape that made this visible is an inline, unannotated async arrow in
a generic call:
interface T { transformHeaders?(h: H): H | Promise<H> }
async function f<O extends T>(o: O | undefined) { … }
f({ transformHeaders: async (h) => ({ ...h, x: "1" }) })
No contextual type reaches the arrow through the type parameter, so its
return slot is contextualized by tsc's own inference, which drops the
spread's index signature: the closure is typed Promise<{ x }> and its body
lowers through the checked-dynamic spread into that closed record — the
keys survive in the record's kept source view. The call site reads the
member through the declared H and adapts the function value, and that
ovf capture dropped `accept`. Node prints "accept,x".
When the source kept such a view, rebuild the target from that live view
instead of the static field list: the same validated keyed conversion the
checked record builder performs, which copies exactly the keys Node
copies. Everything else keeps the existing field-by-field copy.
A program whose functions never convert a checked-dynamic value into a
closed record cannot keep such a view anywhere, so the capture asks the
program too: one lazy scan over every lowered function settles whether a
kept source is possible at all, and only then does the helper finalize its
body with the branch. The scan looks through union arms, promise values,
and array elements; a union reached while it is already being visited
contributes nothing until the outer union settles, and an answer that
depended on such a cycle is not memoized, because another entry into the
cycle may reach the arm it could not see. Programs without such a
conversion keep the previous helper byte for byte and link no
checked-dynamic conversion code.
tests/corpus/record-source-keys-index-capture.ts pins the drop both ways:
the inline block-bodied async arrow (assigned through the await and read
into a local first) and the async method form print the caller keys on the
base compiler and match Node byte for byte with this change, while the
non-generic, expression-bodied, nested-field and sync-arrow forms are
controls that already matched on both.
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>
A Pick or Omit view of a program class is held as a live class protocol,
and generic methods called through it monomorphize against the class.
Both checks read the view from the type's alias symbol, but a view named
by a type alias (type View = Pick<Registry, "m">) carries the user's
alias instead, so it mapped to a record of erased method fields that no
instance converts to. classViewOf reads the alias declaration and sees
the view through non-generic aliases, alias chains included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>