Object.fromEntries(...) as T passed the entries through the
checked-dynamic bridge, which copies native arrays and records: an array
shared by two entries, or held elsewhere, became separate copies, and
writes through the asserted object no longer reached it. The bridge now
applies only when the entries' values are scalars or checked-dynamic
values, which pass by value or by identity; other entries keep the
exact-shape fence (SC2002). Corpus 6467 checks that entries held as
unknown keep their identity; diagnostics fixture
fromentries-aliased-entries.ts pins the fence.
The bakeability check tracks the unions it is inside as well as the
records, so a union reachable from itself through an array
(`type Tree = number | Tree[]`) reports SC1090 instead of recursing without
end. The message names both recursive records and recursive unions.
- Merge origin/main (e9fb01603); the import lines at the top of
lowerer.ts conflicted and both are kept.
- Corpus 6385 gains a wrapper whose block-local `args` alias shadows an
outer one in the same function; each call forwards its own arguments.
Externrefs a guest holds are borrowed pointers. An import's result was
kept only until the outermost call returned, which was before the export
call converted its results to JavaScript, so a result created by an
import was freed before conversion. A table or global slot released the
reference it replaced at once, although the guest may still hold it in a
local after reading the slot. The kept references are now released after
the outermost caller has converted its results (scr_wasm_leave), and a
table or global store during a call keeps the reference it replaces until
then.
Checked memories (explicit bounds checks: sanitized builds, Windows)
grew with realloc, which can move the storage that memory.buffer and its
views alias. They now reserve the address space for their largest size
(mmap on POSIX hosts, VirtualAlloc on Windows) and commit pages as they
grow, so the base never moves. Hosts without address-space reservation
grow in place only until a buffer has been handed out; after that,
growth that would move the storage fails.
Tests: webassembly-externref-lifetimes.mjs (import results through an
export, table and global references replaced by the guest and by
JavaScript), webassembly-memory-growth.test.ts (views written after
growth from JavaScript and from the guest), and translate.test.ts cases
for memargs naming memory 0 explicitly and for local.tee operand types,
which b3bcbaaf7 made consistent with validation. Under SCRIPTC_SAN=1 the
two runtime tests report heap-use-after-free without these changes.
Merges main; internal/compatibility/generated/node-v24-backlog.json is
regenerated with pnpm node-compat.
- matchInferSignature reads a fixed-tuple rest as its positions, so
(...args: [string, number]) needs two arguments, and a positional
any[] pattern parameter matches only when the function's parameter
there accepts an array, as TypeScript's assignability does
(corpus 6398).
- Merge origin/main (200fe8d22); node-v24-backlog.json regenerated.
- Merge origin/main (e1c27176a).
- matchInferSignature answers false when a function needs more arguments
than a fixed-parameter pattern passes, as TypeScript's assignability
does (corpus 6398).
- Record corpus costs for the programs this change adds.
The refusal of callback returns that the inferred result type would cut
down found the return by walking up through parentheses and await only,
so a return inside a conditional's branch or a `satisfies` expression
converted silently and lost the extended members. It now walks through
every parent whose value can be the expression's own: parentheses,
await, satisfies, non-null assertions, conditional branches, the
operands of ??, || and &&, and the right operand of a comma.
An untyped child-stream 'data' listener takes strings when a setEncoding
call on the same stream precedes it. A const alias of an access path
(`const out = child.stdout`) now resolves to that path, so a setEncoding
through one alias and a listener through another, or through the stream
itself, agree. A setEncoding on another receiver that can hold the same
stream (a let copy, a parameter, a call result) leaves the state unclear
and reports the existing SC1090. Receivers that name different members
of one root, or two const roots each initialized by a call or new, are
treated as different streams.
@types/node types the transform/flush completion callback's data as any
and a child stream 'data' chunk as any, and ChildProcess accepts
(code: number, signal: string) exit and close listeners through its
catch-all overload. None of these compiled: the any-typed completion
callback crashed the LLVM emitter, and the child listeners were refused.
The completion callback's data parameter is typed Buffer | string |
undefined while the option callback lowers. Child stream 'data' listeners
with an unknown or any chunk box it as a checked-dynamic value, taking
strings when a setEncoding statement on the same stream precedes the
listener and Buffers when the function never calls setEncoding on it;
other placements are refused. Exit and close listener parameters declared
number or string bind as checked-dynamic values, so the body sees the
null Node passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A callback passed to a generic call without a return annotation can
return values the checker unifies into a narrower result type ('return
items' beside 'return items.map((i) => ({ ...i, extra }))' infers
Item[]). Converting the extended objects into that type copied them
and dropped the added members, which Node keeps and a later wider view
reads back. Such a return is now refused by name, pointing at the
annotation that keeps the members.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1490 also took records with accessor slots out of the dyn-convertible
domain, which refused passing such a literal where `unknown` is
expected. Native APIs read these values through their accessor slots:
fileURLToPath, fileURLToPathBuffer and pathToFileURL call the `windows`
getter of an options object, so tests/corpus/node24-file-url-options.ts
stopped compiling.
Records with accessor slots convert to checked-dynamic values again.
JSON.stringify of their typed values stays refused, since the typed
serializer cannot invoke the getter.
Main split lower-exprs.ts into expressions/*; this branch's hunks move to
expressions/identifiers.ts (typed 'arguments') and
expressions/property-access.ts (static fn.name, forwarded tuple length,
'this'-parameter arity). Main removed the checker's declarationsOf,
valueDeclarationOf and signatureDeclaration helpers; the branch's uses
read symbol.declarations, symbol.valueDeclaration and
signature.declaration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A comptime record value whose union has a discriminant takes the arm its
discriminant field names, so a value that fits two arms with overlapping
shapes gets that arm's tag and layout instead of the first arm it fits.
comptimeBakeable reports a record type reachable from its own fields as
not bakeable instead of recursing without end, so such a comptime result
reports SC1090, and the message names unions and that limit.
Node passes write and end callbacks an error argument (null on success),
but the native callbacks receive none, so an inline callback that names
the parameter read undefined. It now refuses where it is written. The
stream-shims npm case compiles now that stream subclasses are values;
its remaining scenarios are deferred refusals, so it is recorded as one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A class without a constructor that extends a stream class or
EventEmitter inherits the runtime constructor, which takes no arguments
here. Direct construction with arguments already reported that, but
construction through the class's value (`new registry.upper({ ... })`)
completed the arguments against the empty inherited signature and
dropped them. Both paths now share refuseInheritedBuiltinCtorArgs, so
the class-value path reports at the first argument with the same
message.
- `Promise.all([p, q, r])` over different promise types (the tuple
overload) lowers in TypeScript through one interned async helper that
awaits the runtime combinator over the same promises (widened to
Promise<void>, a type-only view), so settlement order and rejection
handling are Node's, then reads each fulfilled entry into the tuple.
Entries keep their identity; plain scalar entries pass through, and a
checked-dynamic entry joins through the promise it holds.
- The ambient `Boolean` as an `every`/`some`/`find`/`findIndex`/`map`
callback in TypeScript is the interned one-parameter truthiness
closure `filter(Boolean)` already used, and `filter(Boolean)` accepts
checked-dynamic elements (runtime truthiness).
- `E[key]` with a runtime string key answers the named member's constant,
or undefined for a name no member carries; a numeric member's reverse
key and inherited Object.prototype names throw a named runtime refusal.
- Regex-argument `replace`/`replaceAll`/`split`/`search`/`matchAll` on a
`string | undefined` receiver take the string lowering and throw Node's
"Cannot read properties of undefined (reading 'replace')".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An object literal with get/set accessors keeps the accessors' closures in
reserved slots of its record. Since function values became convertible to
checked-dynamic values, these records passed the JSON and conversion
checks: JSON.stringify omitted the getter's property, as it omits
function-valued ones, and a copy held as `any` or `unknown` read the
getter's property as undefined and listed the slot's internal name among
its keys. Node invokes the getter in both cases.
Records with accessor slots are now outside the JSON-safe and
dyn-convertible domains, so JSON.stringify of such a value and its
conversion to `unknown` or `any` are refused at compile time. A literal
written directly into an `any` slot keeps its accessors and is unchanged.
Reading Object, Date, RegExp, Map, Set, Promise, WeakMap and WeakSet as
interned function values made every use of those globals a dynamic value,
including the receivers of member reads. The interned function models only
name, length and constructor, so `Map.prototype`, `Date.prototype.getYear`,
`Object.isFrozen` as a value, `Map["prototype"]` and `"prototype" in Map`
stopped reaching their SC2020/SC1090 fences and read undefined through the
dynamic paths instead (769 Test262 variants went from a refusal to a wrong
result).
A builtin global read for its members now keeps its member lowerings and
fences; only name, length and constructor read through the value. A member
read through an alias typed as the constructor's static side (`const D =
Date; D.prototype`) reports SC2020 before the generic field read. Passing
the value to Object or Reflect statics that reflect on or redefine its
members (`Object.hasOwn(Object, ...)`, `Object.defineProperty(Promise,
...)`, `hasOwnProperty.call(Map, ...)`) is refused; identity and the empty
enumerable key lists still compile. The interned functions answer Node's
lengths (Date 7, RegExp 2, Promise 1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A member export resolves to its binding only when the binding is declared
before the write and never reassigned, so a consumer reads the value the
member stored. A module that re-exports another module's exports through
module.exports = require("./x") now counts its own writes after the
re-exported module's: a member it adds resolves, and a member both modules
write stays refused. The exports rebinding refusal and its test now cover
the form that stays refused, exports = value without module.exports.
A null-valued JSON field bakes as null, so the fixture's document now
compiles. It keeps its purpose with a value no literal spells (1e400 parses
to Infinity), and the mixed-table corpus program covers null elements.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>