A global replace or replaceAll of a regex literal called the compiled
matcher once per match: each call set up its frame and seek again, and the
runtime then copied the gap and the replacement. A substitution of a
literal inside the compiled subset with an ASCII replacement without $
patterns now gets a replacer built for that pattern and replacement. It
appends each gap and the replacement to the output and continues the
search after the match, as the runtime's loop does (one past an empty
match), without returning. Gaps of up to 16 bytes are one 16-byte copy,
and the search after a match is a 16-byte scan instead of a memchr call.
scr_regex_replace_with runs the replacer on an ASCII subject; a subject
outside ASCII keeps the runtime's loop. Plain-text patterns, which the
runtime searches as strings, get no replacer.
New corpus program: 8351-regex-literal-replacers.ts.
The one-character strings of U+0080..U+07FF that string iteration shares
between threads were allocated from the calling thread's memory. On Linux a
worker that exits without sharing anything returns that memory, so the table
kept strings that a later read found discarded. They now come from the system
allocator, as other state that outlives a thread does.
Untyped code that loops over a native array (`for (const ch of checks)` in
zod, over arrays the compiled code built) paid for the loop before its
first element:
- Every check of whether a typed capsule is a generator compared its type
name against "async-generator<" with strncmp, and native array type
names also start with 'a'. Iterator creation made several such checks.
The test now looks at the second byte before comparing.
- scr_dyn_iterator called the builtin values() through the general call
path for a native array; it now creates the native iterator directly,
as it already did for untyped arrays.
- Read sites of `length` and `next` call scr_js_get_probe_native, which
answers a native array's `length` and a native iterator's `next`, where
the generic read built the array's checked view or looked the method up
by name. Other read sites keep scr_js_get_probe, so their receivers that
are not ordinary objects still miss without a further test.
The cases that decrypt with a wrong passphrase used keys encrypted
afresh on each run. A wrong key decrypts to random bytes, and about one
run in two hundred those bytes pass the padding check, so OpenSSL fails
at the ASN.1 step with a longer error than "bad decrypt" and the
program's output changes. The section that reads encrypted keys now
uses fixed encryptions of one P-256 key, so each case fails the same
way on every run.
The inline dyn release sorted a surviving value into three groups by
kind: arrays and objects tested the candidate-buffer word and then called
scr_dyn_release_survivor, capsules and handles called it directly, and
functions and proxies appended to the candidate buffer inline. All six
kinds now take one path: a value already in a candidate buffer, or never
one, returns inline, and scr_dyn_release_survivor, which now also covers
functions and proxies, decides the rest. Behavior is unchanged, and each
inlined release site keeps one kind test and one call.
Effect's executable text is 10% smaller and it runs 5.8% faster; zod
runs 3.8% faster, and bench:runtime's dynamic-properties 14% faster.
A thread waiting for its mailbox slept on a condition variable. The
kernel then places the woken thread without a hint, so with every CPU
busy it can queue behind a running thread until that thread is preempted
or blocks: a worker program's phase barrier (post to each worker, wait
for every reply) lost about 1 ms per phase, and up to 5 ms, on 4 CPUs.
On Linux the wait now sleeps in ppoll(2) on the mailbox's wake pipe,
which every post already writes. A pipe write is a synchronous wakeup:
the woken thread can run on the poster's CPU, which the poster frees
when it waits in turn. Bytes left by signals whose events were already
taken are drained under the lock first. Other platforms keep the
condition variable.
for-of, spread, Array.from, yield* and [Symbol.iterator]() over a string
walked a UTF-16 index. Each step mapped the index to a byte offset, built
the character, and measured its UTF-16 length to advance.
They now walk a byte cursor over the WTF-8 data:
- The cpAtByte intrinsic reads the character that starts at the cursor.
ASCII bytes take the one-character table inline; other characters call
scr_str_cp_at_byte. byteLength advances the cursor by the character's
byte count. A lone surrogate is its own three-byte character, so it
stays a one-unit character, as in Node.
- Spread and Array.from of a string call scr_str_code_points, which
counts the characters, allocates the array once and fills it.
- The one-character strings of U+0080..U+07FF are made immortal on first
use and shared, as the ASCII ones are static.
The cpAt intrinsic, which no lowering emits any more, is removed.
String storage can hold bytes that are not well-formed UTF-8: an HTTP
header value keeps the raw bytes a client sent. Encoding such a string
read past its end.
- scr_bytes_next_cp decoded any high byte as the lead of a sequence and
read up to three bytes after it without a bound. It now takes the
storage length, decodes only complete, well-formed sequences, and
returns any other byte as U+0000..U+00FF, advancing by one. That is
each such byte's value in Node, which decodes header values as latin1.
This covers latin1, ascii, binary and utf16le encoding, hex and base64
of non-ASCII text (which narrow through latin1), Buffer#write and
TextEncoder#encodeInto.
- base64 decoding read one byte past its input when the eight-character
groups ended exactly at the end.
The runtime's bytes unit test (built with ASan) encodes ill-formed
storage at every offset within a word and random high bytes, and
compares latin1, ascii and utf16le with a scalar reference. A corpus
program sends such header bytes to a server and encodes them as Node
does.
A program's nursery trigger starts at 256 candidates and doubles, up to
4096, after each scheduled mature or full pass whose dead set held more
mature and old objects than an eighth of what the nursery passes freed
since the previous one; it halves after one that held less than a
thirty-second of that. SCR_CYCLE_THRESHOLD and
scr_cyc_set_nursery_threshold pin the trigger as before. The
scheduled-age rule keeps counting collection points in the pinned or
default trigger.
An intersection that restates an optional member as required with a
separately written element type (`Opts & { files: Array<{ path: string }> }`
over `files?: Array<{ path: string }>`) gives the member the type
`A[] & B[]`. The checker keeps both element literals, and no intersection
rule mapped an intersection of arrays, so the record was refused (SC2009).
When every constituent maps to one IR type that is not the dynamic value,
the intersection now maps to it. The rule runs only after every other
intersection rule declined, so intersections that already mapped keep their
mapping.
pi's NormalizedBuildSystemPromptOptions is such an intersection; it sits on
the chain from main()'s options to the extension API and refused main().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`length` and `byteLength` loaded the typed array's header with
`!invariant.load` (#2016). LLVM may move such a load down to its use,
past the release that frees the view, so `const slots = this.slots;
this.slots = next; return slots.length` returned a garbage number in
release builds. These reads are now ordinary loads that keep the
header's alias tag. Header loads that only bound or address an element
access stay invariant: their values reach only that access, which runs
while the view is alive, so typed-array loops still hoist the length
and data pointer.
Corpus typed-array-length-after-release covers a replaced field, a
deleted map entry and a rebound global.
`f!()`, `t[k]!()` and `o.cb!()` on a value that holds undefined threw
scriptc's checked-extraction message ("undefined is not representable in
the target union …"). The non-null assertion is erased in JavaScript, so
Node throws the call's own "<callee> is not a function". The checked
extraction behind an asserted callee now carries that message, with
V8's spelling of the callee: parentheses and non-null assertions dropped
and call results printed as `f(...)`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comments on net.sockWrite and net.sockWritableLength still described
the unsent bytes; the runtime counts each write not yet completely sent in
full.
The close step drops a socket's listener lists so that a listener which
captures its socket does not keep it alive; the 'drain' list was not
among them. A once('drain') listener still pending when the peer
disconnected, or an on('drain') listener, held its socket and the
socket's buffers after 'close'. The close step now drops the 'drain'
listeners with the others.
A loop to a typed array's length reads the length again on every
iteration, so a body that assigns the array's binding changes the limit
for the next iteration. The strip-mined form took the length once per
block of 64 iterations, and after the body assigned a shorter array it
ran on to the block's end, reading past the new length.
The loop to a typed array's length is now strip-mined only when the
array is a local of the function that the body never assigns
(stableReceiver on the match); others keep the budget and test the
length on every iteration.
New corpus program: native-worker-length-loop-reassign.ts.
RSA_PSS_SALTLEN_AUTO recovers the salt length from the signature and
does not enforce the key's minimum salt length, as in Node (OpenSSL).
The runtime's exemption now says so, and corpus 8178's header, which
said the minimum was still enforced, describes what it checks.
Untyped functions built each member of an object literal with
scr_dyn_key_set: a lookup of the key in the new object, a walk of its
empty prototype chain, a second lookup before the append, and a hash of
the key to share its bytes. The first cached read of the object then
computed its shape from its keys, hashing each key again.
A member whose key is a constant string other than __proto__ now owns a
store cache site and calls scr_js_store_probe, which appends the member
through the cached shape transition, takes the key bytes from the shape,
and leaves the object shaped. Members the probe does not take (Error
field names, members past the 64th, a site's first object of a shape)
keep scr_dyn_key_set, as do computed keys and literals in module
initializers.
push() of an empty Buffer outside object mode put a zero-length entry
into the read buffer, and the chunks pushed after it stalled behind it:
a 'data' listener saw nothing more and a readline interface never
closed. Node's readableAddChunk adds no empty chunk outside object mode;
it clears the reading flag and may read more. The runtime now does the
same, as it already did for an encoded push that decodes to nothing.
A typed-array loop version in a worker program tested its bounds and
its views' ownership together: every view had to have no shared
storage. A loop over views of a SharedArrayBuffer always failed that
guard and ran the checked copy, testing every element's index.
The guard now tests the bounds first and the ownership after. When the
bounds hold and every view is unshared, the loop runs the owned copy as
before. When a view is shared, it runs a new third copy whose accesses
skip their bounds checks and keep the shared-or-local path, without
alias scopes. Versions with a UTF-16 units copy, and programs without
workers, are unchanged.
Corpus 8350 runs such loops over shared, unshared and mixed views, to
counts within and past the views and over subarrays, with a worker
writing through shared views. The emission test checks the split guard
and the shared copy's accesses.
Once a program creates a weak collection, every object freed afterwards
called into the weak-key table to remove entries keyed by it, although
almost none are keys: zod's validation freed 16.6 million objects against
23 keys. A per-thread bit filter indexed by the object's address now marks
every address that entered the table (cleared when the table empties),
and the free paths test it inline, so an object whose bit is clear makes
no call.
Since the probes began caching absent properties (#2186), an untyped
property read that reaches the load or get probe costs more than before:
- Receivers that are not ordinary objects, such as class instances, have
no shape and always miss, but they set up the probe's frame first. The
exported probes now return the miss for them before calling the probe
body, which stays out of line.
- Each matching entry was answered through an out-of-line call
(scr_js_entry_answer). It is inlined into the probe again.
dynamic-properties executed 3.1% more instructions than before #2186; it
now executes 0.4% fewer.
The fixture's loop iterated `[6, 7] as (number | undefined)[]` so that
its binding kept the union layout the LLDB check reads through
`item->tag` and `item->slot.arm0`. Since (T | undefined)[] stores T
elements, an array that never holds undefined gives a `number` binding,
and the check failed with "item is not a pointer".
The loop now iterates `[6, 7, undefined]`, whose binding needs the
union, and the check also reads the undefined arm's tag.
A versioned loop over a non-ASCII string passes the range of indexes it
reads: a counted loop's start and limit, or a while loop's counter and
bound. The runtime converts the string at once when that range has at
least one unit per 16 bytes of it, and otherwise counts the loop as one
indexed read, so a loop over a line of a long source text no longer
keeps a UTF-16 copy of all of it. The publish call is weighted as
unlikely, since loops re-entered over one string skip it.