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 while loop over typed arrays also versioned when its only checks were
thread-owned receivers or static indices. A worker's job loop such as
`while ((job = Atomics.load(next, 0)) < count) { ...; run(job) }` then
got a second copy whose guard fails whenever the array is shared, and
the doubled call kept the job function from being inlined. A while loop
now versions only when it has an access at one of its counters. Other
while loops compile as they did before they could version.
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.
A native frontend cache entry stores the input's LLVM text after its
metadata, with an empty placeholder in the input itself. The placeholder
was an empty string where NativeExecutableInput declares an array of
parts. Under Node the reader's cast of the parsed entry is not checked,
but the native compiler checks it, so every read threw "expected array
at $.input.llvm, got string". The reader treated that as a miss: no
native build ever reused its frontend output, and an unchanged rebuild
ran the frontend, lowering and LLVM emission again.
The placeholder is now an empty array, and the cache test checks the
stored member types.
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.