process.on/off for signals, uncaughtException, unhandledRejection,
rejectionHandled and warning pass the listener to the runtime as a
dynamic value. The int32 slot analysis treated those calls as possible
dynamic stores into any field, so every class reaching dynamic code
lost its int32 fields. The runtime only keeps each listener and calls
it with fresh event values, so these helpers are read-only.
A boxed function converts its result to dyn, which boxes any promise the
result type holds. The feature scan checked only for a result that is
itself a promise, so boxing a function returning `Promise<T> | undefined`,
a record with an async function field, or an array of async functions
still failed to link with an undefined scr_dyn_new_promise_adapting. The
scan now walks the result type like the other boxed types.
Corpus program 5812 and the runtime-features unit test cover these
results.
The allocator's helper thread faults in memory just ahead of each
class slice's bump limit: one 256 KiB step on small pages and the next
2 MiB huge page once a slice is on huge pages. Every thread got that
policy. Programs with many worker threads (parse and check pools)
committed faulted-in ranges, up to a huge page per hot class per
thread, that the workers never carved, and all workers queued on the
one helper.
Worker threads now fault ahead only after their slice limits have
advanced 4 MiB, and the ranges faulted ahead of all workers together
stay within 8 MiB. A range counts against that budget until its
class's limit reaches it or its thread parks its arena, and each class
has one range in flight. Steps outside the budget are faulted by the
owner, as with SCR_SA_PREFAULT=0. A program whose allocation runs on
one worker keeps the pre-faulting. The main thread keeps the previous
policy, and executables built without worker support compile only that
policy, so single-threaded programs are unchanged.
scr_dyn_from_iterator answered kind 0 (not iterable) when reading
Symbol.asyncIterator or Symbol.iterator threw, so Readable.from raised
ERR_INVALID_ARG_TYPE in place of the getter's error. It now answers
kind -1 with the error pending, as its declaration states, and
Readable.from throws the getter's error, as Node does. Corpus 5614 covers
a throwing Symbol.iterator getter and a throwing Symbol.asyncIterator
getter.
Release executables now import small runtime functions from runtime
bitcode, as speed executables do, so LLVM can inline them into program
code. Release units keep the runtime compiler's own objects, unchanged,
and additionally ship the bitcode of the same compilation reduced by the
helper's prune-import. Their unit-local symbols stay local, so only
bodies that need none of them can be imported, which keeps the import
small: about 1.3 MB per program.
Only unsanitized programs small enough for one codegen partition import.
Every partition reads the bitcode again; on the Effect fixtures (64
partitions) import cost +2..18% compile time and +4.3..4.9% size, so
partitioned release builds stay exactly as before. Speed is unchanged.
Measured against main in Linux Vercel Sandboxes, same-VM interleaved
medians (bench-runtime --layouts=4 --runs=16), on two VMs:
- bench-runtime (31 workloads): -1.3% (8 faster, 0 slower) and -1.0%
(1 slower, typed-array-scan-workers +3.2%); size +0.0%
- js-compiled suite (22 programs): -2.2% and -1.8%; size -0.1%
- cold builds of all 53 programs, interleaved: +3.1%; helper time per
build +40..235 ms
- Effect fixtures: byte-identical to main
SC6001 only looked at call arguments and typed initializers, and only at
strict field subsets: a target that adds optional properties, the empty
record type, `r as T`, a later `v = r` into a typed binding, and records
stored as elements of a typed array literal copied silently.
The scan now reports a copy whenever the property sets differ, covers
those sites, and reports a class instance that reaches a plain record
type which does not keep its identity (the remaining boundary: interfaces
of only generic methods). Every corpus program still reports no
divergence.
A class instance passed, stored, returned or asserted as an anonymous
object type or an interface it does not `implements` was copied into a
native record, so writes, identity checks and inspection through the view
missed the instance (`function add(o: { count: number }, n: number)
{ o.count += n }` left a Counter at 0).
ClassProtocols now scans flow sites (arguments, typed initializers and
fields, defaults, returns, arrow bodies, yields, assignments, array
elements, property values, assertions through `unknown`, and annotated
callback parameters) and marks every program object type a class
instance flows into, then propagates the mark along flows between
structural types. Marked types are class-backed protocols: their values
are checked views that keep the original object, like implemented
interfaces already were. Only sites whose source carries identity query
the checker for their target, and a checker panic skips the site.
Assertions read or written member-wise in place keep their direct field
access, and `typeof C` is not an instance.
An assertion of a class instance to such a view boxes it like an
implicit flow, and `view.n++` on a number member lowers to the checked
expanded form.
A setup loop such as `for (i = 0; i < Math.floor(300000 * scale); i++) text.push(word())` multiplies only in its condition; its body does no integer work, so it keeps one copy. Also records the ts7 baseline of the speculative integer reads fixture.
A sieve's inner loop `for (j = i * i; j <= limit; j += i)` only counts, but by a step the static proof cannot bound when the limit is a run-time value; its double copy pays a conversion of every index. Such loops keep their integer copy, while counters stepping by a literal still keep one copy.
A loop whose speculated integers only count and compare gains nothing measurable from a second copy but grows the binary, so a loop is duplicated only when a speculated local or invariant is a multiplication, division or remainder operand, or a speculated write multiplies or takes a remainder. Sum-overflow corpus loops gain a remainder use so they keep exercising their bails.
Locals whose reads the integer range analysis already proves are never shadowed or checked, and a loop with nothing unproven is not duplicated. The unproven targets that remain gain speculative leaves: charCodeAt results, array elements and record or class fields are read once before any check (they have no effect, so a failed check may restart the statement) and checked like any other integer. Module numbers written only by top-level code act as entry-checked invariants in loops of any function, which covers divisors computed at run time.
A loop-written local now gets an integer shadow only when the loop reads it as an integer (multiplication, remainder, bitwise operators, comparisons, indexes) and every local its writes read is speculated, already an integer, or loop-invariant: accumulators that are only added to keep their cheap double add, and values computed from doubles no longer bail on every entry. Statically proven subexpressions use the ordinary integer emission, and sums with one statically bounded operand check the other operand before adding. In loops that run no other code and leave only through their condition or break, module globals are written back on leaving the fast copy instead of on every write.
Statements inside a sequence expression run mid-expression, where a failed check cannot restart them; their writes now leave the local unspeculated. Speculative writes also run in their own frame and refuse to own temporaries, since a bailing path would skip their release. Adds planner unit tests.
Innermost loops whose number locals are only written by statement-level integer arithmetic run as two copies. The fast copy keeps those locals (and loop-invariant number inputs) in i64 shadow slots holding safe integers and computes the writes with checked arithmetic, also storing the exact double so exits and calls need no write-back. A failed check (overflow past 2^53 - 1, -0, a fractional or non-finite leaf, a zero divisor) re-executes the same statement in the ordinary double copy. Module-level numbers written only by their file's top-level code qualify too.
Records in a parsed array usually spell the same keys in the same order.
Like V8's JSON parser following a map's expected transition, the dyn
parser now remembers the key sequences it has built in the current parse
as a small tree rooted at the empty object. When the next member's raw
text is exactly `"<expected key>"`, it needs no string scan, no duplicate
search and no key copy, and the entry shares the tree's interned key.
The object holds exactly the keys on its path, and a transition is
recorded only when a put appended a key that was not yet present and
needed no unescaping, so a byte match proves both the decoded key and its
novelty. Duplicates, escaped keys, keys beyond four candidate transitions
and parses past 4096 transitions finish the object on the ordinary path.
The tree is freed at the end of scr_json_parse and scr_json_parse_schema.
The recycled-key check in scr_dyn_obj_put and the true/false/null match
also use the inline scr_key_equal instead of a libc memcmp call, which
took 2.5% of the 31-json dynamic variant.
Sandbox, interleaved medians of 15 runs against the branch base (with the
read-site caches): 31-json as published 2.33 s -> 2.09 s, its
`checksum = checksum + parsed[i].score` variant 2.43 s -> 2.22 s, the
12-key constant-read benchmark 1.68 s -> 1.25 s. Peak RSS is unchanged.
Every `value.name` on an any/unknown object went through the shared key
helper's kind tests, scr_dyn_obj_read, scr_dyn_obj_read_receiver and
scr_dyn_find_entry, whose one global hint table is keyed by key length and
first byte. Keys that collide there (k05/k11, or any same-length keys with
the same first letter) evicted each other and fell back to a linear scan.
A constant-key read on an ordinary object now calls scr_dyn_obj_read_ic
with a zero-initialized per-site ScrDynReadIc, thread-local wherever
program globals are. The site remembers the entry position where it last
found the key as an own data property and holds a reference to the
interned key bytes stored there. Objects that spell a key alike share
those bytes, so a hit is one pointer compare at one position; long keys
and share-table collisions compare bytes there instead. Accessors, the
builtin Object.prototype and other receiver kinds keep the previous path.
Sites release their keys with the thread's runtime state (scr_atexit),
and the RC audit lane never fills one, so its live-key count stays exact.
Sandbox, interleaved medians against the branch base: 20k parsed 12-key
records with three constant-key reads each, 300 rounds, 1.66 s -> 1.16 s;
31-json's dynamic loop variant 2.37 s -> 2.35 s (its reads are bound by
the miss on each record's entry array). Peak RSS is unchanged.
Boxing a value into unknown converts promises through scr_async_dyn.c.
The runtime feature scan linked that unit only when the boxed value was
itself a promise or a function returning one, so boxing a record with an
async function field, an array of such records, or a union with such an
arm failed to link with an undefined scr_dyn_new_promise_adapting. The
scan now follows record fields, array, Set and Map elements and union
arms.
Corpus program 5812 boxes these values and compares the output with
Node.
Object.setPrototypeOf now follows the spec order. A null or undefined
receiver throws "Object.setPrototypeOf called on null or undefined",
then the prototype is checked, and a boolean, number, string, bigint or
symbol receiver is returned unchanged. Previously any receiver that was
not an object or array threw "Object prototype may only be an Object or
null".
A string path holding a null byte reached the system call, which saw the
path up to that byte: readFileSync("a\0b") read "a", writeFileSync wrote
it, and existsSync tested it. Node refuses such a path before any file
system access with an ERR_INVALID_ARG_VALUE TypeError naming the argument
("The argument 'path' must be a string, Uint8Array, or URL without null
bytes. Received '...'").
The fs runtime entry points now check their string paths first (each
with Node's argument name: path, prefix, oldPath/newPath, src/dest,
existingPath/newPath, target), so the sync forms throw, the promise forms
reject, and the callback form of rename throws before it queues the
operation. The Received text is util.inspect's quoted string, cut at 128
characters as Node cuts it; the JavaScript lane's validation ladders
share the same rendering, which now picks backticks for a string with
both quote kinds and escapes control characters as inspect does.
existsSync answers false.
Corpus fs-null-byte-paths.ts and fs-null-byte-paths-js.cjs cover the
sync, callback and promise forms in both lanes.
createHmac(algorithm) with no key now compiles and throws Node's
ERR_INVALID_ARG_TYPE ("Received undefined") from the key check, which
runs before the digest check as in Node; it used to be refused at
compile time. Reading copy on an Hmac, which has no copy() method,
answers undefined, so `typeof mac.copy` feature checks see what Node
reports. TypeScript already rejects that read.
Corpus 5764 covers createHmac with missing, undefined, null and number
keys, an unknown digest with and without a key, and the copy read.
The one-shot zlib forms now accept any options value and validate it at
run time, so the lowering test's options cases expect no diagnostic, as
does inflateRawSync with options. A Brotli call keeps the SC2020
refusal.
socket.address(), socket.resetAndDestroy() and server.dropMaxConnection
were refused at compile time, and address() and resetAndDestroy() also
through untyped socket handles. They now match Node:
- address() answers { address, family, port } while the socket has a
local address, and {} while it connects or once it is destroyed. The
declared AddressInfo | {} result is a checked-dynamic value, so a type
assertion to AddressInfo narrows it.
- resetAndDestroy() closes the connection with an RST (SO_LINGER with a
zero timeout), so the peer reads ECONNRESET, and 'close' reports
hadError false. A connecting socket resets once it connects: the reset
is a once('connect') listener, so it runs at that place among the
'connect' listeners. A destroyed socket is left alone, a TLS socket
throws ERR_INVALID_HANDLE_TYPE, and the call answers the socket.
- dropMaxConnection is a stored value (undefined until set), since Node
reads it only in cluster mode.
Corpus 5657 (typed) and 5658 (untyped handles and the TLS throw) and the
node-types fixture net-reset-address.ts cover each member.
deflateSync, inflateSync, gzipSync, gunzipSync, deflateRawSync,
inflateRawSync and unzipSync, and their callback forms, accept Node's
options and run Node's processChunkSync over the zlib stream engine.
- Options (windowBits, level, memLevel, strategy, dictionary, chunkSize,
finishFlush, maxOutputLength, rejectGarbageAfterEnd) are validated at
run time with Node's ladder, shared with the stream classes; info is
refused at run time.
- Errors carry Node's errno and code; maxOutputLength reports
ERR_BUFFER_TOO_LARGE and rejectGarbageAfterEnd
ERR_TRAILING_JUNK_AFTER_STREAM_END. The default-options decoders also
read every gzip member.
- Untyped JavaScript inputs may be strings, TypedArrays, DataViews or
ArrayBuffers; other values throw Node's ERR_INVALID_ARG_TYPE after the
options are checked.
- Results are Buffers, as in Node: they convert to their text.
Corpus programs 5378, 5379 and 5415 cover the sync, callback and untyped
input forms.