pi's CLI setup replaces process.emitWarning to silence warnings. The
assignment now stores the replacement: later process.emitWarning(...)
calls apply it to their arguments, and runtime-built warnings (Node's own
modules call process.emitWarning) reach it as (message, type[, code])
instead of the report and the 'warning' listeners. process.title writes
already lowered; the corpus program pins both.
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.
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.
Node runs runNextTicks after each timer and each immediate callback, which
reports the unhandled rejections the callback left before the next one runs.
The event loop now leaves the timer phase when a callback leaves the
rejection ledger non-empty, the check phase waits for that report, and the
check phase reports the ledger after each immediate's checkpoint.
When several timers expired in the same turn, a later timer ran before an
earlier timer's rejection was reported. On macOS the 2 ms and 3 ms timers in
5809-process-exit-and-rejections.cjs expire together, so its uncaught
'thrown string' was printed before the TypeError rejection.
5996-rejections-between-macrotasks.cjs makes five timers expire in the same
turn and checks the order across three immediates.
node:test changes, matching Node 24.22:
- before and after hooks outside any suite run, with the root's context,
whose name is "<root>". They did not run before.
- A module-level before hook runs at its before() call, as Node runs the
hooks of its already-started root at once: its synchronous part runs
before the code after the call, and the first top-level test waits for
an async hook's promise. Every module-level before hook runs, and an
async hook's rejection is not reported as an unhandled rejection.
- Module-level after hooks run after the last top-level test or suite.
- A throwing root before hook fails each top-level test with the first
failure; the test's body and beforeEach hooks do not run, and its
afterEach hooks do. Each top-level suite fails with the error, and the
tests and suites in it are cancelled before they start: they report
without a duration and fail with "test did not finish before its parent
and was cancelled", listed in the failing section in completion order.
- A throwing root after hook reports the entry file as a failed result,
after the top-level results, and lists it in the failing section at the
hook's location. It is not counted, and the exit code is unchanged.
- Hooks record the "file:line:col" of their call for that location.
The root-hooks, root-before-throws and root-after-throws fixtures compare
the reporter output, the order of the hooks against the module code, and
the exit codes with Node, plain and with SCRIPTC_SAN=1.
node:test changes, matching Node 24.22:
- A describe or suite body may take its SuiteContext, with name,
fullName, filePath, attempt, passed and diagnostic. Its passed is false
until the suite has finished, after its after hooks.
- before and after hooks may take the suite's context, and beforeEach
and afterEach hooks the test's. A test's t.before, t.after,
t.beforeEach and t.afterEach hooks may take the test's context.
- SuiteContext maps to the runner node that TestContext maps to, so the
TestContext members that SuiteContext shares lower the same way.
The suite-context fixture compares the reporter output and the order of
the bodies and hooks with Node, plain and with SCRIPTC_SAN=1.
node:test changes, matching Node 24.22:
- t.filePath is the entry file's path, as Node's entryFile, fixed when the
program is compiled.
- t.attempt is 0, since a test runs once.
- t.passed is false while the body runs and the test's outcome once the
body ends, so afterEach and t.after hooks see it, as Node's pass() and
fail() set it before them.
- t.workerId is Number(process.env.NODE_TEST_WORKER_ID) || undefined.
- t.tags is [], since the tags option has no lowering.
- A test's t.before, t.after, t.beforeEach and t.afterEach hooks are
released when the test ends. Hooks whose closures capture the test's
context no longer keep it alive in a cycle, which the SCRIPTC_SAN=1
audit reported as live closures at exit.
The ctx-members fixture compares the reporter output with Node, plain and
with SCRIPTC_SAN=1.
`const C = zlib.Gzip` stopped at SC2020, and `instanceof zlib.Gzip`
(any zlib class) stopped at SC1090. A const binding of a zlib class is
now a compile-time alias with no runtime slot: `new C()`, `typeof C`,
`C.name` and `C === zlib.Gzip` lower at the use. `instanceof` against a
zlib class asks the runtime for the stream's mode when the operand is a
Transform, tests for a Transform first when it is typed as one of
Transform's bases, and answers false for other static types, as in Node.
Other uses of an alias as a value report SC1090 at the use.
promisify(crypto.randomBytes), pbkdf2, scrypt, hkdf, randomFill, randomInt
and generateKey, and promisify(dns.lookup) and dns.lookupService, over a
named or namespace import, now lower. A call through the binding runs the
callback form with a runtime callback that settles the returned promise
where the callback would run: fulfilled with the callback's value, or
rejected with its error. An argument error the callback form throws rejects
the promise instead, as the throw inside Node's promisify wrapper does.
The crypto promise's value follows the declared result type: a Buffer,
the filled view (the same object), a number, or a checked-dynamic value
where @types/node declares unknown (scrypt, randomFill, randomInt) and for
ArrayBuffer and KeyObject results. The runtime adds a callback closure that
owns the promise and settle adapters for each representation, next to the
existing callback forms in scr_crypto_async.c.
dns.lookup's promise fulfills with { address, family }, or the address
array with options.all, and lookupService's with { hostname, service }, as
Node's custom promisified forms do. The runtime builds the checked-dynamic
result; a declared record type is reached through an async helper that
awaits it and checks the value. The type mapper maps the promisified
lookupService result literal like the promisified exec result.
The fallback declarations gain the matching util.promisify overloads.
exec(command, callback) and exec(command, { encoding, maxBuffer },
callback) lower like execFile's callback slice. The command line runs
with /bin/sh -c, as execSync runs it, the call returns the live
ChildProcess, and the error-first callback receives the captured utf8
stdout and stderr. A non-zero exit, including the shell's 127 for a
missing command, reports Node's "Command failed: <command>\n<stderr>":
scr_exec_file takes a shell flag so the message shows the command line.
Under @types/node, child_process.ExecException maps to Error, as
ExecFileException does. Its code member is the child's numeric exit
status, which the runtime error's string code slot does not hold, so
reading it, testing it with `in`, or testing its typeof is a named
refusal.
scan walked every referenced component from its candidates, whitening
each object it reached and restoring the subgraphs of the externally
referenced ones, so garbage inside a live component was walked a second
time and objects whitened before a later root restored them were walked a
third. markGray already records every gray object in a pass of up to 64k
objects, and the survivors are exactly what the externally referenced
ones reach. So scan now sweeps the recorded entries of the referenced
components once, restores from each one that is still gray and
referenced, and drops survivors from the list as it sees them; what stays
gray is the dead set, with no whitening walk. A pass too large to record
still walks scan, but restores from its referenced candidates first. On
test_cycle's growing chain this cuts the objects traced from 1.53M to
1.06M, and the work-bound check now passes at thresholds 2 and 7 with a
growth cap of 1, where it failed before.
Every walk also visits a dense cycle-capable array's elements itself, in
a per-phase loop kept out of line so the per-edge visitors stay small,
instead of calling scr_arr_trace_v's indirect visit per element. Arrays
with sparse entries, properties or metadata keep the trace callback. The
array descriptor's kind is what identifies them, so it is reserved.
The new corpus program mixes dead and live structures in one pass,
inner nodes held from outside, arrays with holes and sparse slots, and a
live tree too large for a pass to record.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the scr_resume_fiber conflict with stackless async coroutines
(#1204): the region the fiber carries is made current before either
resume path and restored after both. scr_async_spawn_stackless now
captures the spawning region like the fiber spawns, so stackless bodies
(every async body in 5013 and 5023 after the merge) keep allocating in
their region after an await and keep it alive while they exist.
5024-lifetime-region-await-forms pins regions carried by stackless and
fiber (for await) bodies that spawn and await each other.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reads of defaultMaxListeners through an ES module namespace import or a
named import answer the namespace's copy of the export, which keeps its
value from program start until module.syncBuiltinESMExports() refreshes it,
as in Node. The default import, the EventEmitter class and CommonJS module
bindings keep reading the current default. A destructured CommonJS binding
of defaultMaxListeners, which held a placeholder string in JavaScript
files, is refused.
Full passes are scheduled by heap growth, and the fraction they wait for
widens while growth-triggered passes find nothing, up to 16 times the heap
after the last full pass. That keeps a large live heap that keeps growing
(a compiler checking a large program) from being walked again and again. But
growth alone cannot tell such a heap from one that keeps garbage: a program
that drops a large structure and builds another, as a compiler rebuilding in
watch mode does, holds the dead structures' cyclic parts until the heap has
grown to 17 times its size. Ten rebuilds of benchmarks/regions/compiler.ts
without lifetime regions reached 1.3 GB that way.
The acyclic parts of such a structure go by reference counting, and that
shows: the live count falls between passes. The collector now sums those
falls since the last full pass. Once they reach 1/32 of the heap that pass
left and have stopped (a structure torn down by reference counting falls
over several checks, and a pass in the middle would find its rest still
referenced), a full pass is due as soon as the heap has grown the base
quarter past it, whatever the growth fraction. One that finds fewer older
objects than had died doubles the fall the next one needs, up to 256 times,
and a productive one restores it.
A full pass that the growth rule did not choose (this one, a backlog pass, a
scheduled-age pass) now raises the growth baselines of the levels it walked
but does not lower them. After such a pass reclaims a dropped structure, the
heap regrowing to where it was is the program rebuilding what it had. From
the low point, growth passes at both levels would walk the rebuilt live heap
and find nothing. On the ten rebuilds that was a third of the collector's
added time. A heap that only grows never falls, so its schedule is
unchanged. The checks run where the release path and the event loop already
decide which pass to run.
A TypeScript Date placed in an `unknown` slot (an annotated binding, an
object or array literal, a record or class field) becomes a Date holding
the same time value. Before, a Date member of an `unknown` object literal
became a function, and other conversions were refused. util.inspect prints
these Dates in their ISO form or as Invalid Date, and their methods can be
read as values through untyped references. Comparisons with a Date operand
keep their fence: each conversion creates a new Date.
The undeclared properties a checked record keeps from its source, and the
entries of an `unknown` index signature, now follow SerializeJSONProperty:
toJSON runs with the member's key, getters are read, a Buffer becomes its
{ type, data } form, undefined, function and symbol results are dropped,
and a circular structure throws the circular-structure TypeError instead of
overflowing the stack. Members whose writing cannot run program code are
written directly; the others go through the JSON.stringify protocol walk.
Index-signature entries are written from the overflow map in place,
without building a key array.
node:test changes, matching Node 24.22:
- t.assert.snapshot(value[, options]) serializes the value with Node's
default serializer, JSON.stringify(value, null, 2), or with an
options.serializers array literal applied in order, and compares it
with assert.strictEqual against the snapshot stored under
"<test full name> <n>" in "<entry file>.snapshot".
- t.assert.fileSnapshot(value, path[, options]) compares the serialized
value with the contents of path.
- Snapshot files are read in the form Node writes them, exports[`key`] =
`value`; statements, with template escapes and CRLF line endings
cooked as in a template literal.
- Missing snapshots, unreadable snapshot files and throwing serializers
throw Node's ERR_INVALID_STATE errors, with Node's messages.
- Running the binary with Node's --test-update-snapshots flag as its
first argument records the values and writes the snapshot files after
the last test, keys sorted as Node sorts them. The flag is kept out of
process.argv and appears in process.execArgv, which is otherwise [].
- Snapshot assertions count toward the test's plan.
The snapshot fixture compares the reporter output with Node, plain and
with SCRIPTC_SAN=1, against a snapshot file Node wrote. The update-mode
harness test runs Node and the binary with --test-update-snapshots and
compares the files they write. The node:test normalization also drops
the "... N lines matching cause stack trace ..." line Node prints for an
error cause, with the stack frames it summarizes.
On Linux, a worker thread that exits without having made any object it
allocated reachable from another thread gives back all of its memory: the
pages it carved, including ProgramLifetime instances and other objects it
never freed, and the system blocks it still holds. The memory goes back
once no other worker thread is running, so that returning it does not slow
the workers that are still running.
A worker carves past each slice's frontier while the arena's free lists and
returned regions are set aside, takes from those free lists only once a
slice has no bump room left, and records the system blocks it allocates.
Messages and intern tables that share published graphs by pointer, and the
process-wide key arrays of typed records, mark the thread as sharing; so
does a remote free of one of its blocks. A worker that did not share parks
with its arena claimed and its discard queued: returning pages makes the
kernel interrupt every running thread of the process to flush its TLB, and
glibc's trim locks the arenas other threads allocate from. The last worker
to park starts one helper that returns each queued worker's pages, frees
its system blocks, and restores its arena to its state at the claim, with
the free blocks the worker took free again. A thread that finds every arena
claimed runs the queued discards itself. A worker that shared keeps its
memory as before.
SCR_SA_DISCARD=0 keeps every worker's memory; SCR_SA_DISCARD=poison makes
the discarded pages inaccessible instead, for tests.
scr_mt_tick removes a fired entry only while it is still the queue's
first entry, as Node's MockTimers.tick does. A callback that queues an
entry sorting before it, such as an immediate due at now - 1, leaves the
fired timer queued, so it runs again, as it does in Node. The comment
now says so and names the fixture that checks the order against Node.
packages/runtime/test/common-symbols.test.ts requires runtime globals to
be definitions, not tentative definitions that some toolchains emit as
common symbols; scr_uncloneable_count now starts at 0 explicitly.
Errors that reach checked-dynamic code are objects carrying the reserved
"%error" marker, with name and message either own or on their prototype.
Several string conversions handled them differently from Node:
- `"" + error`, `error + ""` and `error == "TypeError: ..."` converted
through the plain-object path and answered "[object Object]".
- Template literals and Array join read name and message as own members
only, so a builtin error whose name is inherited printed its message
alone.
- String(error) used the native error, which did not see name and message
assigned through the dynamic view.
scr_dyn_error_string implements Error.prototype.toString over the
encoding, reading both members through the prototype, and the template
renderer, Array join, ToPrimitive, ToString and the numeric conversion use
it. Assigning a string name or message through the dynamic view now also
updates the native error, as assigning stack and cause already did.
markAsUncloneable(value) marks a plain object, a record, an error or a
class instance (scr_json.c); structuredClone and postMessage then throw
DataCloneError "Cannot clone object of unsupported type." for it, also
where it is nested. As in Node, the mark has no effect on primitives,
arrays, typed arrays and ArrayBuffers. Marked values are retained until
the thread exits; with nothing marked, the clone paths add one branch.
worker[Symbol.asyncDispose]() terminates the worker and resolves with
undefined after its 'exit' event, so `await using` disposes a Worker at
the end of its block. The Worker handle's symbol_get op answers the
well-known symbol: the symbol unit registers Symbol.asyncDispose with the
dyn core when the program names it, and scr_dyn_is_async_dispose_symbol
tells the worker unit, so worker programs that never name the symbol do
not link the symbol unit.
static-support.json adds rows for markAsUncloneable, isInternalThread
(already lowered as false and declared) and worker[Symbol.asyncDispose]().
Corpus 5990-5991 match Node v24.22.0; corpus-costs.json records their
costs.
Regex literals and new RegExp calls accept the v (unicode sets) flag, which
the vendored engine implements: class subtraction and intersection, nested
classes, \q{...} string literals and properties of strings. The flag uses
unicode mode for pattern decoding and string iteration, and flags read in
getter order (dgimsuvy). Regex literals now also report their flags in
getter order, as Node does for /x/yg, and the flag getters (global,
unicodeSets and the rest) compile on statically typed regexes.
In v mode the engine rejected `-` and doubled punctuation outside a
character class, and the `\-` escape inside \q{...}. It now applies the
class-only checks inside classes and \q{...} only, and parses \q{...}
contents with the class escapes, matching Node. The d flag is still
refused.
A cycle pass tears down its dead set in the order the walk found it, so
the LIFO free lists handed the blocks of a large dead structure to the
next allocations scattered over every page it used, and the structures
built from them missed the cache on most objects. Sets of at least 16384
objects are now torn down in descending address order
(scr_mem_order_for_free, a bitmap of block indices per size class), so the
free lists hand the blocks back in ascending order, the order the bump
pointers carve fresh memory in.
A full pass takes every candidate buffer, so markGray now clears each
reached object's buffered mark in the header write it makes anyway,
instead of a separate loop over the candidates.
The util.types brands answered isFloat16Array with false for every value.
Float16Array now answers it from the value's storage kind, ahead of that
probe, so this drops the unreachable entry and the declaration that
returns boolean.
check_discard ran trim_allocate again for its returned-regions case, which
writes the blocks into trim_blocks, the array check_trim had freed. Where
glibc maps an 8 MB block by itself (its adaptive mmap threshold, which the
allocator's heap policy leaves alone unless transparent huge pages are in
madvise mode), the free unmaps it and the worker thread's first write
faults. The case now allocates the array again and frees it.
The check that a worker takes the free blocks set aside compared the
highest block it carved with that of the worker before it, which took its
blocks from free lists below the slice's frontier, and it passed when a
worker never took them. Each run now counts the blocks it allocates past
the highest block of the class so far: at most the bump room the slice had
left, against all of them when a worker does not take the free blocks. The
residency bound after a discard allows for pre-fault requests still in
flight.