A helper object over a Map or Set iterator that is stored and consumed
later (const it = set.values().map(f); for (const x of it) ...) stepped
the runtime's checked iterator through an adapter generator, boxing each
value and resuming two generators per element: 5.1x Node's time on a
100k-element Set.
A stage on m.keys(), m.values(), m.entries() (or a Set's), or on
Iterator.from(set) or Iterator.from(map), now walks the collection inside
the helper's own generator. The walk steps the runtime's native iterator
by entry index through the new scr_dyn_collection_iterator_index and
reads each key or value with the typed intrinsics. The native iterator
keeps the walk live and ends the collection's iteration when it is
exhausted or released, so an abandoned or returned helper leaves the
collection free to compact. Other helper sources over a Map or Set, and
Iterator.from over a Set or Map, use a typed generator over the
collection built the same way.
Every change that added a checked-dynamic handle kind appended its tag
just before SCR_DYNH_COUNT in scr_dyn.h, so any two such changes in
flight conflicted there. The tags are now sorted by name, each with its
current value written out, so a new tag goes in its sorted place with the
next free value, and changes adding different tags edit different lines.
SCR_DYNH_COUNT, used only by the capacity check, is gone.
The values are unchanged and the runtime artifacts are identical. A new
test checks what the enum's order used to guarantee: values are unique and
below SCR_DYNH_CAPACITY, and the LLVM backend's hardcoded tag numbers
(DYN_HANDLE_TAG_NUM) match the header.
The error identity cache (scr_dyn_from_error) keeps one checked-dynamic
node per error so a crossing error compares reference-equal and keeps the
properties set on it. It retained both sides for the whole process, so
every error that crossed a checked-dynamic boundary (a caught error read
through a cast, an error delivered to an untyped callback, a dns failure, a
warning) stayed alive until exit: 100,000 caught AssertionErrors read
through a cast grew the resident set by about 210 MiB.
The cache now sweeps itself. An entry whose error and node are referenced
only by the cache is unreachable (a node holds no reference to its error;
a registered subclass box holds one, and a spawnSync error's `error`
property refers to its own node), so a sweep releases both sides. Sweeps
run when the cache doubles past the entries the last sweep kept, so they
cost amortized constant time per crossing. The cache is per thread, and
objects shared across threads are published (immortal), so a sweep never
races another thread's reference. Live errors and live nodes keep their
entries: identity, rethrowing the same instance, isNativeError and kind
checks are unchanged.
scr_error_set_prop sets an own property on an error's node; dns, assert,
cipher, crypto, util.callbackify and warning errors use it for the
properties they attach. util.inspect of a runtime error renders through
its node: Node's own properties follow the bracket form in key order (a
system error's errno, code, syscall and path, and the extra properties a
throw site attached), and an AssertionError renders at depth 0 with long
string actual/expected values shortened, as its custom inspect does. A
node that contains itself renders as Node's <ref *1>/[Circular *1].
child_process errors carry Node's own properties:
- execSync and execFileSync: status, signal, output, pid, stdout and
stderr (Buffers, or strings with an encoding option); a spawn failure
or timeout adds errno, syscall, path, spawnargs and error first.
- execFile's callback and the promisified execFile/exec: code (the exit
status, or null after a signal), killed, signal and cmd, with stdout and
stderr when promisified; a spawn failure has errno, code, syscall, path,
spawnargs and cmd.
The sync forms echo the captured stderr before a timeout throw too, as
Node's inheritStderr does. The cp.execSync libCall takes a flag for Buffer
outputs.
An error's own `stack` stays non-enumerable when assigned through its
node. A worker's record-source table is freed when the worker exits.
node:test changes, matching Node 24.22:
- A subtest cancelled because its parent timed out has its mocks
restored and its mock timers reset when it is cancelled, as Node's
postRun resets the test's mock tracker. Before, a mocked method stayed
mocked for the tests that ran after it.
- Its afterEach chain and t.after hooks run at the next turn, once the
runner has moved on to the next test, as Node runs them when the
cancelled test's own run resumes. Before, they never ran. The chain is
copied when the test is cancelled, since its ancestors drop their hooks
when they finish.
- Its hooks are then dropped, so closures that capture its context do not
keep it alive. The registration parent is still not restored when the
cancelled body settles: it belongs to the test that runs then.
The timeout-cancel-cleanup fixture records the mocked method and the hook
order against the next test's body and compares them with Node, plain and
with SCRIPTC_SAN=1. It fails without this change.
agent.totalSocketCount counted only the requests an agent was serving,
so the idle sockets of a keepAlive agent in agent.freeSockets were left
out, and a second request that dialed a new socket while the first was
idle counted 1 instead of 2. As in Node, the count is now the number of
sockets the agent created that have not closed: a socket joins it when
its dial starts and leaves it when it closes, or before the request's
'close' when it does not go back to the pool. A request that takes a
free socket keeps the count.
Corpus 5593 covers reuse, a second socket, agent.destroy() and an agent
without keepAlive.
In JavaScript, a receiver without an inferred type, such as an untyped
parameter's xs.values(), is the runtime's checked iterator. Its only
method was next(), so xs.values().filter(f), .map(f) and the other
iterator helpers threw "TypeError: ... is not a function" where Node
runs them, and [].values().reduce(f) threw the same instead of Node's
"Reduce of a done iterator with no initial value".
The native array, string, Map and Set iterators now answer the
Iterator.prototype methods. map, filter, take, drop and flatMap make
helper objects (SCR_DYNH_ITERATOR_HELPER) that pull lazily and support
next(), return(), for...of and spreads, print as
"Object [Iterator Helper] {}" and tag as "[object Iterator Helper]".
toArray, forEach, reduce, some, every and find drive the iterator.
The helpers close the iterator they were made from where Node does:
when a callback throws, when take's budget is spent, on return(), and
before the error for an invalid argument. flatMap closes its inner
iterator first. An error from a close that runs because of another
error is dropped. Invalid arguments throw Node's messages, such as
"number 5 is not a function" and "x must be positive".
node:test changes, matching Node 24.22:
- When a test's { timeout } elapses, its subtests that are still running
are cancelled, the innermost first. Each reports its duration so far and
fails with "test did not finish before its parent and was cancelled",
before the parent's result.
- A cancelled subtest's body keeps running unobserved: when it settles it
reports nothing and leaves the runner's state alone. Before, it reported
its result after the run's summary.
The timeout-subtests fixture compares the results, counts and failing
section with Node, plain and with SCRIPTC_SAN=1.
The Hash/Hmac and KeyObject handle dispatchers formatted "<callee> is not
a function" into a 160-byte stack buffer and passed snprintf's return
value as the message length. A callee text longer than about 140 bytes
made that length exceed the buffer, so the error copied bytes past it:
the message ended in stack contents, and sanitized builds aborted. Both
dispatchers now build the message in a growable buffer, as the shared
dynamic-call path does, so the TypeError carries the whole callee text,
as in Node.
Corpus 5774 calls a missing method on a Hash, an Hmac and a KeyObject
through callee texts longer than 160 bytes, and through a short one, and
corpus-costs.json records its plain and sanitized costs.
Node's Buffer.concat(list, length) validates length (after returning early
for an empty list) before it checks that every element is a Uint8Array.
The untyped-list path checked the elements first, so a bad element and a
bad length threw ERR_INVALID_ARG_TYPE where Node throws the length's
ERR_OUT_OF_RANGE.
map, filter, take, drop, flatMap, toArray, forEach, reduce, some, every
and find compiled only as one chain from arr.values() or arr.entries()
that ended in a consuming method. Every other form was refused, and in
JavaScript the refusal was thrown at run time.
IteratorObject now maps to the generator type. A consuming method over
a chain of stages fuses into one loop over the source: a generator
driven like for-of, a Map or Set walked live, or an array by index. A
stage on its own builds a helper object: a generator over its source
that supports next(), for...of, spreads, Array.from and yield*. A
for...of whose head is a chain of stages without flatMap runs the
stages inside the loop.
The source is closed where Node closes it: when take reaches its count,
when some, every or find has its answer, when a callback or the loop
body throws, when the loop exits early, and before the RangeError of an
invalid take or drop budget. It is not closed when it is exhausted or
its own next() throws. return() on a helper object before its first
next() closes the source through a new runtime hook,
scr_gen_set_unstarted_return.
When closing an iterator throws while another error propagates, the
first error reaches the caller, as in Node. This applies to for...of
over a generator too, where the error from the close used to replace
the body's error. The try statement takes a dropFinallyErrors flag,
whose cleanup handler calls the new scr_exc_keep_first.
throw() lowers only on values typed as generators. Iterator helpers and
the built-in iterators have no throw method, and a value typed as an
iterator may hold one.
Callbacks in JavaScript bind their unannotated parameters to the
element type, and flatMap callbacks may return generators. A generator
whose record yields share checked storage with its return channel has
each value converted to the declared element type as it is pulled.
JSON.stringify of a class instance, or of a record, array or union that
holds one, boxes the value and serializes it with the runtime JSON
traversal. The output matches Node: own enumerable properties in
creation order, properties added after construction, toJSON on the
instance or a base class, and no methods, accessors, #private fields or
static fields. A class whose declared field cannot be read through the
checked-dynamic property view, such as a Date field, is refused at
compile time with SC1090 naming the field.
Boxed Map and Set values now stringify as {}, as Node does, instead of
throwing.
Appending a ScrDynHandleTag used to grow the always-linked scr_dynh_ops
table, which shifted .bss and code addresses in every program, including
programs that never box the new handle (Blob and File paid this when
they were added). The table is now sized by SCR_DYNH_CAPACITY (128; 52
tags are in use), so a link-gated unit can add a tag without changing
any other program's executable. This change is the one layout shift that
makes later tags free.
Structured clone between threads (postMessage, workerData) rejected
every handle other than MessagePorts and buffers. A handle's ops can now
carry a ScrDynHandleTransfer (share on the sender, adopt on the
receiver, drop with the message), which scr_message.c uses for any tag,
so a gated unit such as a future WebAssembly.Module needs no reference
from the message encoder and no per-tag registry. No handle declares the
hooks yet; scr_message.test.c exercises them across threads through a
test handle in the last capacity slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Response.json(data, init)` was refused (SC2020). It now builds a native
Response the way undici does: ResponseInit converts, the data serializes
through the checked-dynamic JSON serializer (toJSON hooks included;
TypeError "Value is not JSON serializable" when JSON.stringify answers
undefined), then the response initializes like the constructor with
"application/json" as the default content type, so status, statusText,
header, and null-body-status errors keep their order.
The fetch compatibility profile moves Response.json from unsupported to a
static operation with the static-response-json differential fixture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`new Date(ms).toUTCString()` was refused (SC2020). It now lowers to a
runtime formatter over the stored time value with V8's output: "Www, DD
Mmm YYYY HH:mm:ss GMT" in UTC, a leading "-" and at least four digits for
negative years, and "Invalid Date" for an invalid time value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
undici's Request constructor wraps the URL parser's failure: TypeError
"Failed to parse URL from <input>" with the parser's TypeError as its
non-enumerable cause. fetch() rejected, and new Request() threw, with the
bare "Invalid URL" error instead.
Both paths now replace the pending parser error with the wrapping
TypeError (sf_throw_url_parse_failure).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Headers stores each Set-Cookie value separately, but undici dispatches its
header list's combined value: one line with the values joined by ", " at
the position of the first one. fetch() wrote one line per value.
The request serializer now joins later Set-Cookie values into the first
entry and skips the rest, for every HeadersInit form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fetch's "sort and combine" never combines Set-Cookie: Node's Headers
iterator yields one entry per Set-Cookie value, in insertion order, while
get() still joins them with ", ". The native forEach walked the sorted
unique names and read the combined value, so repeated cookies reached the
callback as one joined string.
forEach now indexes the expanded list per step (sf_headers_sorted_entry),
keeping the live re-sort so callback mutations shift later entries as in
Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A request that took a socket from a keepAlive agent, either queued for a
freed socket or taking a free one, parsed its response with the parser
of the request that used the socket before, and with that request's
maxHeaderSize. A smaller limit was not enforced, and a larger or default
limit was cut down to the earlier one. The request now keeps its limit
(0 for the default) and applies it to the pooled parser when it takes
the socket, before any response bytes are parsed.
Corpus 5594 covers both handoffs with smaller, larger and default limits.
Conflicts with #1286's merged form and the death rule (74702ac4b):
- scr_cycle.c: keep the death rule's per-thread fields and drop the
running buffered total this branch removed (the slow path sums the
buffers); scheduling decisions route bypassed appended candidates and
then note the live count's fall.
- test_cycle.c: keep both new checks (inline buffering, death brings a
full pass).
- The self-capture corpus program moves from 5990 to 5985 (5990 is now
5990-worker-uncloneable.mjs), with its order baseline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep both the entry module's DEP0040 link warning and #1303's CommonJS
top-level this object in the module init header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
React Flight's production server never runs its V8 structured-stack parser
(ReactFlightStackConfigV8): every use is behind __DEV__. Builds compiled from
its sources keep that code behind `throw new Error('... should never be
called in production mode')` stubs, so scriptc lowered it, failed on
Error.prepareStackTrace, and a dead `.stack` read turned on stack-trace mode
(a frame push in every function) for the whole program.
- Statements after return/throw/break/continue in a function body are not
lowered. Declarations keep their hoisting through the existing forward
machinery (later function declarations, `var` reads undefined, `let`/
`const` TDZ); class, enum, module, import/export and generic function
declarations still lower. Functions referenced only from such code are
unreached and dead-stripped.
- Stack-trace mode requires a stack read reachable through live code
(observesStacks).
- Synchronous functions in executables push and pop their source frame
inline instead of calling scr_stack_enter/leave.
- Functions that only perform operations which never create or throw an
Error, and only call such functions, keep no frame (captureFreeFunctions):
no capture can include them.
Error.prepareStackTrace and CallSite stay refused: only development Flight
runs them. Good code is byte-identical (zero-cost gate: bench and
self-hosted compiler IR, LLVM and executables).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A zlib options object that reaches the runtime as a typed reference (a
cast object literal such as `{ level: 0 } as zlib.ZlibOptions`, which the
checked-dynamic options slot holds by reference) was not a plain
checked-dynamic object, so the options reader found no keys: every option
was ignored and out-of-range values were not reported. The sync, callback
and promisified one-shots and the stream constructors compressed with the
defaults instead.
scr_zlib_options_parse and scr_zlib_stream_new now read a typed reference
through its materialized view, as the blob and fetch option readers do, so
the options apply and invalid ones throw Node's ERR_OUT_OF_RANGE.
Every release that left a headered object alive and unbuffered called
scr_cyc_on_release, which looked up the type's bypass statistics, pushed
the object, kept a running total and compared it with two triggers. The
nursery case now has a fast path that appends the object to the nursery
buffer itself, while the runtime-maintained window says the append can
neither make a collection due nor grow the buffer, and the live count is
below the allocation trigger:
- scr_rc.h exports the candidate buffers (scr_cyc_buffers: the three
buffers, the window and the allocation trigger) where the collector
state is ordinary globals, and scr_cyc_append. The runtime's own
release paths (scr_cyc_released) append inline, and scr_cyc_on_release
tries the append first.
- Optimized emitted code buffers through one sc_cyc_append helper per
module, called with preserve_mostcc, so release fast paths keep the
size of the plain call they had (inlining the append into every
release fast path grew the self-hosted compiler's text by 4.6%) and
their callers keep their registers. Its runtime call goes through a
second preserve_mostcc function, which keeps the register saves off the
append path.
- scr_cycle.c keeps the window: recomputed where buffers, capacity or the
trigger change in bulk, lowered by one on older buffering and nursery
deaths, 0 during passes. The running total is gone; the slow path sums
the buffers.
- An appended candidate whose type bypasses the nursery would have been
buffered past it. scr_cyc_route_bypassed moves such candidates before
any scheduling decision or pass reads the buffers, consuming the bypass
counts as scr_cyc_on_release does (skipped while no type bypasses).
test_cycle checks the window (the release that reaches the trigger still
collects) and the routing of inline-buffered candidates of a bypassed
type; its mature-bypass probe routes before reading generations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each local-time getter (getFullYear, getMonth, getDate, getHours, ...,
getTimezoneOffset) called localtime_r and walked the calendar for its
own field. The thread now keeps its last local conversion, keyed by the
instant and a timezone generation, so reading several local fields of
one Date converts it once. The UTC walk and the second computation use
integer division instead of floor.
Writing or deleting process.env.TZ calls tzset and starts a new
generation, so later local-time reads use the new zone, as in Node.
Before, the zone the process started with stayed in effect.