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.
fork() copies only the calling thread, so a child forked while the
allocator's prefault helper held scr_sa_pf_lock waited on that lock forever
at its first fault-in request, and a child that released the range the helper
was faulting in waited forever for the helper to finish it. Native builds'
emit workers hung this way, with the parent waiting in waitpid.
The helper's first start now registers pthread_atfork handlers. The prepare
handler takes the lock across the fork and the parent handler releases it.
The child handler resets the ring, the busy range, the waiter state and the
heap polling state, reinitializes the lock and both conditions, and marks the
helper not started, so the child's first request starts a helper of its own.
check_fork_while_prefaulting in scr_alloc.test.c forks while the helper
faults in 64 MiB, over 64 rounds; each child releases that range, faults in a
range of its own and must exit within five seconds. ThreadSanitizer builds
skip it.
charCodeAt on a non-ASCII string mapped the UTF-16 index to a byte offset
through a string index entry, which the first read claimed and the
string's release purged. For a string of one character, such as each
character of a for-of loop that codePointAt(0) classifies, the entry cost
more than the read.
A non-ASCII receiver of up to 16 bytes is now decoded from its start on
each read, as scr_str_utf16_len already counts short receivers. Longer
receivers keep the index entry and its cursor.
The constructor took the program object with an empty name for the main
program, which is how glibc reports it. musl reports the main program
with its path, or "/proc/self/exe" in a static executable, so on musl
targets --large-pages found no segment and left the code file-mapped.
The constructor now takes the executable segment that holds its own code:
the runtime links into the executable, so that is the main program's
code under either C library.
scr_text_pages.test.ts runs its check against a static musl build made
with zig as well as the glibc build.
A large program's hot code spans hundreds of 4 KiB pages, more than the
instruction TLB holds. tsc-ts has 12 MB of code, and 90% of its
main-thread samples at --checkers 1 fall in 274 distinct code pages.
`scriptc build --large-pages` links the new runtime unit
scr_text_pages.c, whose constructor moves the executable's code onto
2 MiB transparent huge pages before main. It copies the executable
segment, from its first to its last 2 MiB boundary, into anonymous memory
advised MADV_HUGEPAGE, makes the copy read-only and executable, and moves
it over the original range with one mremap, so addresses and instructions
stay the same. The move runs only on Linux, only when THP is "always" or
"madvise", and only in executables. If a step fails, the code stays
file-mapped. Like Node's --use-largepages, it is off by default: without
the flag the unit is not linked, and executables are unchanged.
The move costs about 3 ms per 10 MiB of code at startup, and all of the
moved code stays resident as private memory. The flag is part of the
executable cache key and the runtime-pack selection, and is refused for
non-executable output, like --detached-exit.
Object.freeze of a fresh record or array literal now sets the native
frozen bit instead of compiling to the literal, so an untyped reference to
the value sees it frozen: Object.isFrozen answers true and strict-mode
writes throw Node's TypeErrors.
Untyped property writes in sloppy-mode code (IrModule.sloppyDynWrites,
decided per write by isStrictCode) call scr_dyn_key_set_sloppy and its
computed form, which drop exactly the TypeError a refused assignment raises
(a read-only or frozen property, a non-extensible object, a frozen array
element) while a setter's exception still propagates. Computed-write
dispatch helpers are shared only by writes of the same mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The static hello binary grew past the 96 KiB ceiling of the runtime
tree-shaking test on Linux hosts that link with GNU ld (99,040 B). CI does
not see it: its pinned LLVM ships ld.lld, which the compiler prefers, and
hello links to about 65 KB there. The Sandbox gate installs Ubuntu's
clang-22 without lld, so it links with GNU ld.
dd418b067 crossed the ceiling (98,032 -> 98,960 B). Its scr_glibc_math.c
defines floor, fmod, frexp and other glibc names as hidden functions so a
program can start without libm. With GNU ld that cannot work: ld decides
--as-needed before section GC, so the runtime's unreachable code keeps
libm needed, and it keeps every regular definition of a name a needed
shared library defines, hidden or not. Every executable carried the whole
unit (about 2.5 KB of code and unwind data) and still loaded libm. Links
without lld now leave the unit out; its functions answer what glibc's do.
192ea3247 installed scr_gen_tag_reader from a load-time constructor in
scr_async.c. A constructor is a link root on every format, so each
executable kept scr_gen_tag and, with GNU ld, all of scr_async.c's string
literals (about 1.1 KB). The generator runtime now installs the reader
when it creates a generator, the only time a reader can be needed.
Hello is now 93,928 B with GNU ld and 65,472 B with lld.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A template literal of strings and numbers reserved 32 bytes for each
number, the longest number text, and kept that capacity for the result's
lifetime: an 8-byte name built from two numbers used a 96-byte block. The
numbers now format into a stack buffer first, so the result is allocated
once at its exact length.
String.prototype.indexOf took its byte-search fast path on a proven-ASCII
receiver only when the start index was not positive. A positive start went
through the general path: ToIntegerOrInfinity, the surrogate-needle check,
and UTF-16 to byte conversions in both directions, although byte offsets
are UTF-16 indexes in an ASCII string. The fast path now covers every start
index: a positive start truncates and clamps to the length, where only the
empty needle is found, and the search runs from that byte offset.
An untyped write of a present declared field of a live record capsule now
stores into the native record through a per-shape `set` hook (checked like
the commit, refusing to the snapshot path for values the field cannot hold),
so the capsule keeps answering from the record instead of turning into a
permanent snapshot. Object.assign from a plain object takes the same path,
and checked conversions and matches read a capsule through a fresh,
uncached snapshot.
A checked conversion of a record capsule into a record type whose storage
is a same-offset prefix of the capsule's record (and whose extra fields hold
no references) passes the pointer: both names keep one object, writes show
through either, and the record remembers its own capsule parameters, so
every later box of it and the narrower type's reflective surfaces answer
for the whole object (decision D3). Other cross-layout conversions keep the
refreshed copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runtime vendored zlib 1.3.1 and archived it at -Os, and host C-lane
builds linked the system libz, so deflate, raw deflate and gzip output
differed from Node's at every level. The vendored copy is now Node
v24.22.0's deps/zlib (Chromium's zlib, 1.3.2.1-motley-285e94b), built at
-O3 with the units and defines Node's zlib.gyp selects per target: the
SSSE3 Adler-32 and SSE2 inflate and slide-hash code on x86-64, the NEON
and ARMv8 CRC-32 code on arm64, and the portable code on wasm32. The SIMD
units check the CPU at run time and give the same output, so compressed
bytes match Node's on every target.
zlibInputs (runtime-pack-common/scripts/zlib-inputs.mjs for the runtime
packs, vendor-inputs.ts for Zig cross builds and the sanitized runtime)
holds the per-target table, and a unit test keeps the two identical.
Every C-lane driver now links the vendored objects instead of -lz.
Chromium's BSD license ships beside zlib's in the runtime packs.
Corpus 8405 compares compressed bytes with Node across levels,
strategies, memLevel, windowBits, dictionaries and flushing streams;
8406 covers chunked inflation with small input pieces and output chunks.
A checked object (SCR_DYN_OBJ) now starts in layout mode: v.obj.layout names
its key sequence, a node of the thread's interned transition tree, and
v.obj.slots holds one 8-byte value per key, where every property used to take
a 48-byte entry. All properties of a layout-mode object are writable,
enumerable, configurable data properties. An accessor, a non-default
attribute, a delete, more than 64 keys, spent layout budgets, publication to
other threads, or any code that reads entries directly moves the object to
dictionary mode (today's entries) for good, so unported paths stay correct.
Layouts replace the dynamic tier's lazily computed hidden classes (ScrJsShape)
and keep their budgets. Inline caches key on the layout pointer and read the
slot directly; JSON.parse builds objects along layouts; stringify, key
enumeration, release, trace and the runtime read cache read slots in place.
The emitted index-signature checks read members through runtime accessors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each chunk a readable side delivered looked up its 'data' listeners by
name twice: once to decide whether a flowing stream emits directly, and
again in emit('data'), which scans the stream's event names even when a
piped stream has no 'data' listener at all. The stream now keeps a
data_listeners flag, set from the emitter registry's empty/nonempty
transitions through scr_emitter_observe, and reads it on those paths;
emit('data') is skipped when the stream has no 'data' listener.
forEach, map, filter, some, every, find, findIndex, findLast,
findLastIndex and flatMap on a dynamic array, and through
Array.prototype.<method>.call on a native array, called the callback
without its thisArg, so `this` in the callback was not the second
argument. These methods now bind `this` to the thisArg, or undefined
when there is none, once per call, around all of the method's callbacks.
Runtime units outside the object core (scr_json.c, scr_js_object.c) read and
adjusted SCR_DYN_OBJ entries in place: v.obj.len, v.obj.entries[i] and the
48-byte ScrDynEntry fields. The single object model's generic objects
(stage S3) change that storage, so these units now go through own-property
accessors in scr_dyn.h:
- scr_dyn_obj_count, scr_dyn_obj_key_at, scr_dyn_obj_value_at and
scr_dyn_obj_prop_at (a ScrDynProp view: key, value, getter, setter and
attributes) walk the properties in creation order;
- scr_dyn_obj_index_of finds one by key;
- scr_dyn_obj_set_attrs_at restricts a member a builtin object just added;
- scr_dyn_own_symbol_prop and scr_dyn_copy_prop_at wrap the symbol-entry
and copy primitives.
The accessors are inline over today's entries, so behavior is unchanged.
21 units move (inspect, assert, util compare, console, http, http2, fetch,
tls, fs, os, lib, array, message, publish, region, symbol, worker events,
handle properties, builtin values, declaration adoption). A package test
keeps entry storage reads inside the core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An async generator whose body suspends only at its own awaits and yields
now compiles to an LLVM switched coroutine, as async functions of that
kind already do, instead of running on a fiber with its own stack. Its
steps run on the stack that resumes it (the loop's after an await), its
awaits suspend through scr_coro_await_prepare and scr_coro_hop_prepare,
and its yields store OUT through scr_coro_yield_* before suspending.
- scr_async_gen_new_stackless creates the generator. The stackless hop
settles an async generator's state as the fiber hop does, and a step
that would start below the stack guard throws a RangeError, which
rejects its request.
- Calling an async generator's next, return or throw no longer counts as
parking the caller: they queue a request and run the body to its next
suspension, which returns to the caller. Bodies that consume async
generators can now be stackless too.
- An untyped await (async.awaitDyn) has a native coroutine form,
extracted by scr_coro_await_dyn_value, so async bodies that await
untyped values can be stackless.
- An async generator's return through a finally region takes its await
turn through the coroutine when the body is one.
Debug builds, libraries, worker programs and Windows executables keep
fibers, as for stackless async functions.
Node passes a stream's one onwrite function to every _write and _writev
call, and a new function to every _transform call. A write or writev
option callback now receives the same closure on every call
(scr_stream_write_closure), kept or not, so a Set, WeakSet or WeakMap
finds it again. A reused transform, flush, final, construct or destroy
callback is first forgotten by weak collections, as at its free, so a
WeakSet or WeakMap that recorded the previous call's callback does not
find it on the next call.
Each call of a typed or untyped write, writev, transform, flush, final,
construct or destroy option callback allocated a closure and a box holding
the stream for its `callback` argument. The emitted thunks now take it
from scr_stream_done_closure, which keeps the last one on the stream state and
hands it out again while the stream holds its only reference and it has
no own properties. A callback the user kept (stored, or passed to
setImmediate or a promise) is not reused: the next call gets a new
closure, and the kept one keeps its own stream reference.
The cached closure holds the stream, so the stream drops it when it
emits 'close' and traces it, which lets the cycle collector reclaim a
stream dropped before it closes.
RUNTIME_ABI_VERSION goes from 46 to 47 for scr_stream_done_closure.