fs.cpSync, fs.cp and fs/promises cp compile with every option:
dereference, errorOnExist, filter, force, mode, preserveTimestamps,
recursive and verbatimSymlinks. They follow Node's two implementations.
cpSync follows cp-sync.js over the fs binding: copy errors are plain
Errors with a code, std::error_code failures throw an ErrnoException,
and links inside a copied directory point at their canonical targets.
fs.cp and fs/promises cp follow cp.js, whose copy errors are
SystemErrors with code, info, errno, syscall and path.
Options and paths validate in Node's order with Node's errors. filter
runs for the top-level paths and each entry in Node's order; an async
filter must return a promise that is already settled, and a pending one
throws SC2020. The callback and promise forms complete the copy before
the callback runs or the promise settles. Windows builds throw SC2020.
The copy lives in the new scr_fs_cp.c. Directory walks keep their frames
on the heap, so deep trees also copy on a fiber's stack. File contents
copy with sendfile on Linux, and COPYFILE_FICLONE uses FICLONE
where it exists. ScrError gains the SystemError context and the
ErrnoException key order, with matching fields in the compiler's Error
layout. The stored fs.cp callback uses the same implementation.
Corpus 5567-5571 cover the sync, callback and promise forms, errors,
symlinks and 1500-level trees.
union, intersection, difference, symmetricDifference, isSubsetOf,
isSupersetOf and isDisjointFrom now compile with a Map argument whose key type
is the Set's element type, as well as with a Set. A Map's set record reads its
keys, and its size, has and keys are the builtins, so the helper loops walk
and test the Map's keys in the order the spec gives. Other set-like arguments
are still refused.
Node's util.promisify returns the custom promisified form of setTimeout
and setImmediate, which is timers/promises' function of the same name.
A const binding of promisify over the global functions, the node:timers
named imports or the node:timers namespace members now registers that
timers/promises member as the projection, so calls through the binding
lower as the timers/promises call: they fulfill with the given value
(undefined without one) after the delay or in the check phase.
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.
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.
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.
`const parsed: { score: number }[] = JSON.parse(doc)` printed a different
checksum from Node: the checked conversion (and the schema-directed
JSON.parse fast path) stored only the declared fields of each object in
its native record, so JSON.stringify(parsed) silently dropped `id`,
`name` and `active`, and every other reflective surface agreed.
A closed record converted from an ordinary object now keeps the source's
own properties beside it when the object has undeclared keys or holds the
declared ones out of declaration order. The runtime keeps a per-thread
table from the native record to a view of the source's own keys
(placeholders for declared keys, which the record still owns), released
through the native dispose hook when the record is freed:
- the checked record builder notes its source (scr_record_source_note);
- the schema parser parses undeclared members into the view instead of
declining, so typed parses keep their one-pass fast path;
- typed JSON.stringify writes such records in source key order, declared
fields through a generated per-shape field writer and the others through
the dyn writer;
- record capsules adopt the view and refresh it in place, so untyped reads
and writes see and persist undeclared properties;
- util.inspect and Object.keys/values/entries answer through the live view
(values/entries check the extra values into their static result type and
throw the checked-conversion TypeError when one does not fit).
Copies made by object spread, Object.assign, rest destructuring or
conversion to another record type still hold only the declared fields;
the limitations page documents this.
The partial for the "TLS ciphers" section was tls-cipher.mdx, while the
limitations tooling names a section's partial after its heading's slug
(tls-ciphers). It is renamed, so a join and split of the page leaves the
docs unchanged.
dgram.Socket's setBroadcast, setTTL, setMulticastTTL,
setMulticastLoopback, setMulticastInterface, addMembership,
dropMembership, get/setRecvBufferSize, get/setSendBufferSize,
getSendQueueSize, getSendQueueCount and disconnect were refused at
compile time. They now lower to scr_dgram.c and match Node:
- The TTL setters and setMulticastLoopback return their argument. The
value checks run first: a TTL outside 1-255 (0-255 for multicast,
after ToInt32) throws "setTTL EINVAL" with code, errno and syscall.
- A socket that has not been bound has no descriptor yet, so the
options throw EBADF, and the buffer size methods throw Node's
ERR_SOCKET_BUFFER_SIZE SystemError.
- After close(), the members that read Node's null handle throw its
TypeError ("Cannot read properties of null (reading 'setTTL')"), and
setMulticastInterface and the membership calls throw "Not running".
- Buffer sizes must be uint32 values (ERR_SOCKET_BAD_BUFFER_SIZE), 0
reads the current size, and sizes past the int32 range fail as EINVAL.
- IPv6 interfaces and groups go to the IPv6 socket options, so a udp4
socket answers ENOPROTOOPT as in Node.
- disconnect() throws ERR_SOCKET_DGRAM_NOT_CONNECTED until 'connect' was
emitted, and dissolves the association with an AF_UNSPEC connect.
addMembership and dropMembership before bind() throw SC2020, where
Node binds the socket to an ephemeral port first; the limitations page
documents it, along with the missing info object on the buffer size
SystemError. Corpus 5655 and 5656 and the node-types fixture
dgram-options.ts cover each method.
Hash and Hmac values are Transforms, as in Node: they can be written
and ended, piped into, used as pipeline() and stream/promises pipeline
stages, read, and listened to, and setEncoding() applies to the digest
they push. Each value gets one native Transform on its first stream use.
The stream refers to its value without a reference; a pipe destination
or pipeline stage holds the value until the stream flushes or is
destroyed, and a destroyed stream is released so that listeners that
capture the value do not keep it alive.
After the stream flushes, a Hash answers one more digest() with the
same digest and then throws ERR_CRYPTO_HASH_FINALIZED, and update()
throws ERR_CRYPTO_HASH_UPDATE_FAILED; an Hmac answers digest() with an
empty digest. A stream write after digest() reports
ERR_CRYPTO_HASH_UPDATE_FAILED through 'error'. copy() of a Hash whose
stream flushed, and stream use of a value after its stream closed,
throw a coded SC2020 error.
Writing a checked-dynamic value that is not null, a string or a view to
any stream now throws ERR_INVALID_ARG_TYPE; it used to report the
null-values error.
Corpus 5760-5762 cover the piping, pipeline, setEncoding and event
forms, the states after a flush, and the JavaScript forms, and
errors.test.ts covers the two SC2020 errors.
A const binding of promisify(exec) now lowers like promisify(execFile):
each call runs the command line with /bin/sh -c, as execSync does, and
settles with { stdout, stderr }. A non-zero exit, a missing command (the
shell's 127) or a timeout rejects with Node's "Command failed: <command>\n
<stderr>" message. The options are the execFile slice: encoding utf8,
cwd, env (inline spread included), timeout, and maxBuffer accepted.
Three gaps closed for both targets along the way:
- the { stdout, stderr } result maps as a record when it is held in a
variable, passed or returned, instead of only when destructured at the
await (it is an anonymous literal in @types/node and in the fallback);
- a namespace member is a recognized target (promisify(cp.exec));
- util.promisify(...) through a namespace or default import is
recognized like the named import.
Every release of a LifetimeRegion, and every retry at region creation, ran
scr_collect_cycles(): full cycle-collector passes over the thread's heap, to
a fixpoint, before checking any region, in case dead cycles of ordinary
objects held counted references into it. Freeing a region ran them again
whenever the owned ordinary objects left counts behind after the region
released its fields. Next to a large heap of candidates (a type checker
beside per-file emit regions) those passes cost far more than the regions.
A retry now checks the due regions first. A region whose references are all
explained frees without a pass; only when one would be kept does the heap
get the full pass, after which every due region is judged as before. The
first check leaves a region it cannot free as it was, so the backoff, the
kept report and SCRIPTC_REGION_DEBUG see only the check after the pass. It
keeps what its walk found: a region whose counted references the pass did not
change is judged from that, without walking it again. A pass that freed a dead
cycle referring only to an owned object leaves the count unchanged; such a
region waits for its next attempt.
After a region releases its instances' fields, what can still hold counts
is the owned ordinary objects' cycles. Each has a member that lost a
reference in those releases and is buffered as a candidate, and the release
walk entered every cycle member. The cycle collector gains a pass limited to
a set of candidates (scr_cyc_collect_set): it takes the set's members out of
the buffers up to a given generation, reading only the buffers' pointers, so
the candidates it leaves are not touched, and it walks at full depth without
moving the schedule's baselines. A dead object such a pass reaches through a
candidate it did not take leaves its buffer before it is freed. Freeing a
region runs these passes over the walk's objects from the youngest buffer
out, and the whole heap only if counts remain.
SCRIPTC_REGION_STATS also prints, per thread at exit, how many full passes
over the heap regions needed.
new MessageChannel() from node:worker_threads makes two entangled
MessagePorts. They share the handle model of parentPort: postMessage
with the existing message clone, 'message' and 'close' events,
on/once, start, close, ref, unref and hasRef. A port's 'close' reaches
the other port after its queued messages. receiveMessageOnPort(port)
answers the next queued message as { message }, or undefined.
A channel is a thread-neutral record (scr_mailbox.c): each end has its
queue and the inbox of the thread that owns its port. A port listed in a
transfer list, given as postMessage's array or { transfer }, or as the
Worker constructor's transferList, travels with the message, and the
decoding thread adopts it. The sending port becomes unusable. A port in a
message without that listing throws DataCloneError, and so does a port
listing itself.
Port events follow Node's loop phases. Each port's queued messages
arrive as one batch, ports in creation order, before setImmediate
callbacks, with nextTicks and microtasks after each message (a new loop
hook keeps the pass going until the batch is delivered). A port's own
'close' runs in the following close phase, the latest close() first. The
other port's 'close' arrives on a later poll, in the order the notices
were sent, and only while the loop keeps running.
When the main thread's script ends, the executable released every module
global and ran a full cycle collection before exiting, which takes time
proportional to everything the program still holds (about 15 ms for
1.5 million live objects) and frees memory the operating system reclaims
anyway. The main thread now skips those releases, the exit-time
collection and the collector's buffer cleanup. Exit listeners and output
flushing run as before. Worker threads still free their heaps when they
exit, since later threads reuse their arenas, and builds that check for
leaks (the RC audit, AddressSanitizer) free everything as before, as does
any process run with SCRIPTC_EXIT_FREE=1 for external leak checkers.
SCRIPTC_WORKER_STATS=1 reports, as each worker thread exits, how long it
took to start, to run its script and to exit.
Buffer.from of strings, arrays and Uint8Arrays, Buffer.allocUnsafe and
Buffer.concat now return views into a per-thread pool slab of
Buffer.poolSize bytes (64 KiB by default) when the size is below
Buffer.poolSize >>> 1, following Node's allocate, fromStringFast and
fromArrayLike: 8-byte aligned offsets, a new slab when a request does not
fit, and the four-bytes-per-unit reservation for strings. Buffer.poolSize
can be read and set, and Buffer.allocUnsafeSlow returns an unpooled Buffer.
A slab lives while any of its views does. Its ArrayBuffer cannot be
transferred, and structuredClone of a typed array view copies the whole
ArrayBuffer and keeps the view's offset. Buffer.alloc, allocUnsafe and
allocUnsafeSlow validate sizes with Node's ERR_OUT_OF_RANGE error.
The first pooled allocation finds the startup slab, which has the
default 64 KiB size. A request it cannot hold gets a slab of the current
Buffer.poolSize, as in Node, and the room is checked after every new
slab, so a Buffer.poolSize raised before the first pooled allocation
cannot place a reservation past the end of a 64 KiB slab. A reservation
larger than a fresh slab traps instead of writing past it.
Corpus 5531 (typed) and 5532 (untyped) raise Buffer.poolSize before the
first pooled allocation and allocate just under the new threshold
through each pooled path, each case in a fresh child process, and also
shrink Buffer.poolSize and change it between allocations. The
node-types fenced fixture no longer lists Buffer.poolSize, which now
compiles.
node:test's mock.fn(original[, implementation]) and t.mock.fn compile.
A Mock<F> is the function-with-properties record { %call, mock }: the
callable is a wrapper closure that shares the mock context with the mock
field. The context records each call's arguments, result, error and this,
and keeps the current implementation, the original and the one-time
implementations by call index. fn.mock supports, as in Node 24.22:
- calls, read as a copy of the log;
- callCount();
- mockImplementation(fn);
- mockImplementationOnce(fn[, onCall]), for the next call by default, a
later registration for the same index replacing the earlier one;
- resetCalls();
- restore(), which brings back the original.
A throwing implementation records the error and rethrows it. Functions
with fixed parameters or a single rest array parameter are supported; a
union return type needs an undefined arm. mock.fn() without an original,
mock.method, the tracker's reset and restoreAll, and t.mock.method are
refused with SC2020.
The node:test fixture mock-fn compares the behavior with Node, and
tests/diagnostics/mock-fences.ts covers the refusals.