When a 'newListener' listener threw while events.once registered its
listeners, the exception escaped the call, the 'error' listener's closure
was never registered or released, and the shared state and its promise
leaked with it. Node registers both listeners inside the promise's
executor: the throw rejects the returned promise, and the listener whose
announcement threw is not added (the event's listener stays when only the
'error' announcement throws).
scr_events_once now takes the pending exception, takes back the listener
whose announcement threw without a 'removeListener' event, releases the
'error' closure when it was never registered, and rejects the promise
with the exception.
events.once(emitter, name) was refused. The emitter dispatches statically
emitted events to typed listeners through invokers built per argument
tuple, so the lowering takes the event's tuple from the program-wide event
table and builds a capture-free function that boxes that tuple into the
any[] Node fulfills with, and one for the 'error' tuple.
scr_events_once registers both as one-shot typed listeners behind emitted
adapters (emitterOnceAdapter). The first to fire settles the promise
(fulfilled with the arguments, or rejected with the error) and removes
the other, as in Node; awaiting 'error' itself registers only the one
listener. The shared state holds the emitter and the listener closures
weakly, so a never-fired once creates no cycle.
Options, computed or meta event names, EventTarget and stream receivers,
and events emitted with computed names or checked-dynamic listeners stay
refused. The fallback declarations type the result Promise<unknown[]>;
under @types/node (Promise<any[]>) TypeScript code narrows it to
unknown[].
When appending one more entry would grow a hashed number-key map's
index, Map.set and Set.add first look the key up to decide whether the
map should move to the direct index. A key that lookup finds was then
looked up again by the generic set. The found entry now takes the new
value directly, so overwriting a present key in a map at its growth
boundary probes the table once.
Number-key reads of a map without the direct index called
scr_map_entry_f64 for every lookup. The internal sc_map_entry_f64 helper
now calls sc_map_entry_f64_hashed for such maps instead: it hashes the
key as scr_map_find_f64 does (SameValueZero key word, scr_key_mix, the
absent-key filter), scans the entries of a small map or probes a compact
bucket table, and calls the runtime lookup only for a table wider than
the compact limit. It stays out of line so the direct-index path stays
small at every call site.
The runtime fallback is called as a read-only call, so both helpers only
read memory and LLVM merges lookups of one key when nothing is stored in
between, such as a has() followed by get().
The runtime benchmark suite gains number-maps: a node table indexed by
dense ids, scope tables keyed by sparse ids, and get/has/set mixes.
- dns.lookup(hostname, callback), lookup(hostname, family, callback)
and lookup(hostname, options, callback) with family (0/4/6, 'IPv4',
'IPv6'), all, order and verbatim, validated with Node's messages.
getaddrinfo answers any family, ordered verbatim or one family first;
literal addresses answer on the next tick, an empty hostname gives
Node's null answer and DEP0118 warning, and failures carry code,
errno, syscall and hostname.
- dns.lookupService(address, port, callback) checks the address and
port as Node does and answers getnameinfo's host and service.
- dns.getServers() lists the nameservers in /etc/resolv.conf, and
getDefaultResultOrder/setDefaultResultOrder keep the default order
('verbatim').
- A typed callback receives "" and NaN (or []) after a failure, where
Node passes undefined; the limitations page says so.
Corpus 5648-5650 cover each case; the options-record diagnostics no longer
list the all and hints options as refused.
The server's 'connection' and the client's 'connect' race, and macOS runs
them in the other order from Linux. 5644 now prints both sides once both
have run, and 5645 keeps the server's and the client's events in separate
lists printed one after the other.
- socket.remotePort, remoteFamily, localAddress, localPort, localFamily,
bytesRead, readyState, connecting and pending. The peer's fields are
cached at first read as remoteAddress is (undefined while
connecting), and the local ones are undefined while connecting and
after close.
- write(data, cb) runs cb with null once the data is flushed, and
end(cb) / end(data, cb) once the socket finishes.
- 'close' listeners receive hadError, 'ready' fires after 'connect',
and 'data' listeners may take the string chunks setEncoding gives.
- setTimeout(ms, cb) adds cb as a once('timeout') listener (0 removes
it), and setTimeout throws ERR_OUT_OF_RANGE for a negative, NaN or
infinite msecs where it disabled the timer before. setKeepAlive
chains.
- server.maxConnections closes connections past the cap and emits
'drop' with their addresses; server.getConnections(cb) answers the
count on the next tick.
- net.isIP, isIPv4 and isIPv6 follow Node's address patterns.
- A callback adapted to a void return keeps the identity of the
function it wraps, so removing it by that function finds it.
Corpus 5644-5647 cover each case.
The fs validation ladder and the callback dispatch check their options
argument as Node's getOptions does. null, undefined and functions take
the defaults, and a string is the encoding. Numbers, booleans, bigints
and symbols throw ERR_INVALID_ARG_TYPE ('The "options" argument must be
one of type string or object'). assertEncoding lets 'buffer' through;
before, every 'buffer' encoding threw ERR_INVALID_ARG_VALUE.
With 'buffer':
- readFile and readFileSync read the file first, so a missing file
reports ENOENT. Then buffer.toString('buffer') throws
ERR_UNKNOWN_ENCODING; an empty file answers "".
- fs.mkdtemp's callback receives the path as a Buffer.
- mkdtempSync, opendirSync, createReadStream and createWriteStream
throw the fence error.
fs.mkdtemp's callback form now checks its encoding. It also returns its
path in the requested encoding: re-encoded text for names other than
utf8, as the binding does.
Corpus 5545 covers these forms against Node 24.22.
In worker-enabled executables a typed-array access tests whether the view
is shared. The test used to come before the index check, and the shared
path called scr_bytes_get_or_nan/scr_bytes_set with the index as a
double, then converted the element back to an integer. Both paths now
use the index the inline check produced: the shared test follows the
check (a view's length never changes), and the shared path calls new
runtime entries that take the index as a size_t
(scr_bytes_shared_get_int, scr_bytes_shared_get_f64,
scr_bytes_shared_set). Integer elements join as one 32-bit value and
convert to a number after the join, so compares and ToInt32/ToUint32
of the result stay integer operations in every element kind.
The amortized worker cancellation poll moves from the loop header to
the loop's back edge (the continue target). In the header it was a loop
exit ahead of the body, which kept LLVM from rotating the loop and left
loads such as the view's length inside it.
Adds the typed-array-scan and typed-array-scan-workers runtime
benchmarks: Uint8Array, Uint16Array, Int32Array and Float64Array scan
loops in an ordinary and a worker-enabled executable.
The npm case fixtures lost their only native test with the native-only
compilation change. tests/harness/npm.test.ts runs every case again with the
default static npm compile and compares it with Node. A KNOWN table records
the cases the compile refuses (with the diagnostic code), the cases whose
run reaches a deferred refusal (the code in the program's output), and the
two that still diverge, which run as expected failures. The suite joins the
case-sharded CI corpus files.
Fixes from the run:
- A package imported only by subpath (`pkg/env.js` from a package with no
"main", "exports" or index file) was refused as having no runtime entry.
Eligibility now resolves the imported specifier when the root has none.
- diagnostics_channel: a throwing subscriber stopped publish, and a
throwing bindStore transform stopped runStores. Node runs the remaining
subscribers, skips that store, and reports each error as uncaught on a
next tick; the runtime now does the same.
- A '#' import that the package's "imports" field maps to another package
failed at run time with Node's "not defined" error, which Node does not
raise. It is now a compile-time SC1010 refusal.
- An uncaught runtime refusal whose code is an SC code now names the code.
- Mapping a type in the optional array-element analysis could lower an
unsupported local class and let its PoisonError escape as a compiler
crash; the analysis now treats that element ABI as unknown, so the
program fails with the recorded diagnostic.
- A lexical module binding initialized from an unchecked array element
holding a function failed validation (SC9001): its widened storage was a
union, which a TDZ guard cannot hold. Such bindings now keep checked-value
storage and their guard.
- A dynamically reached ArrayBuffer had no constructor property.
The backend converted size_t lengths and indexes to numbers with sitofp. On
wasm32 size_t is 32 bits, and an array length reaches 2^32 - 1, so a sparse
array of length 4294967295 read -1. sizeToDouble picks uitofp for a 32-bit
size_t and keeps sitofp for 64 bits, whose sign bit a length never reaches,
so native output is unchanged.
An unsettled top-level await always exited 13. Node exits with
`process.exitCode || 13` and runs the exit listeners first, which can set
process.exitCode again. scr_promise_finish_top_level now notes 13 only when
the program set no nonzero exit code, and the pending exit returns the
exit code after the listeners, as the other exits do.
Three WASI tests still exercised the embedded-JS fallback that native-only
compilation removed: an embedded-source fetch scan and two npm fixtures
that every target now refuses. They now check the same behavior with
static programs: fetch named only in text compiles, a global fetch call
reports SC3002, and CommonJS and ESM packages compile and match Node.
opendirSync(path[, options]) and fs.promises.opendir(path[, options])
return a Dir that reads one Dirent at a time in OS order, as libuv's
uv_fs_readdir does. Dir supports path, readSync(), read(), closeSync(),
close() and for await. readSync and read answer null at the end. A
closed Dir throws ERR_DIR_CLOSED from readSync and closeSync, and
rejects with it from read and close.
for await over a Dir reads with read() until null and closes the Dir
in a finally, as Node's Dir.entries() does: normal exhaustion, break
and a throw all close it, and a loop whose body closes the Dir ends
with ERR_DIR_CLOSED.
A Dir also crosses 'unknown' slots as a native handle (Dir), so a
closure returning one can be passed to untyped JavaScript functions.
Through such a slot, path and closeSync() work, and other members throw
the "not supported yet" Error.
Open failures carry the opendir syscall and no path. options.bufferSize
is checked after the directory opens, with Node's ERR_OUT_OF_RANGE
messages. The options must be an object literal with a numeric
bufferSize and a utf8 encoding. Recursive walks, buffer encodings and
the callback forms are not lowered. TypeScript sources get a compile
diagnostic for them; in JavaScript sources, recursive walks and the
callback forms throw the fence error when the call runs.
Corpus 5544 covers both forms against Node 24.22.
The header-limit program sent heads of up to about 19 KB with long field
names. macOS loopback delivers that in more than one segment, so Node's
server rejected the head with bytes still unread and its close reset the
connection; the client socket had no 'error' listener, so Node exited on
the ECONNRESET after printing the first 431 case.
Every field is now a minimal `h:` line, which keeps each request under
7 KB, and the client socket ignores a reset so the case still ends. The
cases and their output are unchanged.
A function body with more than 256 inline dyn release sites now calls
scr_dyn_release at every site. Generated dispatch tables and the module
initializers and exit paths of large packages have thousands of release
sites, and LLVM's optimization time grows faster than their number:
published Effect schema programs took over 9 minutes to compile.
Sanitized builds compile the program in one AddressSanitizer pass, so
they define the inline reference counting helpers noinline and call
them at each site.
wasm32-wasi executables always compiled their program as one LLVM module,
so the object step of a large program ran the whole O2 pipeline and code
generation on a single thread. Native targets divide a module of more than
4 MB of LLVM text into up to 64 partitions that the helper optimizes and
compiles concurrently, importing small functions across partitions for
inlining. Nothing in the helper or the link plan is specific to native
objects: partitions link as ordinary objects with wasm-ld.
nativeProgramPartitions now applies the same size rule to every target and
no longer takes the target. A WASI test emits a program whose internal
callees, state and table entries land in different partitions, at release
and dev optimization, and checks that the partitioned objects are
deterministic and link to the same program as one object.
`x % c` for a constant c whose magnitude is a power of two of at least 1
now computes `x - trunc(x / c) * c`, which is exact for every double, and
gives a zero result the dividend's sign. Divisors written as `2 ** k`
count as constants. Other divisors keep frem, and a fractional constant
divisor skips the integer check it can never pass.
The inline integer path for unproven operands now accepts dividends up
to 2^62 instead of 2^53. The remainder of two doubles is a double, so
the conversion back is exact, and products such as
`seed * 1103515245 + 12345` no longer call fmod.
A Dirent row that becomes a dynamic value (untyped JavaScript, unknown
slots, rows passed from a typed module) was a plain object: its type
probes were missing, so `readdirSync(dir, { withFileTypes: true })
.map((d) => d.isFile())` in a .mjs program threw "d.isFile is not a
function", and Object.keys, JSON.stringify and util.inspect showed the
internal %dtype field.
Both record-to-dyn converters now finish Dirent rows with
scr_dyn_dirent_finish. The row gets a shared Dirent prototype that holds
isDirectory, isFile, isBlockDevice, isCharacterDevice, isSymbolicLink,
isFIFO and isSocket as non-enumerable methods. The entry kind is stored
under an own Symbol(type) key, as in Node, and %dtype becomes
non-enumerable, so a dynamic Dirent still converts back to the typed
row. Live record ops are not emitted for Dirent rows, so key walks read
the finished view. A method called with a nullish receiver throws Node's
TypeError.
util.inspect of a dynamic object now skips non-enumerable properties and
lists own enumerable symbol keys after the string keys, as Node's
default view does. Typed Dirent rows render through the dynamic view, so
both print `Dirent { name: 'a.txt', parentPath: '.', Symbol(type): 1 }`.
The finisher lives in scr_symbol.c. Programs that have the Dirent record
shape link that unit.
Corpus 5565 and 5566 check the untyped and cross-module forms against
Node.
A function that returns a record its caller only projects fills the
caller's frame storage through a %into.<name> entry. These entries are
small and called once per use, but LLVM kept them out of line in loops:
record-boundaries' pathLength called %into.pointAt twice per iteration.
They now carry inlinehint, like the indexed-read and interface-dispatch
helpers.
Once inlined, the scalar array reads in them still blocked hoisting the
arrays' header loads: the slow path's scr_arr_state and getter calls had
no memory attributes. They are now read-only cold calls, as the dense
read fallbacks already are (the getter runs only for a present element,
so it does not trap).