An exported function declaration is a mutable binding, so its module can
replace it; the hooks treated only `let` and `var` exports as live and
snapshotted the function, leaving island imports on the first function.
Bind function exports like `var`, and cover a reassigned function export
in the shared-singleton corpus program.
Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
An island signal registration is now tagged with the raw listener it
installed, so a once registration and an on registration of one function
dispatch through their own listeners instead of both resolving to the plain
function, and the once registration no longer fires indefinitely. The
removal handler forgets the entry whose raw listener is no longer on
process, so a once wrapper that removes itself leaves its runtime
registration behind no longer; duplicate registrations of one function
share a raw listener, and the most recent of those is removed.
refresh() of a timer armed before Node loads re-arms at now + the delay the
timer was created with. Its Node handle was created with the remaining delay
at handoff and a due timer had no handle at all, so the handle is replaced
rather than refreshed.
The signal program registers one function once and on; a new timer program
refreshes a timer most of whose delay passed before Node loaded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records checked from a proxy in a program that freezes untyped values no
longer fail on the proxy's integrity query: a proxy is frozen exactly when
its target is. A sloppy-mode function called with undefined or null
receives the global object that globalThis reads, so its builtin members
such as console answer before anything else reads the global object. The
new corpus programs are named instead of numbered and record their CI
costs.
A method value of a web stream class, such as a saved
WritableStream.prototype.close, called on an instance whose own property of
the same name wraps it, ran the own property again, so the wrapper recursed.
The classes' method values now call the class's method directly
(ScrDynHandleClass.method_invoke); member calls still look at own properties
first.
TransformStream resolved its start promise with a transformer start's
result without adopting a thenable, so both sides started at once and a
rejection was lost. The start promise now resolves as a promise's resolve
function does (scr_dyn_promise_resolve_with): a thenable adopts through its
resolve job, a throwing `then` read rejects, and the stream starts or errors
when it settles, in Node's order.
Lowerer.namesWebStreamClass, a cache made when first needed, is listed in
Lowerer.unjournaled.
The scr_map unit test compiles 23 runtime translation units with
ASan/UBSan, and that build is essentially the whole test: 90s of clang
against a 2.1s binary run. scr_hash.c alone takes 29s at -O1 because the
sanitizer instruments its unrolled digest rounds; scr_json.c and
scr_lib.c add 28s more. The test then normally ran within 20s of its
120s timeout, and whenever a lane shared the box it repeatedly timed out
in both the plain and sanitized lanes.
The link set cannot shrink: scr_map.c's dyn ops need scr_dyn_* from
scr_json.c, scr_lib.c supplies the remaining date/math/number/perf
symbols, and scr_lib.c needs scr_hash.c. The test asserts Map semantics
under sanitizers, not codegen, and its cost checks are relative bounds,
so build it at -O0: every assertion and sanitizer check still runs and
the build drops to about 15s. Eight concurrent copies now take 20-22s
each instead of 90s, with the timeout unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In JavaScript sources, WritableStream with its default writer and
controller, TransformStream with its controller, pipeTo and pipeThrough,
and TextEncoderStream and TextDecoderStream join the native web stream
classes, following Node 24's webstreams step by step: sinks and
transformers, backpressure and the writers' ready and closed promises,
abort with the controller's signal, the pipe's shared write requests and
its abort signal, and UTF-8 encoding and decoding across chunks settle in
Node's order. The streams take own properties, which a pipe does not use
for its own reads and writes, and the global object answers the classes
by computed name in programs that name them.
A JavaScript class that extends one of these classes keeps the SC1090
refusal, and TextDecoderStream refuses encodings other than UTF-8.
The node24-web-stream lifecycle, errors, queues, transforms, pipes,
pipe-errors, pipe-abort, tee and encoding cases now match Node.
Response.clone and Request.clone tee the body. The tee branches of a body
read from a JavaScript ReadableStream did not keep that body's chunk check,
so consuming the original or the clone took string chunks as text. The
check is now a flag on the body's stream that its tee branches inherit, and
consuming either branch rejects with Node's TypeError "Received
non-Uint8Array chunk".
Under Node.js, publish() deep-freezes everything it reaches, so
Object.isFrozen and Object.isSealed return true and Object.isExtensible
returns false for a published object, array, Map or Set. Native builds
already refused writes into published values, but answered these queries
as for an unpublished value: publication marks an object immortal and sets
no frozen bit.
- record.isFrozen, array.isFrozen and the extensibility test of Maps and
Sets also read an immortal rc word as published, the test of the
published write guards, for the types the program may publish
(PublishedTypes). Other types emit the code they did.
- scr_dyn_is_frozen, scr_dyn_is_sealed and scr_dyn_is_extensible answer
for a published object, array, Map or Set, or a checked view of one,
before materializing the view.
Worker runtimes reserve one region for 32 arenas of 40 class slices. When
the host refused the full region (32 arenas of 1 GiB slices, 1.28 TiB of
address space), the allocator kept 32 arenas and shrank every slice. Under
valgrind, which maps at most 56 GiB, slices dropped to 16 MiB, and every
class whose live blocks outgrew 16 MiB took the system allocator.
Callgrind profiles then counted system allocations that a native run
never makes: tsc-ts checking VS Code made 10.3 M small calloc calls under
valgrind and 0.06 M natively.
The reservation now tries arena counts from 32 down to 4 at each slice
size, from 1 GiB down, before smaller slices, and 2 and 1 arena only when
no slice size fits 4. Arenas that are not reserved stay claimed, so
threads beyond them use the system allocator, and the region bounds of
foreign frees use the arenas reserved. Hosts that map the full region keep
32 arenas of 1 GiB slices.
A runtime test limits RLIMIT_AS to 60 GiB and checks that the first thread
gets 256 MiB slices for 400,000 blocks of 56 bytes, that three more
threads get arenas and a fourth uses the system allocator, and that blocks
freed across threads keep their contents.
A JavaScript ReadableStream given to Response, Request or fetch as a body
was converted to the string "[object ReadableStream]". The fetch unit now
reads such a stream through a reader of its own. A locked or disturbed
stream gives Node's TypeError, and the chunks must be Uint8Array
when the body is consumed ("Received non-Uint8Array chunk").
instanceof against a web stream class held in a variable or passed as an
argument walked no prototype for handles and answered false. It now starts
at the class prototype of the handle. The fetch unit's own streams, readers
and controllers, such as Response.body, are instances of the JavaScript
classes, so `response.body instanceof ReadableStream` is true again.
These paths, and the fetch reader functions' hand-off to web stream
readers, are in SCR_HANDLE_CLASSES variants of scr_json.c and scr_fetch.c,
which only programs with JavaScript web streams link; other programs that
use fetch build to the same size as on main.
View target structs no longer grow: every stack copy of such a record grew
by the two words, which enlarged unoptimized frames enough for the
sanitized self-rebuild to run out of stack. The guard's cold path looks the
slot up through the view's record-source entry (scr_record_view_slot),
which now holds the viewed record and its offsets, and stack objects of
target types are allowed again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unoptimized builds give every value a frame slot, so the inline guard
grew the frames of large recursive functions enough for the sanitized
self-rebuild to run out of stack. Development builds now call
scr_record_view_field for the slot address; optimized builds test the view
mark as one range (the frozen bit plus a count) before the cold lookup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The view guard's slot lookup moves into scr_record_view_slot, and the
branch to it is weighted as unlikely, so a field access of a view target
type adds one rc-word test to its hot path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A value of a view target type can point at another storage (a shared
field read of a shape union, for one), whose block ends before the view
words. The view mark is now the rc word's frozen bit, which no other value
of a view target has (targets are never freezable; an immortal word is no
view), and the trailing words are read only after it. View targets release
like freezable shapes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tagged shape's values share field reads with the other arms of its shape
union, which carry no view words, so tagged shapes keep their copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A checked conversion of a capsule of record A into record type B, when A
has all of B's fields with the same types in any order, now answers a view:
a B object whose two trailing words hold A and A's offsets for B's fields.
Every field address of a B value tests the first word (recordViewFieldPtr,
the record clone and the JSON walker), so reads and writes through either
name reach the same storage. The view's record-source entry owns A's
capsule: boxing the view answers that capsule, so identity holds, B's
reflective surfaces and spreads see the whole object, and the entry dies
with the view, which releases A's capsule. The capsule finds its views
again through borrowed cast-list entries.
Only the target types of such conversions carry the words and the test.
They are planned from the IR before emission (record-views.ts): record
types a checked conversion can produce for which another record type in
the module has all their fields. Programs without checked conversions are
unchanged. This replaces the prefix pointer pass, whose narrower release
could not free the source's extra fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each thread keeps up to 8 MB of arena chunks and up to 64 dropped arenas
for reuse (scr_arena.c). Nothing freed them when a worker thread finished,
so every worker that used a group arena left its pool behind: 40 workers
run one after another, each dropping a 12 MB arena, kept 328 MB resident.
scr_arena_thread_exit frees the calling thread's pooled arenas and chunks.
The worker thread calls it after its context cleanup, before it posts the
exit event. The new harness test runs 24 such workers in sequence and
checks that the resident set grows by less than 32 MiB over the last 20
(160 MiB before this change, 1 MiB after).
In JavaScript sources, ReadableStream, ReadableStreamDefaultReader,
ReadableStreamDefaultController, CountQueuingStrategy and
ByteLengthQueuingStrategy (globals and node:stream/web exports) are class
values over a new runtime unit, scr_webstreams.c, which follows Node 24's
webstreams step by step: underlying sources, queuing strategies, default
readers, tee, ReadableStream.from and async iteration settle their promises
in Node's order. scr_dyn_handle_class.c gives a dyn handle tag a
constructor value with a prototype, brand checks and Node's constructor
errors. Stream state carries cycle headers, so the collector reclaims
streams whose callbacks close over their controllers.
Promise steps run as native promise reactions, a variant of scr_async.c
that only programs with web streams link; every other program links the
default object. TypeScript sources keep their static web stream lowerings.
Byte streams, BYOB readers, pipeTo and pipeThrough throw SC2020.
node24-web-stream-readers and node24-web-stream-readable-lifecycle now
match Node.
The LLVM backend can emit scr_census_write, which a census program
defines and the runtime calls through a weak reference, so the header
declares it as it does scr_detached_exit.
Libraries never build the census (heapCensusOf), but the runtime's weak
scr_census_write reference made library archives list an external
symbol outside their declared set. Declare and call it only in programs.
`toUpperCase` and `toLowerCase` now record that a result of their ASCII
path is ASCII. `padStart` and `padEnd` take the UTF-16 lengths of the
receiver and the fill the way `length` does: a proven-ASCII or short
string is counted directly instead of claiming a string-index entry. A
one-unit fill repeats without a division, a one-byte fill with memset,
and a result padded from an ASCII receiver and fill is recorded as ASCII.
`word.slice(0, 3).toUpperCase().padStart(4, "-")` previously built an
index entry for the three-byte receiver on every call.