Object.freeze, Object.seal and Object.preventExtensions of a class
instance held in an untyped value threw SC2020 at run time. Node changes
the instance itself: writes through its typed references then throw the
read-only TypeError in strict mode code, adding properties through any
reference fails, and Object.isFrozen, isSealed and isExtensible report
the state through either reference.
- In a program that freezes, seals or prevents extensions of an untyped
value that is not a fresh literal, every class hierarchy that can be held
there gets the integrity word that static Object.freeze already uses, and
with it the existing write checks.
- The runtime applies the operation to the instance's checked view and
commits the view. The commit of such a hierarchy stores the view's
integrity state in the word (scr_dyn_class_view_integrity) and makes the
instance's property bag not extensible with it. A hierarchy without the
word still throws SC2020.
- These commits replace and remove the bag's field entries whatever their
attributes, so a sealed or frozen view does not leave stale field values
in the bag.
- The analysis behind records and arrays now starts from these calls only,
rather than from every untyped freeze the lowering emits, such as the
frozen strings array of a tagged template.
- Programs without such calls compile as before. The runtime ABI moves to
v38.
Corpus 7407 covers freeze, seal and preventExtensions through unknown of
classes and subclasses, writes through typed and untyped references and
through methods, properties added before and after the operation, and the
integrity queries, and matches Node.
Refs #1374
Co-authored-by: blurname <50226652+blurname@users.noreply.github.com>
Two kinds of number compares still went through doubles when the integers
were already at hand.
Integer typed-array elements and `charCodeAt` results are integers or NaN
(past the end). A compare of two such values, or of one and an exact
integer, all within int32 (or all within uint32), now compares their
32-bit views, which hold ToUint32 of the value, as signed (or unsigned)
integers, and folds in an ordered test of each double: NaN makes `<`,
`<=`, `>`, `>=` and `===` false and `!==` true, as before.
A sum, difference or product of two operands that always equal their
doubles, such as `2 * i + 1` of a speculated counter, has a double that is
its integer rounded once, even past 2^53. Rounding is monotonic, and the
neighbors of a safe integer are doubles, so such a value compares with an
exact safe integer as its integer does: the compare uses the i64s. A
product by a power of two never rounds, so its wide view always equals its
double.
A class declared `extends Array` (or `extends Array<T>`) was refused with
"extending classes not declared in the program ('Array')", which blocked
lru-cache at module load. An instance is now an array of the class's
element type: its struct begins with the runtime's array header, whose
reference count it shares, and the class's own fields follow it. The
class's TypeScript type maps to its element array type, so every array
operation applies to an instance, and passing it where an array is
expected passes the same object. The class's own fields, methods,
accessors and statics read the array as the class object.
super() follows the Array constructor: one number argument sets the
length (a RangeError for an invalid one), other arguments become the
elements, and the implicit constructor forwards its arguments the same
way. The runtime finds the class through the array's ext (ScrArrClass:
name, destroy, the own fields, the species constructor), so instanceof
the class (or a binding that holds it) works on static, array-typed and
dyn values, util.inspect
prints `Name(length) [ ..., field: value ]`, Object.keys lists the own
fields, and a release through any array path reaches the class's
teardown. Instances whose elements or fields can form cycles carry the
collector header and are traced.
map, filter, slice, splice, concat, flat and flatMap construct their
result through the class as ArraySpeciesCreate does: `new C(length)` with
the specification's argument, then the elements, then the length where
the specification sets it. A map to another element type retypes the
new instance when its constructor added no elements. Array.of and
Array.from called on the class construct through it. Programs that
declare no Array subclass compile as before.
A generic subclass, a subclass of such a class and `static
[Symbol.species]` are refused at compile time; a species result the
class's constructor cannot construct throws a TypeError.
String() of an array with checked-dynamic elements now uses the
runtime's String() instead of being refused.
Corpus 7720-7726, diagnostics fixture array-subclass-fences.ts.
An Error's view had no prototype or a holder of its default name and
message, so Object.getPrototypeOf(new Error()) was Object.prototype or a
fresh object. A view of an error built as an Error, whose prototype is
still the one its view starts with, now answers Error.prototype.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A class instance converted to an untyped value was boxed in a capsule of
its static class. Before any member table read it,
%dyn.class.normalize tested it against each subclass and boxed it again
at the instance's own class: two capsules per crossing for every value
of a class with subclasses, and the subclass tests on every member read.
After the dispatch tables settle, each conversion of a value whose static
JavaScript class has a normalization branch now calls
%dyn.class.boxExact:<Class>, which runs the same subclass tests in the
same order once, at the conversion. The capsule is created at the exact
class, and normalization falls through for it. TypeScript hierarchies,
classes that are never boxed, and hierarchies without subclasses are
untouched.
zod 3, z.array(Order).parse over 500 orders: 178.5M -> 174.4M
instructions per round (-2.3%).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zValidator's default failure hook serializes zod 4's ZodError with
`name` after `message` natively, and with the first `name` zod's factory
assigns ("$ZodError") when the second assignment took the typed slot.
ZodError instances are views of a JavaScript class extending Error: each
view rebuilds `message` and `name` from the error slots at fixed
positions (after `stack`, and after every other property), and the
property bag that holds the dynamically added properties copied its
committed, possibly stale, values over them.
A JavaScript Error subclass with a property bag now marks its class
(errorBagOrder), and its view calls scr_error_dyn_follow_bag after the
bag is copied: `name` and `message` move to follow the property they
followed when the bag was committed, and data values come from the
slots. TypeScript classes keep their views.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zod 4's z.record rejected every plain object natively ("expected record,
received object"): its isPlainObject reads o.constructor.prototype, which
was undefined, and asks whether that prototype has its own isPrototypeOf,
which Object.prototype answered false.
- The Object, Array and Error constructors held as values have their
builtin prototype as a non-writable, non-enumerable, non-configurable
own `prototype`; reading Function's constructor value's prototype
throws SC2020 instead of answering undefined.
- hasOwnProperty / Object.hasOwn on Object.prototype answer its builtin
members, and on Array.prototype its constructor;
Object.getOwnPropertyNames lists them in Node's order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dynMatch helper for a class target tested the capsule's class key and,
when it did not match, materialized the capsule's view and matched that.
A materialized view is a plain object, which never matches a class target,
so the answer was always false: the helper now answers false directly.
Checked casts from unknown to unions of classes no longer build a view per
non-matching arm (1,000 casts x 2,000 rounds: 255 ms -> 67 ms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The children 6611 spawned ran `process.stdout.write('ok')` and were killed
right after spawn. When a child exited before the SIGTERM took effect, Node
reported `close 0` for it and closed it after the second child, so the
expected output depended on process start-up timing.
- The program re-runs itself as the child (`main.cjs child`), which writes
`ready` and then waits. Each parent kills its child on the first stdout
data, so both closes report `null` and `SIGTERM`.
- definer.cjs runs on the first child's `close` instead of its `exit`, so
the first close always prints before the second module's output.
JavaScript erases `x!`. scriptc lowered it on a union as a checked
narrowing to the asserted type, so a value that still held null or
undefined threw a TypeError, even where it went straight into a return
value or argument whose type holds it: `return cache.get(key)!` from a
function that returns `T | null`, with null stored in the map, threw
where Node returns null, and `String(names.get(key)!)` threw where Node
returns "null".
A non-null assertion that is a returned value, an arrow function's body,
or a call or new argument now keeps the value when the destination type
is any or unknown, or holds the null or undefined the assertion removes;
the destination's coercion re-tags it. Bindings and assignments keep the
checked narrowing, because the checker narrows them to the asserted type
for later reads.
The native compiler hit this in the bytes-loop versioning analysis,
which caches null results that way (`if (shapes.has(e)) return
shapes.get(e)!;`): it refused programs whose counted loop reads or
writes several typed arrays at one masked index with SC3004, including
tsc-ts's release build, which the Node-built compiler compiled.
typed-array-masked-index-stores.ts is such a program; the LLVM emitter
self-hosting test now emits and runs it with the natively built emitter.
`x!` performs no runtime check, but the compiler extracted the asserted arm
with a checked narrow, so a value that still held undefined or null threw
"TypeError: undefined is not representable in the target union" wherever it
was used. In Node the value reaches the operator, which converts it.
When `x!` is an operand of an arithmetic, bitwise or relational operator,
`+`, a compound assignment, a unary `-`, `+` or `~`, or an argument of a
Math function, and `x` is a union of numbers, strings, booleans, null and
undefined, the operand is now converted with ToNumber: undefined is NaN and
null is 0. As an operand of an equality operator, of `!`, of a template
span, of `+` with a string, or of a comparison with a string, the union
itself reaches the operator, so `x! === 0` is false and `${x!}` is
"undefined". Array and typed-array element reads keep their handling of a
missing element, and `+` whose result TypeScript types as a string keeps
the checked narrow where Node's result is NaN.
`x == null` on a union with both null and undefined arms evaluated its
operand twice, so an operand other than a plain read was refused. It is now
evaluated once into a hidden local.
Corpus 7392.
A call argument that wraps a binding into a nullable-pointer union passes
the binding's pointer itself. The call-lifetimes analysis now treats it
like the bare binding, so a record or instance passed to a `T | undefined`
parameter that is only projected keeps its frame storage and result slot.