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>
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>
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>
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.
'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.
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>
lastIndexOf built the receiver's UTF-16 index map and converted positions
both ways even when the receiver is proven ASCII, where byte offsets are
UTF-16 indexes (indexOf already has this fast path). A short id such as
"bk_00042".lastIndexOf("_") cost about 200 instructions; the Hono
fixture's list endpoint does it 80 times per request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Headers kept its name/value pairs in a string array and read every element
through scr_arr_get_ref (index checks, retain, release). forEach and the
iterators re-sorted and rescanned the whole list for every step, and every
Response or Headers built from another Headers object re-validated and
re-normalized each copied pair. The Hono fixture spends 12-15% of a request
there.
- Pair loops read borrowed elements of the dense array (sf_pair_at).
- The sort-and-combine list is cached on the Headers object and dropped by
every mutation, so live iteration keeps Node's semantics.
- get() of a single value returns the stored string.
- Copying a Headers object whose pairs are canonical (built by the
constructor or a Response) into an empty list copies the pairs; wire
response headers keep the per-pair path because they may repeat names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The EventTarget method values are cached for the program, and exit
released them only when an EventTarget, an event or a listener had
registered the unit's teardown. A program that read an AbortSignal's
addEventListener and never added a listener or made a target left the
cached functions live, and RC-audit builds exited with 99. Caching a
method value now registers the teardown too.
An AbortSignal answered addEventListener, removeEventListener and
dispatchEvent read as values with functions of its own, which only ran on
AbortSignal handles: calling one on a plain EventTarget threw
ERR_INVALID_THIS. In Node they are EventTarget.prototype's methods,
shared with every target. The signal's property read now answers the
EventTarget method values (scr_event_target_method_value), which run on
any EventTarget receiver, AbortSignal included, so
signal.addEventListener === new EventTarget().addEventListener holds.
The signal's own method table keeps throwIfAborted.
A child spawned from a listener that runs during a poll pass, such as a
'close' listener, can exit before the next pass fires its 'spawn'
listeners. The same pass's waitpid loop then settled it with 'spawn' never
emitted. Settling an exited child now fires its pending 'spawn' listeners
first, before its stream data and 'exit', as Node emits them.
Object.freeze of an array that another reference may still hold stopped at
compile time with SC2020, and freezing an array, Map or Set held in an
untyped value threw SC2020 at run time. Node freezes the object itself: a
frozen array refuses every later write through any reference with the
TypeError of the operation's first write, and a frozen Map or Set only
stops being extensible, since its entries are not properties.
- An array a program may freeze keeps a frozen bit in its storage flags.
An array type 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. The writes of those types test the bit with the
guard published arrays already use; other array types are unchanged.
- The guard's errors follow Node for every operation: splice, fill,
copyWithin, sort and reverse throw at their first write, and do not
throw when they would write nothing. Published arrays get the same
errors, and a statement splice of a published array is now guarded.
- A Map or Set records that it is not extensible, for Object.freeze, seal
and preventExtensions, typed or untyped. Object.isFrozen, isSealed and
isExtensible report it, and its entries stay writable.
- Writes to a frozen array through an untyped value are refused with the
same errors, as are writes to frozen checked arrays and objects, which
now name the property and the object as Node does.
- Programs that neither freeze nor publish arrays compile to the same IR
and LLVM.
Corpus 7406 covers typed and untyped freezes of arrays, Maps and Sets,
every array write on frozen arrays of zero, one and three elements,
comparator sorts, nested arrays, and frozen arrays that are released, and
matches Node.
Refs #1374
Co-authored-by: blurname <50226652+blurname@users.noreply.github.com>