`rmSync?.(…)`: an imported node:fs function has a call lowering but no
value form, so lowering the guarded callee failed with "library
functions as values". The checker types the callee never-nullish, so the
guard can never short-circuit and the step is the plain call. When the
callee is never-nullish (any/unknown excluded) and has no value
lowering, mark the call's `?.` node chainHandled and re-dispatch the
plain call; it reports its own diagnostics when it has no lowering
either.
Also drops the unused isSafeToDiscard import left behind in
optional-chains.ts. Corpus: tests/corpus/optional-call-library-function.ts
(Node vs native, byte-identical).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`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>
Reading a literal key off a union whose STORED value still carries a
scalar primitive arm answers undefined: JS boxes the primitive, finds no
such property, and returns undefined, the answer the unit and
missing-field arms already give. Lowering erases flow narrowing, so a
read the checker proved unreachable (`{ url?: string } | "pending"` read
after a typeof guard as `x?.url`) still dispatches over the string arm
and turned the whole read into an SC1090 refusal.
lowerUnionKeyedRead now asks the checker whether the primitive type
declares the property; a name it does declare (`length`, `toUpperCase`)
keeps the read's own lowering or refusal. The backend's unionKeyGet
stores the join's interned undefined arm for such an arm, and the IR
validator accepts it under a literal key when the result carries an
undefined arm.
A JavaScript-source read on a dyn-convertible receiver keeps its dynamic
fallback (`dynKeyGet`): every call site already answers that way, so a
program that compiles today keeps byte-identical IR
(tests/corpus/6331-jose-root-entry reads `jws.payload` off a
`{ … } | boolean` union). The static answer only replaces a refusal.
Corpus: tests/corpus/union-primitive-arm-members.ts (Node vs native,
byte-identical). Unit: packages/compiler/test/union-primitive-arm-member-emission.test.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A function value converted to unknown on its own takes its name from the
conversion site, but one held in a record, an array, a map or another
converted value had no name there, so util.inspect printed
`[Function (anonymous)]` where Node prints `[Function: toString]`.
The function box now looks a nameless function up in a per-module table
of (entry point, name) pairs, built from the functions the module makes
closures of: each declared or lifted function's sourceName, which follows
NamedEvaluation, and for an object literal's method its key
(IrFunction.displayName). An adapter is looked up through the function it
adapts. displayName is set only in modules that box a value that can hold
a function, and the table exists only in modules that box functions.
`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.
http.request runs the ClientRequest constructor's path and method checks
before it calls a createConnection option, takes a pooled socket or dials
a socketPath, as Node does. Before, the createConnection callback had
already run, and a socketPath server had seen a connection, when the
check threw ERR_UNESCAPED_CHARACTERS or ERR_INVALID_HTTP_TOKEN.
evolving-let-socket-optional-reads.cjs ended the server's side of the
connection as soon as it was accepted and closed the server while the
client was still ending its own side. On macOS the client could then read
ECONNRESET, under Node and the compiled program alike, and each reports the
unhandled 'error' event in its own form.
The server now ends its side once the client's end arrives, and the server
closes when the client socket has closed. The program's output is unchanged.
A closure declared before the island array binding it reads, such as
`const show = () => describe(local); const local = list();`, made the
lowering predeclare the binding's slot with its declared native array type.
The island lowering then retyped the binding checked-dynamic at its first
use, so the closure's capture and the binding disagreed (SC9001). The
predeclared slot of an island handle binding is now checked-dynamic from
the start, as the binding's own declaration lowers.
Test: 6897-node-island-declared-arrays gains arrows and a function
declaration that capture an island array declared after them.
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.
assert.throws and assert.rejects with an expected object held in a
variable compare each key with the thrown error's own or inherited value,
as Node's `key in actual` check does. An error's name is on its
prototype, so { name, message, code } failed against every error.
Untyped code called a method of a compiled class instance through the
site's dispatch helper: the callback that resolves own function-valued
properties, then the helper, which normalizes the capsule, tests each
class key and unwraps the instance before calling the method.
A call of a method dispatch helper from a dynamic-tier function now
tests the receiver's capsule key against the helper's plain class
branches (a direct call of one method with the helper's arguments) and
calls that branch's method with the capsule's instance. A branch
qualifies when every subclass the normalization can rebox its key to
calls the same method, and when the callback has no branch for the key
or those subclasses, so it answers undefined for the capsule. Every
other receiver calls the callback before the arguments and then the
helper, as before. Module initializers call this way only inside loops.
A JavaScript child stream (`child.stdout`) reached through the child's
checked-dynamic box registers its listeners through the stream's own
`on`. The declarations type an unannotated 'data' listener's chunk as a
Buffer, so the listener body checked the chunk as one and threw
`TypeError: expected Uint8Array at $, got string` once `setEncoding` made
the stream pass strings, as in Node's test-child-process-default-options.
When the file calls `setEncoding` on the same access path, an inline
'data' listener registered with on, once, addListener, prependListener
or prependOnceListener now binds its unannotated chunk as a
checked-dynamic value, which holds the string or the Buffer Node passes.
Streams without a `setEncoding` call keep the Buffer-typed chunk.
Three fixes.
A function-local island handle read inside a closure, such as an island
array spread in an arrow (`() => [...local]`), resolved to the enclosing
function's binding without registering a capture, so the closure referred to
a local it does not have (SC9001). The handle is now read through
resolveLocal after its binding is retyped checked-dynamic, which captures
it like any other local.
A static island import or re-export loaded the island module in the
importer's %init prelude, after every native dependency the file imports.
A native module imported after the island package therefore evaluated
first, which reverses Node's dependency order. The load now runs in its
statement's place among the dependencies' %init calls. An island statement
the import order leaves out keeps the prelude's place.
Tests: 6897-node-island-declared-arrays gains closures that read, measure
and spread a function-local island array. node-island-import-order checks
evaluation order when an island re-export precedes a native import, and
when an island import sits between two native imports.
The SC1010 refusal of an import or re-export that resolves to type
declarations now applies only when the declaration has no runtime file
beside it (x.js for x.d.ts, x.mjs for x.d.mts, x.cjs for x.d.cts). A
relative import of `./node_modules/dependency/index.js` names a module
Node loads, and keeps its previous SC2013. The noUnusedLocals test reaches
its declaration file through a type-only re-export, since a runtime import
of a declaration-only file is now refused, as Node fails to load it.
Inside a nested function, a read of a captured handle `let` narrowed to
the handle whenever it was a member receiver. A function declared before
the first write reads the binding as `any`, so the narrowed handle reached
member reads typed `any`, and an optional read (`c?.remoteAddress`)
compiled to an unconditional SC1090 throw. js-dyn-server, whose listener
reads `server.listening` inside the callback passed to createServer, hit
the same path.
The read now narrows only when the checker types it as the handle. An
optional member read keeps the tagged value, so undefined short-circuits
and the handle arm narrows; other reads take the checked-dynamic value.
The lowerer's closureDynLets collection is a checked journal table.
Corpus evolving-let-socket-optional-reads.cjs reads and calls members
through `?.` on a captured socket before and after its first write.
Reflect.has over a target held as any, unknown or a union answered a
presence result for a primitive. It now checks the target at run time
after both arguments are evaluated and throws Node's TypeError
("Reflect.has called on non-object"). The stdlib-fence snapshot no
longer lists Reflect.has, which lowers.
process.stdin.isRaw read a flag that only the dynamic setRawMode path
set, so a direct process.stdin.setRawMode(true) on a TTY left it false.
The flag now lives with scr_process_stdin_set_raw_mode, which every
path calls, readline's raw mode included.
StringDecoder.end(chunk) with a dynamic chunk that is undefined threw
ERR_INVALID_ARG_TYPE. It now only flushes, as Node's end skips the
write for undefined. A dynamic ArrayBuffer or SharedArrayBuffer chunk
decoded its contents; write and end now throw Node's
ERR_INVALID_ARG_TYPE for it, as it is not an ArrayBufferView.
A program module that runs in a Node island loads with nodeIsland.require
instead of nodeIsland.defer, so the build-time probe never evaluates the
program's own source. Deferral and its probe stay with island packages.
A program island module ships the data files it names by their exact path
relative to its directory, including consecutive literal arguments of one
call (join(here, "fallback", "payload.txt")). A file elsewhere in the tree
with the same name no longer ships. Files the source does not name ship
when scriptc.nodeIslands.files declares them (paths or globs relative to
the package.json); a declared path without a file is SC8004.
The island module order in the lowering is cached in a Map the lowering
owns instead of a WeakMap.
After socket.setEncoding(enc), socket.pipe(destination) wrote the raw
bytes it read to the destination. It now writes the decoded strings, as
Node's pipe writes each string chunk: the destination receives their
UTF-8 bytes, and a partial character the decoder holds at EOF is written
before the destination ends.
tests/node-parallel/run.mjs runs each job's files with its own
TEST_THREAD_ID, as Node's test runner does, so test/common/tmpdir gives
concurrent files different directories instead of one shared .tmp.0.
- listeners() and rawListeners() in TypeScript of an event that no literal
emit or listener gives a tuple (one registered only under computed names)
read the untyped array, so each returned listener takes every argument a
call passes. The array was checked into zero-argument functions, which
called the listener without its arguments.
- A typed 'newListener' or 'removeListener' listener in a program with a
checked-dynamic listener of the other meta event registers through the
adapter. The meta lowering's check looked only at the listener's own
event and refused it with SC2020.