A package function declared to return its own type parameter, which also
types one of its parameters (<T extends object>(obj: T) => T), hands back
the argument or something standing in for it, and the package may track
that object's identity. Effect's Equal.byReference returns a proxy it
keeps in a WeakSet. The declaration-typed boundary cast such a result to
the argument's type and built a separate native record, so
Equal.equals(a, b) on two by-reference objects compared them by value and
printed true where Node prints false. The result's type comes from the
caller's argument, not from data the package declares, so the projection
no longer casts these calls.
The package re-export coverage test now expects three dynamic statements:
since the boundary, readRows's declared number result keeps its type, so
count - 1 in 7004-package-declared-type-reexports is native arithmetic.
Object.freeze of a record that another reference may still hold stopped at
compile time with SC2020 ("possibly-aliased value"), and freezing a record
held in an untyped value threw SC2020 at run time. Node freezes the object
itself: every later write through any reference throws a TypeError in
strict mode code and leaves the object unchanged in sloppy-mode code.
- A record a program may freeze keeps a frozen bit in its reference count
word. A shape is freezable when a typed Object.freeze names it, or, in a
program that freezes untyped values, when a value of it can be held as a
live untyped reference. Only those shapes change: their release ignores
the bit, and every write to them, keyed writes and deletes included,
checks it first.
- Object.freeze of a record held in an untyped value sets the bit and
freezes the record's checked view, so Object.isFrozen, Object.isSealed
and Object.isExtensible through that value answer for the frozen record.
A typed Object.isFrozen of a record reads the bit.
- Programs that never freeze a record compile to the same IR and LLVM.
Corpus 7404 covers typed and untyped freezes, a recursive freeze through
unknown, keyed writes and deletes, optional-field deletes, Object.assign,
tuples, and frozen records that are released or collected as cycles, in
strict mode code. 7405 covers the same writes in sloppy-mode code. Both
match Node.
Refs #1374
Co-authored-by: blurname <50226652+blurname@users.noreply.github.com>
The prototype object of a function-local class, created once per evaluation
of the class, now starts with its own non-enumerable `constructor`, the
class value of that evaluation, as every class prototype does. Reflecting
the prototype of a function-local class without prototype members exposed
an object without it: `proto.constructor` answered the base class, and
`Object.hasOwn(proto, "constructor")` and its property descriptor did not
match Node.
zod 4 defines its error message with Object.defineProperty(inst, "message",
{ get, set }). The typed-ref commit of an Error subclass read the accessor
as a data value and threw "expected string". The commit and the slot sync
now leave the slot alone and mark the property absent; typed reads and
writes of name and message call the accessor when the program reflects on
an untyped or Error-shaped target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node's constructNT reads _construct on the construction tick, so an
assignment after the constructor and before that tick replaces a scheduled
construct hook. The assignment was dropped, so the original hook ran. It
now lowers to stream.replaceConstruct, which swaps the hook while its tick
has not run and has no effect after it or on a stream without a hook.
Runtime decodeStrings and defaultEncoding values are read into locals
before the constructor evaluates the other option members, while Node
evaluates the members in order. A runtime value after a member whose value
has effects (a call or an assignment) now refuses instead of reading a
value that member could still change.
An int32 array candidate can now be a module global or a parameter, not
only a local. A number[] parameter qualifies when its function is reached
by direct calls alone (not as a value, a method or a constructor, and not
through a result-slot entry the backend calls in its place), it is never
written, and every call passes a candidate at its position; a local,
global or parameter may then be passed there as well. A global qualifies
when every function reads it only in candidate positions and every write
stores a fresh array. moduleInt32ArrayCandidates finds them by dropping,
pass after pass, the ones a function or call disqualifies.
The candidates one array can reach through calls form a group
(int32ArrayGroups): the int32 slot analysis keeps or drops a group as one,
since a store through any member writes the same array. A module with a
module namespace object keeps its globals in doubles, as member reads can
reach any global by name.
The native compiler spreads only known record shapes, from values whose
type no longer includes undefined (SC1090). runPairedWhileLoopsAsFor
copied a counter's local read through a non-null assertion, and renamed
counters by spreading nodes narrowed to several kinds. It now tests the
local against undefined and renames one node kind at a time. The pass
rewrites the same loops to the same IR.
Object.getPrototypeOf on an instance of a function-local class refused
unless its prototype members were installed at run time. A class that
declares no prototype methods or accessors (zod 4's
class Definition extends Parent {}) has nothing to install, so its shared
prototype view is returned, as is a bare Local.prototype value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A JavaScript package function lowered once per argument-type tuple
collects its local class per instance. A function hoisted above the class
statement in a later instance resolved the class an earlier instance
collected (zod 4's $constructor: "expected class constructor"). It now
takes the forward-class TDZ box unless the collected class is this
instance's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fresh array literal wrapped in parentheses or `satisfies` on the right
side of a write to a member of a typed view of the global object
(`g.cache ??= ([])`, `g.cache ||= [] satisfies number[]`) is now built as
the declared native array too, so typed reads return the same array.
globalViewSlot walks up through those wrappers to the write's right operand
and answers it, and the slot type comes from that operand's contextual type.
- A bare arrow parameter after `async` (`async x => ...`) gets parentheses
with its projected type, as `x => ...` does. Without them the rewritten
file failed its check and lost its projection (SC6102).
- A cast that starts an unbraced loop or `if` body gets no `;` guard. Only
a statement in a statement list can continue the previous line, and in
a body the `;` became the body, which moved the statement after the
loop.
- Adoption walks a tuple's elements through the live element capsules of
the tuple's view, so references package code holds to them see the
native values.
- scr_runtime.h declares scr_dyn_decl_adopt (in scr_dyn.h), which the
backend emits, so llvm-runtime-abi.test.ts finds its prototype.
- npm.test.ts gives zod3-adoption and zod3-boundary the long per-case
timeout of the other zod 3 cases; their sanitized builds compile zod's
types module too.
host.c defines _POSIX_C_SOURCE, under which Darwin's <sys/file.h> hides
flock and LOCK_EX/LOCK_UN, so the native compiler build failed on macOS
with an implicit declaration error. Define _DARWIN_C_SOURCE on Apple
targets before the system headers. Linux preprocessing is unchanged.
Any computed write with a key TypeScript cannot know (zod's
`curr[el] = ...` in ZodError.js, for one) puts every class method name
in the method census. Every typed call of such a method then went
through the observable prototype slot: a dynamic call with boxed
arguments, followed by a checked conversion of the result (for a
`Product[]` result, a deep walk of every element). Bundling untyped
JavaScript made unrelated typed code several times slower per call.
The prototype data now keeps each observed method's original value in a
global. A typed, non-virtual call of a TypeScript method still reads the
slot first, as before, and calls the method natively when the slot holds
that original value. A replacement, on the prototype or as an own
property, still takes the dynamic call. Virtual calls and methods
declared in unchecked JavaScript keep the dynamic call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fresh array literal written to a checked-dynamic slot is built as a
dynamic array. On a member of a typed view of the global object
(`(globalThis as typeof globalThis & { cache?: number[] }).cache ??= []`)
the program reads it back through that declaration, and each typed read
extracted a new native copy. `cache().push(x)` was lost, and two reads
were never the same array. Node keeps one array.
When the literal is the right side of a write (`=`, `??=`, `||=`) to a
member of the global object, and the member is declared as a native
array, build the native array and store it boxed. Every typed read then
extracts that same array, and untyped reads see it through its live
capsule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BigInt of byte storage joined the elements with "," for every kind, so a
Buffer, whose text is its UTF-8 decoding, and a DataView, whose text is
"[object DataView]", converted as typed arrays. BigInt now parses the
value's ToString, which the runtime value decides, as String does. This also
covers a Buffer held in a Uint8Array slot.
Corpus 7403 adds Buffers, a DataView, and a Buffer in a typed array slot.
Refs #1647