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.
`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).
The three glob entry points run a port of Node 24's
lib/internal/fs/glob.js walk in scr_glob.c: the same LIFO queue of
(path, patterns) items over the sorted directory listings, the same
lstat, stat, readdir, realpath and seen-pattern caches, the same result
set in insertion order, and the same exclusion checks, so matches come
out in Node's order. Patterns compile with the existing minimatch port.
glob_match is split into a compile step and a test step so the walk can
reuse each compiled pattern and test it segment by segment.
nocaseMagicOnly is now an option, because exclude patterns turn it off.
Patterns (a string or string array) and options (cwd as a path or
file: URL, exclude as patterns or a function, followSymlinks,
withFileTypes) are validated in Node's order with Node's errors,
including the internal assertion Node hits for a non-string cwd with
exclude patterns. withFileTypes results are Dirent rows: name,
parentPath and the type probes. parentPath follows Node's rule for each
form: the joined path for globSync, the resolved path for fs/promises
glob.
fs.glob checks the callback and validates synchronously, then delivers
string[] or the walk's error through the filesystem callback queue.
fs/promises glob lowers as the source of a for await loop. The walk runs
when the loop starts, and the matches found before an error bind before
the error is thrown, as Node's async iterator yields them first. The
older glob(3)-based fs.glob branch is removed.
Corpus 5448-5452 and a node-types fixture check the behavior against
Node.
A record parameter of a function the record-flow analysis does not track
widens to the fields its own arguments can leave undefined, and since the
flow-class change those arguments include object literals and call
results. Another call can pass that parameter a value stored as
`R | undefined`: a for-of element whose loop the element analysis cannot
prove free of holes binds `R | undefined` while the checker types it `R`.
Coercing that union into the widened layout of R found no mapping and
failed with SC2003, where the declared parameter used to take the checked
extraction of the record arm.
A union of one record arm and unit arms flowing into a record that arm
lifts to now takes the same checked extraction, then the record's width
lift into the destination layout.
socket.ref()/unref() and server.ref()/unref() decide whether the handle
keeps the event loop alive, on typed handles and through checked-dynamic
handles. The loop's net liveness now counts only ref'd sockets and
servers; unref'd ones still deliver their events while other work keeps
the loop running, because the sleep polls the net unit's fd whenever any
handle exists.
Corpus 5595 covers the server, client and accepted sockets.
The runtime appends to a string in place when the left operand is
uniquely owned, but three common forms never got there:
- `obj.field += s` on a plain class or record field kept a second
reference to the old value, so every append copied the whole string.
The compound assignment now reads a stored string field into a mutable
local, stores "" into the field (no code runs in between: both operands
are already strings), runs the self-append `old = old + rhs` and stores
the result.
- `s = s + a + b` copied per append: the self-append recognized only
`s = s + x`. A concat chain whose leftmost operand is the target now
takes the remaining operands as the suffix, in the same order.
- `s += String.fromCharCode(c)` built a one-character string first.
scr_str_concat_move_char_code appends the character, in place for ASCII.
Self-appends also borrow the suffix instead of retaining it, and a
discarded compound assignment no longer retains and releases its result.
Release builds now emit the string retain and the dyn retain and release
fast paths inline. The dyn release decides the cases scr_dyn_release
decides by kind and calls scr_dyn_release_survivor for capsules and
handles. String releases and the other families stay runtime calls, as
string-heavy programs have many release sites. The speed posture keeps
every fast path and gains the dyn release. The runtime ABI version moves
to 21 because program objects call the new runtime entry point.
docs/content/docs/limitations.mdx now holds the frontmatter, the
headings, the anchor spans and one <include> line per partial. The text
moves unchanged to docs/content/limitations/: one file per labeled
bullet (<section>/<label>.mdx) and one per section's other text
(<section>.mdx). Edits to different bullets no longer touch adjacent
lines of one file.
remarkDocsConventions splices the included partials into the page and
joins the lists that consecutive bullet partials contribute, so the page,
its Markdown twin and the search index render as before. The route tests
read the page with its partials inlined. A page that includes partials
takes its last-modified date, used by the sitemap, from the newest commit
to the page or any of its partials.
docs/scripts/limitations-partials.mjs joins the partials back into one
file, splits a single-file page, and merges a branch's edits to the
single-file page into the partials.
indexOf and includes on arrays of class instances, records, arrays, maps,
sets, typed arrays and strings call an internal helper that LLVM inlines.
When the array's hole-free prefix covers its whole length, the helper
scans the dense slots itself: references compare as 64-bit slots, four
per branch, and strings compare by pointer, then length, then bytes, with
strings of up to 16 bytes compared as two words and longer ones through
scr_str_eq. Arrays with holes or sparse entries, and every other element
type, keep the runtime search.
randomFillSync(new Uint16Array(32), 8.5, 8) fills bytes 17 to 32, so the
last 4-byte group the check prints holds one random byte, which is zero
1 time in 256, in Node as well. The check now ORs the bytes of eight
fills before it reports the groups. The expected output is unchanged.
libuv's uv_fs_scandir sorts entries with strcmp on POSIX hosts, so Node's
readdirSync, readdir and fs/promises readdir list names in byte order
(with and without withFileTypes). The runtime returned the directory's
raw order, which differs from Node on most filesystems. The POSIX
scandir listing now sorts its names and entry kinds together, and
scr_fs_readdir reads names from that listing as the Windows path already
did. Windows keeps the directory's own order, as libuv does.
Corpus 5447 checks the listings against Node.
- Object.keys(util.types) lists Node's 43 keys in Node's order, and
Object.keys over the node:util/types namespace lists them sorted with
"default". Object.values and Object.entries over util.types are refused,
because some members do not compile yet.
- The HTTP server answers 431 Request Header Fields Too Large when a head
or trailer block exceeds 1000 fields or the header byte bound, and the
trailer block counts its fields on its own, like Node. Parser errors send
Node's fixed responses without a Content-Length header.
- Server requests start paused like Node's IncomingMessage: body bytes and
'end' wait for a 'data' listener, pipe() or resume(). When the response
finishes, an unread request is dumped and emits 'end', before the
response's 'close' for an empty body and after it otherwise. A body that
is still arriving when the response finishes keeps reaching its
listeners. req.complete reads true once the body has arrived.
- Document that small Buffers own exact-size ArrayBuffers instead of
sharing Node's 64 KiB pool.
Corpus 5405-5409 cover each case.