Commit Graph
1083 Commits
Author SHA1 Message Date
Chris Tate 496cf577c7 Print iterators and iterator helper objects as Node does
console.log and util.inspect of a generator object, an iterator helper,
or an array or string iterator were refused, and printing a Map or Set
iterator threw "util.inspect of a dynamic Array Iterator is not
supported yet".

Compiled generators now carry the tag their object prints with:
"Generator" or "AsyncGenerator" by default, "Iterator Helper" for the
helpers, "Array Iterator" and "String Iterator" for the iterators the
compiler builds as generators. The spawn wrapper sets it through the new
scr_gen_set_tag; an adapter over another generator copies that
generator's tag (scr_gen_copy_tag), and generators that stand in for an
object of another class, such as a Set iterator adapted into an
iterator-typed slot, keep refusing to print. They print as Node's
Object [<tag>] {} at any depth, and Object.prototype.toString answers
[object <tag>].

The runtime's native iterators print as Node does: Object [Array
Iterator] {} and Object [String Iterator] {}, and for Map and Set
iterators the entries the next steps would produce, without advancing,
under [Set Iterator], [Set Entries], [Map Iterator] or [Map Entries],
as [Object [Set Iterator]] past the depth limit and {  } when none are
left. Object.prototype.toString answers [object Set Iterator] and the
other tags.
2026-10-19 01:10:39 -05:00
Chris Tate 96d3a0f5e4 Carry AsyncLocalStorage stores into timer, immediate, tick and microtask callbacks (#1365) 2026-10-19 01:01:02 -05:00
Chris Tate 05f2dd5859 Walk Map and Set iterators inside iterator helper objects
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.
2026-10-19 00:03:25 -05:00
Chris Tate aef5b052b5 Sort the handle tags by name with explicit values (#1362) 2026-10-18 22:33:15 -05:00
Chris Tate 1f7f029a05 Carry AsyncLocalStorage stores into timer, immediate, tick and microtask callbacks
AsyncLocalStorage changes, matching Node 24.22:

- setTimeout, setInterval, setImmediate, process.nextTick and
  queueMicrotask callbacks run with the stores that were active when they
  were scheduled. Before, getStore() returned undefined in them.
- An interval keeps that context on every tick, and a refreshed timer
  keeps it too.
- enterWith inside one of these callbacks lasts until the callback
  returns. An enterWith in top-level code ends when the main module's body
  returns, and one inside a callback scheduled with no store active ends
  when that callback returns, so the store does not reach callbacks that
  were scheduled without it.

Programs that enter stores (store.run, exit, enterWith, or a
diagnostics_channel store binding) schedule through new *_als runtime
entry points, which wrap a callback scheduled with a store active in a
closure that installs the stores around the call. The emitter picks them
from the new asyncContext runtime feature; other programs keep the plain
entry points, so their scheduling is unchanged.

The limitations page notes that completion callbacks of callback-style
I/O APIs still run with no store.

5815-als-callback-context compares the stores each kind of callback sees
with Node, plain and with SCRIPTC_SAN=1.
2026-10-18 22:26:09 -05:00
Chris Tate 708b1350bf Free errors that crossed into checked-dynamic code and attach Node's error properties (#1359) 2026-10-18 22:17:52 -05:00
Chris Tate b9cc0ed820 Sort the handle tags by name with explicit values
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.
2026-10-18 22:06:48 -05:00
Chris Tate ecff232688 Close Next.js proxy gaps: fetch header and URL-error parity, JSON undefined, and twelve compile gaps (#1341) 2026-10-18 21:54:08 -05:00
Chris Tate 96f59b0f3f Free errors that crossed into checked-dynamic code and attach Node's error properties
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.
2026-10-18 21:53:14 -05:00
Chris Tate f629d03dd8 Merge main 2026-10-18 21:28:03 -05:00
Chris Tate a7e3a9d0aa Cancel running subtests when their parent test times out (#1350) 2026-10-18 21:24:04 -05:00
Chris Tate e8e432b6dd Clean up subtests cancelled by their parent's timeout
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.
2026-10-18 20:58:20 -05:00
Chris Tate 684f73d186 Add ThreadSlots, per-thread values on shared objects, to @scriptc/experimental (#1271) 2026-10-18 20:39:11 -05:00
Chris Tate d9ac08e57c Merge main 2026-10-18 20:25:10 -05:00
Chris Tate ee9a0e1c33 Pool keep-alive sockets in http Agents and use globalAgent for default requests (#1230) 2026-10-18 20:21:54 -05:00
Chris Tate 2d37d451e9 Run iterator helpers on untyped iterators (#1352) 2026-10-18 20:07:41 -05:00
Chris Tate ada2657873 Stop creating self-capture cycles and buffer cycle candidates on the release fast path (#1320) 2026-10-18 20:03:40 -05:00
Chris Tate f761f7d75a Count idle pooled sockets in agent.totalSocketCount
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.
2026-10-18 19:54:32 -05:00
Chris Tate ec79cf2789 Run iterator helpers on untyped iterators
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".
2026-10-18 19:42:27 -05:00
Chris Tate 3de2fb56a0 Cancel running subtests when their parent test times out
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.
2026-10-18 19:28:55 -05:00
Chris Tate 6ab49f33b2 Stringify class instances with JSON.stringify (#1338) 2026-10-18 18:59:22 -05:00
Chris Tate aa9289b9d9 Concatenate untyped Buffer lists as Node does (#1272) 2026-10-18 18:46:36 -05:00
Chris Tate 707ac5cf77 Carry Hash and Hmac values through unknown and any slots (#1257) 2026-10-18 18:38:52 -05:00
Chris Tate cf11fbaaeb WebAssembly W0–W2: free handle tags, a cross-thread clone hook, the WebAssembly types and refusals, and a decoder that reports every module a program can load (#1339) 2026-10-18 18:12:42 -05:00
Chris Tate 4dc25a18c1 Lower iterator helpers on generators and Map and Set iterators (#1342) 2026-10-18 18:11:21 -05:00
Chris Tate 14372c8274 Build "is not a function" errors for Hash, Hmac and KeyObject handles at any length
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.
2026-10-18 18:09:08 -05:00
Chris Tate 05b63df116 Check Buffer.concat's length before the elements of an untyped list
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.
2026-10-18 18:05:09 -05:00
Chris Tate dd2444fbb6 Keep React Flight's development-only stack parsing out of production builds, and make stack mode cheaper (#1330) 2026-10-18 18:01:06 -05:00
Chris Tate b32f4e31f3 Merge main 2026-10-18 17:47:55 -05:00
Chris Tate 2f40bcb8d4 Lower iterator helpers on generators and Map and Set iterators
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.
2026-10-18 17:46:31 -05:00
Chris Tate b119b4cf03 Stringify class instances through the runtime JSON traversal
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.
2026-10-18 17:32:17 -05:00
Malte UblandClaude Opus 5.5 4b78531ab4 Size handle tables by a fixed capacity and let handles cross threads
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>
2026-10-18 15:19:38 -07:00
Malte UblandClaude Opus 5.5 6c1af69d03 Add static Response.json
`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>
2026-10-18 15:16:17 -07:00
Malte UblandClaude Opus 5.5 1fd405c033 Add Date.prototype.toUTCString
`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>
2026-10-18 15:16:16 -07:00
Malte UblandClaude Opus 5.5 2957a9c042 Report unparsable fetch and Request URLs as "Failed to parse URL from"
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>
2026-10-18 15:16:14 -07:00
Malte UblandClaude Opus 5.5 46d238fc0a Send repeated Set-Cookie request headers as one combined line
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>
2026-10-18 15:16:14 -07:00
Malte UblandClaude Opus 5.5 77b137268d Visit each Set-Cookie value separately in Headers#forEach
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>
2026-10-18 15:16:14 -07:00
Chris Tate 356d968528 Merge main 2026-10-18 17:07:58 -05:00
Chris Tate 385465ff24 Remove dynamic-path leaks from typed code (two-tier stage S1) (#1326) 2026-10-18 16:47:13 -05:00
Malte Ubl 59edd160aa Merge remote-tracking branch 'origin/main' into opt/flight-stacks
# Conflicts:
#	packages/compiler/src/backend/llvm/emitter.ts
2026-10-18 14:46:05 -07:00
Chris Tate a1453ccbbf Merge main 2026-10-18 16:38:07 -05:00
Chris Tate afef6f09f5 Apply each request's maxHeaderSize to a pooled socket's parser
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.
2026-10-18 16:12:14 -05:00
Malte UblandClaude Opus 5.5 98d4611ef6 Merge origin/main into opt/gc-closure-cycles
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>
2026-10-18 13:49:20 -07:00
Malte Ubl 0620e30501 Merge origin/main into opt/tier-s1-leaks 2026-10-18 13:31:56 -07:00
Malte UblandClaude Opus 5.5 147013d7b9 Merge origin/main into opt/npm-small-fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:58:58 -07:00
Malte UblandClaude Opus 5.5 40c56b96ce Merge origin/main into opt/npm-small-fixes
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>
2026-10-18 12:58:53 -07:00
Malte UblandClaude Opus 5.5 26b703a821 Keep React Flight's development-only stack parsing out of production builds
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>
2026-10-18 12:44:31 -07:00
Chris Tate a5548597e5 Read zlib options held in typed objects
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.
2026-10-18 14:02:03 -05:00
Malte UblandClaude Opus 5.5 989d0d6bc2 Buffer nursery cycle candidates on the release fast paths
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>
2026-10-18 11:51:00 -07:00
Malte UblandClaude Opus 5.5 8597a364f6 Merge origin/main into opt/npm-small-fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:45:47 -07:00