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 cases were [label, run] tuples in an array, which JavaScript inference
types as (string | (() => void))[]. console.log(label, ...) then received a
union that includes a function value, so coverage counted two statements as
runtime fences. Each case now calls attempt(label, run) directly; the
output is 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.
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 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.
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.
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.
promisify(fs.X) over a named or namespace import of node:fs lowers as
the fs/promises member of the same name for access, appendFile, chmod,
chown, copyFile, lchown, link, lstat, lutimes, mkdir, mkdtemp, readFile,
readdir, readlink, realpath, rename, rm, rmdir, stat, statfs, symlink,
truncate, unlink, utimes and writeFile. For these, Node's promisified
callback form settles with the callback's single result and rejects
with the same errors as the fs/promises call. Calls through the bound
function accept the forms the fs/promises lowering accepts and refuse
the others by name.
fs.open (an fd number where fs/promises.open gives a FileHandle),
fs.exists (a custom promisify) and fs.read/fs.write (multi-value
callbacks) remain refused.
The program reads the same files through readFileSync with "utf8",
"utf-8" and an options object, the fd form, fs.promises.readFile and
FileHandle.readFile, and compares each with Buffer.toString("utf8"):
valid files (ASCII, 2- to 4-byte characters, a BOM, empty) and invalid
ones (stray continuation and lead bytes, encoded surrogates, overlong
forms, sequences cut at the end of the file).
fs.statSync(path, options) and fs.lstatSync(path, options) now compile
with an inline options object, where throwIfNoEntry takes any boolean and
bigint must be false, or with a stored options object whose only member
is throwIfNoEntry, read at each call. With throwIfNoEntry: false the
result is Stats | undefined, and a missing entry (ENOENT, or ENOTDIR for a
path through a file, a dangling link's stat included) answers undefined
without building the error object, as Node's binding does. Other
failures still throw. Before, any second argument was refused.
Stats | undefined is a nullable union, so the result is the Stats pointer
itself (NULL for undefined) rather than a union box, and Stats snapshots
come from the small-object allocator. A stat-and-test probe such as
`stat !== undefined && stat.isFile()` therefore makes no system
allocator calls. stat?.isFile() and the other type tests also lower
through optional chains.
existsSync answers false for a path with a null byte, as Node does,
instead of testing the path up to that byte.
Corpus 5747 and 5748 cover the typed and JavaScript forms.
"Append to strings in place for fields, concat chains and character
codes" made `obj.f += s` linear by handing the field's reference to a
local before the concat. Two neighbouring forms still copied the whole
string on every append, so their loops stayed quadratic:
- `parts[i] += s` on a `string[]`: the old element reached the concat
through a retained snapshot (the hidden %compoundOld local) while the
slot still held it. 1.6M appends spread over four slots took 11 s.
- `o.f = o.f + s` written out: the field read retains a snapshot and
the field keeps its reference until the store.
A statement `a[i] += rhs` on a `string[]` now lowers to
`a[i] = (hole ? "undefined" : a[i]) + rhs` with the array and index saved
first (evaluation order unchanged). The backend recognizes that store and
the direct `recv.f = recv.f + suffix` on a plain string class or record
field. After the snapshot and suffix are evaluated and the store guard
ran, a field unlinks and releases its current value; an array slot in
the hole-free prefix that still holds the snapshot is swapped for the
immortal empty string and released. The concat then appends in place
when nothing else holds the string, and the result is stored as before.
Both paths unlink only when the snapshot is frame-owned (or immortal).
Aliases, interned literals, throwing suffixes, suffixes that read or
replace the field or element, rebound receivers and indices, holes,
appends past the end and appends that grow the array keep their
semantics (tests/corpus/string-field-append.ts, which also pins the
field `+=` forms).
A for-of over a user iterable calls next() once per step, and a next()
that builds its { value, done } record with `this.i++` (or a compound or
nullish assignment) lowers a field through a seqExpr. Record result
entries rejected any seqExpr, so every step allocated and released a
heap record. The result entry splits a returned literal into one store
per field; that is safe for a field's seqExpr as long as no other field
names a hidden local declared inside it, which is now checked. Other
seqExpr placements keep the original rejection.
A 30M-step class-iterator loop: 200.7M -> 63.2M instructions per 3M
steps (callgrind), 150 ms -> 40 ms.
A native synchronous generator body whose yield channel is a number or a
boolean caches its ScrGen's OUT slot at entry and stores each yield into
it directly instead of calling scr_coro_yield_f64/bool; for-of, spread
and yield* drives read such values back inline instead of calling
scr_gen_take_out_f64/bool. The IR validator types every yield operand as
the channel, so OUT never holds a reference at a yield and the store
needs no release. The emitter mirrors the slot as %ScrGenHead and
%ScrGenSlot; scr_async.c pins both layouts and the tags with static
assertions. WASI keeps the call paths.
A generator frame owns its parameters until its final exit, across every
suspension, so an unchanged, unboxed reference parameter is a stable
owner that reads may borrow (CallLifetimes.generatorParameters). Calling
a callback parameter on every step no longer retains and releases it.
Generator declarations entered only through direct (spawn) calls are now
constant-callback consumers: when every call passes the same function,
the body enters it through the guarded direct path, which inlines it and
drops the dynamic-`this` bracket for callees that cannot observe it.
Both hierarchies construct the base and the subclass, so the base-typed
call sites are closed sets now: they still load the vtable slot, then
call each implementation directly behind a slot test.