An early exit from a loop over an iterator-typed Map or Set method parks
the iterator with a second yield of the current entry. That yield read the
entry again by its index, which aborted with a map iteration error when
the loop had deleted the entry. Each step now reads the entry once into a
local that both yields answer.
`entries()` takes the typed walk only for a slot whose yield type is the
key and value pair record. Other yield types, such as
`IterableIterator<any>` or `<unknown>`, keep the checked adapter instead of
failing to compile.
EC and Ed25519 keys read and write Node 24's raw formats:
- generateKeyPair(Sync) encodings: raw-public gives the EC point,
uncompressed or, with type "compressed", compressed, or the Ed25519
public key; raw-private gives the EC scalar at the curve's size or the
Ed25519 seed.
- KeyObject.export({ format: "raw-public" | "raw-private" | "raw-seed" }),
with Node's check of the point type, and Node's error for a passphrase
with any format other than PEM and DER, which now also covers JWK.
- createPublicKey and createPrivateKey of { key, format,
asymmetricKeyType, namedCurve }, in Node's argument order, with
compressed EC points, and createPublicKey of a raw private key.
As in Node, raw-seed and every raw format of an RSA, RSA-PSS, DSA or DH
key throw ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, which generateKeyPair
passes to its callback. Invalid key data throws ERR_INVALID_ARG_VALUE.
Raw X25519, X448, Ed448 and post-quantum keys, EC scalars of 0 or at
least the curve order, and the EC point at infinity, which Node accepts,
throw a coded SC2020 error.
A global constructor read as a value in untyped code (zod's
`data instanceof Map`, `Set`, `Date` in getParsedType) resolves its name
at run time: Function, then the Error constructors, then the other
globals, each by strcmp. Every miss in the Error and global tables cost a
call, about a dozen per read.
Each table entry is now skipped unless its first byte matches the name's,
before the strcmp. The tables, their order and the values they return are
unchanged.
zod 3, z.array(Order).parse over 500 orders: 187.3M -> 183.2M
instructions per round (-2.2%).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The recursive boxability check rejected TextEncoder and TextDecoder
records, so an array or record holding a codec could not convert to
'unknown' (SC1101), although the converter boxes a codec as its own
handle at any depth. Codec records are now boxable there, and a live
view of a record or array boxes a codec field or element as its handle
too, instead of a view of the codec's internal fields.
for...of, spread and destructuring over an untyped array opened the
iterator by calling the builtin values() function through scr_dyn_call
with the array as `this`, and stepped it through the general element
path: retain the source, compute the live length, read the element with
hole handling, release the source.
An array whose iterator method is the builtin values() now gets the
native iterator that call returns directly. A step over a plain array
whose index is an own element, or past the end, answers without the
general path; no getter can run there. Holes, keys(), entries(), typed
array capsules and every other source keep the general path.
zod 3, z.array(Order).parse over 500 orders: 187.3M -> 182.2M
instructions per round (-2.7%).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the index assigns the receiver's binding (`a[(a = b, 0)] = c[0]!`),
holding the index alone left the receiver read at the store, after the
index ran, so the value landed in the replacement array. The receiver is
now held whenever the index or the value could change what it reads.
A loop with two counters closing in on each other, such as
`for (let i = lo, j = hi; i < j; i++, j--)`, checked the bounds of every
typed-array element it read or wrote at either counter. Within the body
the low counter only rises and stays below the high one, which only
falls, so every such index lies between the two starts.
Typed-array loop versions now cover these loops. The guard tests that
both starts are exact integers, that the low start is at least 0, and
that the high start is below the length of each view the loop indexes at
a counter. The fast copy then reads and writes those elements without
checks, and the checked copy runs whenever the guard fails. In the fast
copy the guard also stands for the loop's own start test.
The exact-integer test of a start is now one function, shared by the
typed-array version guards and the dense array two-counter guard. The
dense guard, which compared the round trip with `fcmp oeq`, now also
leaves a -0 start to the checked loop.
A codec stored in untyped storage (hono's StreamingApi keeps
`this.encoder = new TextEncoder()` in a JavaScript class field) had no
dynamic representation and threw SC1101 at run time. The codec records
now cross 'unknown' slots as handles of two new tags that hold the record
itself, so identity and a decoder's streaming state survive the round
trip, and checked casts unbox the same record.
The handles answer encode, encodeInto and decode (with options objects),
the encoding, fatal and ignoreBOM properties, method values with Node's
ERR_INVALID_THIS, instanceof, String() tags and JSON's {}. The unit
scr_text_codec_dyn.c links only into programs that convert a codec to or
from an untyped value (the textCodecDyn runtime feature, found with the
dgram carrier walk, now shared).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A JavaScript binding referenced by its own initializer's inference
collapses to `any` in the checker while it still lowers to a Uint8Array:
hono's etag digest declares `const nextChunk = new Uint8Array(new
ArrayBuffer(f(chunk)))` beside `chunk = nextChunk`. The typed-array
lowerings key on the checker type and declined such a receiver, and the
dynamic paths required a dyn value, so `nextChunk.set(...)` and
`nextChunk.byteLength` threw SC1090/SC2020 at run time and multi-chunk
etag digests failed.
Method calls and property reads on such a receiver now box it by
reference (the box shares the native storage, so writes stay visible) and
dispatch on the runtime kind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An element assignment whose value is an optional element read or call,
such as `out[n++] = src[n]!`, held the value in a temporary and then
evaluated the receiver and index inside the store, after the value. So
`n++` ran after `src[n]` was read, and `src[0]` was stored where Node
stores `src[1]`. The receiver and index are now evaluated first whenever
the value could change what they read: when it assigns a binding they
read, or runs code that can assign their `let` and `var` bindings or
fields. A literal, or a binding the value cannot change, is still read
at the store.
scr_dyn_object_key_set, behind every untyped property write and every
member of an untyped object literal, looked the key up as many as five
times: a linear scan for an own accessor or read-only entry, a lookup
before the prototype walk, one before the non-extensible check, one to
know whether the key was created, and one more inside the store.
The own property is now looked up once. An own data property is written
in place (an own setter still runs; a read-only or getter-only property
still refuses), and an absent key walks the prototype chain as before
and is then added by scr_dyn_obj_set, which is unchanged.
zod 3, z.array(Order).parse over 500 orders: 199.8M -> 194.1M
instructions per round (-2.8%).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tee learned a source error only through a pending read's error
steps, so a source that errored while both branches held buffered
chunks and no read was pending left the branches readable: the next read
returned a chunk where Node rejects. The tee now watches its source's
error, as the streams spec reacts to the source reader's closed promise,
and errors both branches in a microtask (resolving the cancel promise
unless both branches were canceled). A read's error steps only end the
read, and a clone of an already errored body errors both branches the
same way.
The differential and LLVM differential suites can run the same corpus
program's Node oracle at the same time in one shard. Programs whose
scratch directory or file was derived only from argv[1] collided, and
one oracle removed the paths under the other. Their names now include
the pid, as 1403 and 5567 already do. Printed output never carries the
name.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
'new TransformStream()' had no lowering, so hono's stream() and
streamSSE() helpers failed to compile their checked-dynamic paths. The
constructor without arguments now builds a native handle: its readable
side is a native ReadableStream with a zero high-water mark, and its
writable side runs the streams spec's WritableStream state machine
(getWriter, abort, close, locked; the writer's write, close, abort,
releaseLock, ready, closed and desiredSize). A write completes when the
readable side reads its chunk, close/abort/cancel propagate across the
pair, and deferred reactions take as many microtask turns as in Node.
Web ReadableStreams can also be weak collection keys (hono keys a
WeakMap by the response stream). A transformer, queuing strategies and
the WritableStream constructor keep the SC2020 refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Response.clone() and Request.clone() on checked-dynamic handles answered
'res.clone is not a function' (hono's etag middleware hashes
res.clone().body). The runtime now implements the streams spec's
ReadableStreamDefaultTee over native streams and clones the body with it,
so the original and the clone each read every chunk. A used or locked body
throws undici's TypeErrors, the clone's headers are a copy with the same
guard, and a cloned Request's signal follows the original's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Headers and Response constructors combine duplicate entries (a: x,
a: "") into one value with the separator, which can end in its space
("x, "). They marked the pairs canonical anyway, so a copy took them
unchanged where Node trims the copied value ("x,"). The constructors
now mark the pairs canonical only when every value is already trimmed;
otherwise copies normalize them pair by pair.
Every untyped property read or write in a program with dynamic-tier
classes calls a generated helper (%dyn.class.property.N). For a value that
is not a class instance, the helper still called the normalization, the
instance-bag and the static-bag tables, each of which returns the value
itself, then compared the bag with the value and finally read the key.
When every class branch of a helper tests a capsule key or a class key,
its fallthrough is still the access it was created with, and every table
that fallthrough calls has its kind exit, a value that is neither a class
capsule nor a function now gets the same access on itself at the top of
the helper. Helpers for `constructor` and `toString`, which answer
primitives and errors specially, keep their full body.
zod 3, z.array(Order).parse over 500 orders: 235.8M -> 199.8M
instructions per round (-15.3%).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A keyed read of a string-indexed record whose values are numbers or
booleans (`counts[key]` on a `Record<string, number>`, typed
`number | undefined`) allocated a heap union for every read, also when
the only consumer was `??`, `!` or a comparison with undefined. The
runtime already returns the entry's value and whether it is present;
the box came from wrapping that result.
Such a read is now a stack union source: under a projection, the read
writes the tag and bits of the declared field or index-signature entry
the key names, or the undefined arm for a missing key, into a private
stack box, and allocates nothing. This applies when every hit is a
number or boolean arm of the result union, or an inline field of the
same union, and also to an argument a callee only projects (the helper
that `x!` lowers to). Other consumers keep the heap union.
dictionary-walks allocates no union instead of 363,359 and runs 14.6%
faster; no other runtime benchmark's code changes.
Corpus 7390.
Appending an empty value to an existing header leaves the separator's
space at the end of the combined value ("x, "). Node keeps that in the
Headers object but trims it when the value is copied into another
Headers, Request or Response ("x,"). The copy fast path copied a
canonical object's pairs as they were, so the copy kept the space. Such
an append now marks the pairs as no longer canonical, and copies
normalize them pair by pair.
A class table (property bags, normalization, computed reads) tests the
receiver's capsule key against every compiled class in turn. A capsule
boxed by another class has another key pointer, and each such probe called
scr_dyn_typed_ref_is_key to compare the names. The probe now rejects it
inline when the key length or its last eight bytes differ, and calls the
out-of-line comparison only for a likely match, so capsules boxed with an
equal key elsewhere still match.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>