Remove the `length` fast path. A `length` read on a value typed unknown
that the caller checks as a number goes back to the `length` property
helper the class dispatch generates, and scr_dyn_array_length is removed.
The numeric index path (scr_dyn_index_get) is unchanged, and
IrFunction.fastRead now has only the `index` value.
Indexing a value typed unknown with a number, such as `items[i]` on an
unknown[], converted the index to a string and looked it up as a property
key, and `items.length` read the property by name and boxed the result.
In a loop over an unknown[] that costs a string allocation, a key parse
and a number box per iteration.
The computed-read and `length` helpers the class dispatch generates are
now marked for a numeric fast path. A call to one of them with a number
index first asks the runtime for the element of a dyn array or a live
array capsule (scr_dyn_index_get), and a `length` read checked as a number
asks for the array's length (scr_dyn_array_length). The key string is
built and the helper runs only when the receiver is anything else, so
objects, strings, proxies, class instances, holes and out-of-range or
non-index numbers keep the keyed path.
randomFillSync accepts every TypedArray, DataView, Buffer, ArrayBuffer and
SharedArrayBuffer and returns its argument at its own type. Offsets and
sizes count elements and are checked in bytes with Node 24.22's
ERR_OUT_OF_RANGE and ERR_INVALID_ARG_TYPE errors. randomFill fills the
same buffers and calls back in a later macrotask, or synchronously when
nothing is filled, as Node does.
randomInt takes a callback in both forms, and its errors carry Node's
received values and the max or "max - min" label. Values come from a
6 KiB cache, as in Node, so a callback runs on the next tick while the
cache has data and after an asynchronous refill when it is empty.
randomUUID and the new randomUUIDv7 validate options.disableEntropyCache
and draw from a 128-UUID batch unless it is set.
A non-u8 view passed to randomFillSync no longer needs a Uint8Array
type, and the bytes callback adapters pass views of any element kind
and dyn values through.
The generated localeCompare table now agrees with Node on spellings that
tie at the primary and secondary levels. "l·" and "L·" collate as one
unit, equal to U+0140 and U+013F, through a contraction table. Precomposed
characters take their decompositions' elements after the case weights are
assigned, so U+0130 equals I + U+0307. Ligatures whose spelled-out letters
tie with them (ij and ij) get the case weight Node places them at among
their first letter's case forms, and ligatures with their own secondary
(Æ) keep their own case. The generator's check now also compares every
covered character's decompositions, case forms and ligature spellings in
four contexts. A per-thread class table lets the lazy primary comparison
find characters that need element lists with one lookup.
util.stripVTControlCharacters ran Node's ANSI pattern through the
regex replace loop, which tried the OSC branch's lazy scan at every
introducer. Text with many unterminated OSC introducers took quadratic
time (about 200 s for 100,000 of them).
The function now scans once and gives the same result as Node 24.22's
pattern: the OSC branch ends at the first terminator after ESC ], and
that position is cached across introducers; the CSI branch resolves its
first match without backtracking. A removal that brings lone surrogates
together keeps canonical storage.
Corpus programs 5470-5474 cover OSC and CSI forms, seeded random input,
long unterminated runs and surrogates, and strip-vt-linear.test.ts checks
that 400,000 unterminated introducers finish quickly.
Hash.update and Hmac.update accept every Buffer input encoding and any
TypedArray or DataView. Hex input of odd length throws Node's
ERR_INVALID_ARG_VALUE, an encoding Node does not know reads as UTF-8, and
data of other types throws ERR_INVALID_ARG_TYPE. digest() returns a
string for every Buffer encoding and a Buffer for a falsy or unknown one,
and a repeated Hmac.digest() returns Node's empty result.
createHash and hash.copy take options.outputLength for the SHAKE and
KECCAK-KMAC digests, with Node 24.22's validation and its
ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH error, including the library and
reason properties, for other digests. crypto.hash takes an output
encoding or an options object with outputEncoding (including "buffer")
and outputLength. The functionName and customization options are
validated and then refused with Node 24.22's errors. createHmac takes
TypedArray, DataView and ArrayBuffer keys and options.encoding for string
keys, and checks them in Node's order.
Calls whose encodings and options the compiler can resolve keep their
typed fast paths. Other forms go through runtime entry points that apply
Node's checks and return a string or Buffer, checked at the call's type.
A literal digest encoding for which Node returns a Buffer is refused
when the declared result is a string.
The digest compression functions start on a cache line, so their speed
no longer depends on the size of unrelated runtime code.
The gallop searches in the comparator sort held their search bounds in
long, which is 32 bits on Windows x64, so a run of more than 2^31 slots
gave a truncated bound and the merges could copy outside the array. The
bounds are now ptrdiff_t; every other index, length, offset, hint, run
length and run-stack entry in the sort was already size_t. Each gallop
result is checked against its run length (0 <= k <= length) before any
merge uses it, and a result outside the run is an internal error rather
than a copy.
A new runtime unit test calls gallop and the merge step, through test-only
entry points built with SCR_SORT_TEST, at indices past 2^32 and over runs
of more than 2^32 slots in a sparse mapping, and checks that an
out-of-range gallop result traps.
A class instance reached through an untyped or interface-typed value
printed as a plain object ({ r: 1 }); it now prints with its class's name
(Circle { r: 1 }), including subclasses and class expressions. Values
whose static class has subclasses, which were refused, print through the
same view. Maps and Sets held in untyped values printed as an empty
string; they now print as Map(n) { k => v } and Set(n) { v }.
A number or string seen as unknown lives in a dyn box. Allocating one
cleared the whole value union, although both kinds set a single payload
word, and releasing one went through the destroy queue, the generic dispose
switch and the weak-dispose hook before the node returned to the free list.
Number and string boxes now skip the union clear, and their last release
returns them to the free list directly. They hold at most one string, are
never cycle candidates and cannot be weak-collection keys, so the skipped
steps have nothing to do for them.
createHash, createHmac, crypto.hash, pbkdf2, pbkdf2Sync, hkdf and hkdfSync
accept every digest crypto.getHashes() lists in Node 24: SHA-512/224,
SHA-512/256, SHA-256/192, SHA3-224 to SHA3-512, Keccak-224 to Keccak-512,
SHAKE128, SHAKE256, KECCAK-KMAC-128, KECCAK-KMAC-256, RIPEMD-160,
BLAKE2b-512, BLAKE2s-256, SM3 and MD5-SHA1. The new runtime unit
scr_hash.c implements Keccak, RIPEMD-160, BLAKE2b, BLAKE2s and SM3, and
is registered in both runtime pack matrices and the C-compile path.
Names resolve as Node 24.22 resolves them: case-insensitively, with
OpenSSL's provider aliases and object identifiers, ending at the first
NUL, and with trailing spaces allowed except on the RSA signature
aliases. "dss1" and "DSS1" are SHA-1, and createHash and crypto.hash
accept "dss1" in any case. The compiler no longer refuses literal digest
names outside SHA-1/SHA-2 and MD5; unknown names throw Node's errors at
run time.
SHAKE and KMAC digests use their default output lengths. createHmac
rejects them with Node's OpenSSL error, pbkdf2 with "xof digests not
allowed", and hkdf with "Deriving bits failed". pbkdf2 with a key length
of 0 now fails with "Deriving bits failed" as in Node 24.22, and the
callback forms pass these failures to the callback.
PBKDF2 computes the keyed HMAC states once instead of on every
iteration, and digest contexts are cleared with memset and a compiler
barrier instead of a byte loop. The Keccak permutation, the RIPEMD-160
and BLAKE2 compression functions and the SM3 rounds are unrolled with
constant indices.
Node converts a string chunk with Buffer.from(chunk, encoding) and
returns Buffer.concat for read(n), so every byte chunk user code
receives from a non-object-mode stream is a Buffer. The runtime made
these chunks as plain Uint8Array storage. A typed write(), transform()
or 'data' callback then printed String(chunk), `${chunk}` and
chunk + "" as comma-separated bytes (97,98) instead of the text, and
read(n) and read() after unshift(string) returned non-Buffer values to
untyped code.
The chunks the stream creates now carry the Buffer brand: write() and
push() of a string (with or without an encoding), transform output given
as a string (the callback or this.push), unshift() of a string, and the
concatenation read(n) returns.
Corpus 5445 (typed callbacks, pipeline and stream/promises pipeline)
and 5446 (untyped code, Buffer.isBuffer at each boundary) check this
against Node.
String.prototype.localeCompare keeps its results and gets fast paths for
the common cases. Byte-equal strings compare equal at once. ASCII strings
compare their primary weights in one pass from a per-thread table and
read the secondary and tertiary weights only when every primary is equal.
Latin text whose marks carry no primary weight compares primaries
lazily, decoding only until the first difference. Other text, and every
tie at the primary level, uses the existing element lists, which no
longer zero their inline buffers or check capacity per element. The
backend lends both strings to the call instead of retaining them.
Array.prototype.sort and toSorted with a comparator now run V8's
TimSort (array-sort.tq) step for step in the runtime, in the generated
sort for booleans and unions with undefined, and in the checked-value
sort. The comparator is called as f(x, y) < 0 in Node's argument order,
so a comparator sees Node's calls in Node's order and an inconsistent
comparator yields Node's permutation. Undefined values, including a
union's undefined arm, are set aside before sorting and never reach the
comparator, and a checked comparator's result converts with ToNumber.
Structured inputs take fewer comparator calls than the previous merge
sort. Each thread reuses one sort buffer of up to 1 MiB.
Blob and File (globals and node:buffer exports) compile to native
handles in the new scr_blob.c, following Node 24.22's
lib/internal/blob.js and file.js. A Blob is a window over a shared,
immutable byte store plus the byte length of each part. Sources (strings,
typed arrays, DataView, ArrayBuffer, Buffer, other Blobs, Set and Map)
convert as a Web IDL sequence, with each non-buffer part converted to a
string, before the options are read, and are copied when the Blob is
built. The constructors throw Node's Web IDL TypeErrors and its
ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE and ERR_MISSING_ARGS errors,
and read and convert their options in Node's order. The type is
lowercased, or cleared when it holds a character outside U+0020..U+007E.
endings: "native" rewrites CRLF, CR and LF to the platform's line break.
File takes its name with lone surrogates replaced and lastModified as a
number (Date.now() when absent).
slice() converts its offsets as a [Clamp] long long (rounded half to
even) and keeps the parts Node's data queue keeps. text(), bytes() and
arrayBuffer() settle on the same microtask turn as in Node, which depends
on the part count. util.inspect prints Node's `Blob { size, type }` and
`File { ..., name, lastModified }` forms, with `[Blob]` past the depth
limit. String() and toString() give the class tag, JSON.stringify gives
{}, and structuredClone returns a new Blob or File over the same bytes.
instanceof works for both spellings. stream(), textStream() and the
Object.prototype members other than toString() fail at compile time.
Non-array iterables other than Set and Map throw SC2020.
Three handle-table additions support this: a structuredClone hook, a
JSON `{}` flag, and a bracketed past-depth inspect flag. scr_blob.c is an
optional unit selected by dynAsync, because the dynamic dispatch path
boxes its reads as dyn promises through scr_async_dyn.c.
Corpus 5285-5290 and a node-types fixture check the behavior against
Node.
In TypeScript, Object.values over a checked-dynamic value whose result is
only iterated with for-of, indexed, or passed to every, some, findIndex,
forEach, join or reduce now returns a native vector of the checked values.
The program never observes the array itself, so the checked-dynamic array
and, for for-of, the dynamic iterator protocol are skipped. Typed records
fill the vector through their record ops table and plain data objects
from their entries in own-key order. Other uses, and JavaScript files,
keep the checked-dynamic array.
The schema reader scanned every member name with the general string scan,
compared it with memcmp, and re-entered the recursive reader for every
value. The expected next member's name is now compared directly against
the text and its closing quote when the name has at most eight bytes and
none that JSON text escapes; other keys keep the general scan and compare
names with scr_key_equal. Number, boolean and string members and elements
are read in the record and array loops; records and arrays still recurse.
For radixes other than 2, 4, 8, 10, 16 and 32, V8 folds each 32-bit digit
chunk with `value * multiplier + part`. Node's arm64 builds fuse that
multiply-add into one rounding and its x64 builds round twice, so long
inputs such as parseInt("9007199254740993", 36) answer
1.9896986116031812e+24 on arm64 and 1.9896986116031807e+24 on x64. The
runtime followed the same split only through the C compiler's default
contraction, and the committed string oracle held one architecture's
answers: regenerating it on x64 replaced the two arm64 values, failing the
string oracle tests on macOS arm64.
scr_radix_fold now writes out each form: fma() on aarch64, and an unfused
multiply and add with contraction disabled elsewhere, so the result matches
the same architecture's Node under any contraction setting. The generator
records the architecture-dependent cases once per architecture
("parseInt.arm64" / "parseInt.x64") from a listed table and checks the
current machine's answer against Node, so the committed file is the same
whichever machine generates it. The C harness runs the cases for the
architecture it was built for.
scr_cyc_types, scr_str_scan and scr_wtf8_lone were tentative definitions.
The macOS runtime build emits tentative definitions as COMMON symbols, and
Mach-O `ld -r` writes COMMON symbols back out as commons, which
-exported_symbols_list does not demote. Every runtime-localized library
archive therefore defined them next to its declared API, failing the
library-multi M1 and M7 symbol checks on macOS. Linux clang defaults to
-fno-common, so the same declarations were ordinary definitions there and
the checks passed.
Initialize the three globals explicitly, as scr_sa and
scr_stdio_write_hook already are, and add a runtime test that compiles
every runtime source with -fcommon in the plain and worker configurations
and expects no COMMON symbols, so a new tentative definition fails on
Linux too.
OutgoingMessage now corks the message for a tick like Node 24.22, so
chunked writes in one tick (and a corked end(chunk)) go out as one chunk,
for server responses and client requests. Fetch accepts a connection
header that is a comma list of tokens, as Undici 7.29.1 does. The
crypto.constants declaration matches the Node 24.22 table, with a pin test.
The emitted JSON writers read each array element through scr_arr_state
and scr_arr_get_ref, retaining and releasing it, and appended every
bracket, comma and key through a runtime call. Arrays whose indices below
the length all hold values now serialize from their slots
(scr_arr_dense_slots) with borrowed elements; other arrays keep the
element walk. Fixed text is stored inline when it fits the buffer's spare
capacity, booleans write their literal inline, and numbers are formatted
directly into the buffer.
Pin Node v24.22.0 (commit f14cd3aa7bf9, Undici 7.29.1, OpenSSL 3.5.8) in
.node-version, Dockerfile.sandbox, the docs matrix fallback, the
compatibility pin, the fetch profile, and process.version. A node-pin test
checks that these pins agree. The Node compatibility snapshot is
regenerated from the v24.22.0 API docs; the new stream_iter chapter maps
to node:stream/iter, and the SuiteContext rows no longer match the
TestContext evidence.
Runtime and compiler changes follow Node 24.22 behavior:
- fs.read, fs.readSync, and FileHandle.read validate the position before
the zero-length return.
- fs.statfs results include frsize; Windows reads
FileFsFullSizeInformation and reports bfree and bavail separately.
- fs.utimes, futimes, and lutimes keep nanosecond precision.
- ServerResponse sets writableFinished where 'finish' is emitted and runs
end() callbacks before the 'finish' listeners while the socket is still
attached; a request read to 'end' closes before its response.
- socket.write(string, 'buffer') throws ERR_UNKNOWN_ENCODING.
- URL protocol-setter rejections keep the current URL, the host setter
no longer leaves a "/." escape when it adds a port to a "//" path, and
new URLSearchParams(null) parses "null".
- util.stripVTControlCharacters uses Node 24.22's ANSI pattern,
util.styleText accepts #RGB and #RRGGBB colors, and
util.types.isDataView has its JS name and length.
- structuredClone and AbortSignal.any use the "cannot be converted to
sequence" message.
- crypto.getCiphers, getHashes, and constants match Node 24.22.
- dns.lookup, net.connect, and tls.connect throw ERR_INVALID_ARG_VALUE for
hosts with null bytes.
- fs.readFile validates options.buffer and refuses its valid forms;
httpValidation and certificateCompression refuse by name.
- url.domainToASCII, url.domainToUnicode, and url.parse hostnames follow
ada 4.0: ASCII domains are lowercased without checking their xn--
labels, the RFC 5893 rule checks how LTR labels end and how RTL labels
start, a Punycode label whose decoded form begins with "xn--" fails, and
ToUnicode decodes only labels that pass the ToASCII checks. The url
legacy test cases are regenerated with Node 24.22.
Corpus programs 5275-5279 cover the new behavior. 1640, 2283, 2599, and
3136 are updated where they pinned Node 24.15 output.
Typed and JavaScript classes can extend the global EventTarget and Event.
%EventTarget and %Event are runtime-provided hierarchy roots like the
EventEmitter: a subclass instance embeds a prefix pointing at the engine
state that super() creates (the target's listener core, the event's
state), and the backend's emitter prefix handling becomes a table of
runtime prefixes that covers all three bases.
The instances' checked-dynamic capsules use the runtime RC adapters, so
the engine recognizes them: a target instance is its listeners' `this`
and its events' target, and an Event subclass instance is the event its
listeners receive, with its own fields and methods. The inherited
methods and accessors lower directly on typed receivers (including
super.dispatchEvent) and answer through the class property dispatch and
the runtime's capsule hooks for checked-dynamic values, with Node's
util.inspect output, @@toStringTag and leak-warning class name. Members
read as values are the prototype's functions, also for plain handles.
A target keeps its own capsule in its state, so repeated dispatches reuse
it; the state and an event's target are traced, so the collector
reclaims targets whose listeners capture them. Subclass members that
spell an inherited member are refused, since the engine would never call
or read them.
Listener objects that are class instances now work: when a program can
register EventTarget listeners and declares a class with a handleEvent
method, %main installs a compiled `listener.handleEvent` reader that the
engine uses at every dispatch, so the class's own, inherited or
overridden method runs with the instance as `this`, as in Node. An
unannotated arrow in an object literal whose contextual type maps to a
checked-dynamic slot or to no lowered type (an EventTarget subclass's
listener union) no longer adopts the void callback slot's return type,
so `{ handleEvent: (e) => n++ }` passed as a listener keeps the result
its record field promises.