With SCRIPTC_INFER_JS=1, shape inference writes its overlay for the program's own JavaScript files, with or without npm-static packages, and leaves package files as they are. The analysis and overlay keep a value untyped where an inferred type would be narrower than what the program holds: awaited values of for await, iterable object literals with computed keys, matchAll and other builtin results, fields an instance may lack, overridden methods and getters, class instances reached by reflection, constructor parameters stored into wider fields, and results the analysis cannot type. A computed member name no longer folds a read that runs before its binding is initialized. In every build, a JavaScript binding written with a value that may be a string, or with a union, keeps the dynamic representation.
The event loop slept to its next timer with nanosleep. macOS applies the
timer slack of the thread's QoS class to nanosleep: a 5 ms sleep takes
about 38 ms at utility QoS and 180 ms at background QoS, where launchd
services such as CI jobs run. Timers then fired out of order against
Node, whose libuv loop waits with a kevent timeout that keeps its
deadline at every QoS. On macOS the loop now sleeps with a kevent timeout
on a kqueue with no events, shared by every thread's loop.
Intervals also re-arm from the time their callback started, as Node's
listOnTimeout does, instead of from the clock after the callback: a
callback that takes 30 ms of a 50 ms interval keeps the ticks 50 ms
apart, and one slower than the interval makes the next tick due on the
next turn. interval-slow-callback.ts checks both against Node.
Node sets SIGPIPE and SIGXFSZ to SIG_IGN at startup, so a write to a
closed pipe or past RLIMIT_FSIZE fails with EPIPE or EFBIG, and libuv's
spawns start children with every signal at its default action. The
runtime instead blocked both signals in the writing thread. On Linux the
signal a write raises goes to that thread, but on macOS it goes to the
process, so a write to a closed pipe from the fs pool's threads ended the
program with SIGPIPE.
- scr_init ignores SIGPIPE and SIGXFSZ. A process.on listener replaces the
disposition while it is registered, as before.
- scr_fs_write_once writes directly in executables, and the fs pool's
threads no longer block the two signals. Library builds keep the
per-thread blocking, since their host owns the dispositions.
- scr_spawn_command starts children with SIGPIPE and SIGXFSZ at their
default actions (POSIX_SPAWN_SETSIGDEF), for spawnSync, execFileSync
and spawn alike.
write-signals-ignored.ts and write-signals-listener.mjs check the ignored
signals, a listener, and each spawn path's child against Node.
A JavaScript object pattern over an untyped source (the result of an untyped function, a parameter, a property of an untyped object) now checks the source first when V8 can spell it, as the `any` path in TypeScript files does: a null or undefined source throws "Cannot destructure property 'size' of 'lookup(...)' as it is null." instead of "Cannot read properties of null (reading 'size')". When the first element has a default and a static key (`{ size = 0 }`), the source is not checked first and the read throws, as in V8; this also applies to `any` sources in TypeScript files and to nullable union sources.
A call through a function-valued private static field (this.#fn(...))
completed its arguments against the field's fixed parameter list, so a
rest-parameter function lost the arguments for its rest parameter, and a
function that reads arguments was refused. The call now takes the
ordinary call-through-value lanes (typed rests, runtime-arity closures,
checked-dynamic values) and binds this to the checked receiver.
Taking the head entry of a Readable's buffer moved every remaining entry
down one slot, so draining a queue of n buffered chunks cost O(n²). The
buffer now keeps the index of its first entry: a take advances it, a push
at the tail moves the entries down only when at least half the slots are
free at the front (and grows otherwise), and unshift takes a freed front
slot when there is one. The drop, trace and boxing loops walk the entries
from that index.
The stream-buffered-drain runtime workload buffers 200,000 objects and
100,000 Buffers before draining them; corpus 8533 covers reads across
entries, unshift into a freed slot and onto a part-read head, setEncoding
after takes, and object-mode takes, unshifts and pushes.
events.once(process.stdout, name) with a name typed "drain" returned the
pending promise without evaluating the name, so a call or comma
expression producing "drain" never ran. A name that is not a string
literal now runs before the promise is created.
A write completion callback that returns a value (`() => count++`, a
stored function typed `(): number`) reported SC2020. The stream ignores
the return value, so such a callback is adapted to return nothing, as
before the error-argument form was added.
The stream-shims npm case compiles and matches Node now that write
callbacks take the error argument, so its SC2020 expectation is removed.
A Response body's stream gets a web ReadableStream view when a member the
fetch stream lacks is read (for example `body.tee`). The view's source holds
the stream, and the stream released the view only when that source was its
last holder. While the transfer runs it holds the stream too, so a dropped
body with a view kept a reference count of 2: the unobserved-response
discard never ran, the paused socket was never resumed, and a compressed
response dropped before HTTP EOF kept the process alive.
The stream now releases the view when only the view's source and the live
transfer hold it, and then takes the unobserved-response discard, as a
body without a view does.
The static-abandon-view fixture reads `body.tee` and `body.locked` of a
4 MiB incompressible gzip response from the new /gzip-incompressible route
and drops it. It hung until the test's timeout before this change and now
exits as Node does.
process.memoryUsage() reported external and arrayBuffers as 0. arrayBuffers
is now the storage of the ArrayBuffers, typed arrays, Buffers, DataViews
and SharedArrayBuffers the program holds, and external the same storage
without SharedArrayBuffers, as Node splits them. A view over existing
storage adds nothing. v8.getHeapStatistics().external_memory reads the
same figure as external.
Each ScrBytes owner records the bytes it counts in the struct's padding,
so ScrBytes stays 64 bytes and the offsets compiled code reads hold; a
static assertion pins them. Owners are counted in a per-instance counter,
because their reference counts are not atomic and they are released on
their instance's thread. Shared storage moves its owner's count to a
process-wide atomic counter, since any thread may release it.
- The wrapper check before JSON, util.inspect, structuredClone and deep
equality searched the value's graph recursively with only a depth
limit, so an object with two references to itself took exponentially
many calls, and a wrapper more than 64 levels deep was not found. The
search now keeps a set of the objects it reached and an explicit stack:
each object and array is searched once, at any depth.
- String.prototype, Number.prototype and Boolean.prototype are wrappers of
"", 0 and false, as in Node, so their valueOf and toString answer the
primitive instead of throwing the brand TypeError. String.prototype has
its own `length` of 0.
- A string-named method call on a dynamic receiver in a program with
wrappers read the member twice: once to check for the builtin
String.prototype method and once for the call. The call now reads it
once, and wrapper.isStringMethod checks that value.
- createsWrappers is listed with the lowerer's first-use caches that the
unit journal leaves out.
The wrapper corpus program covers the prototypes' values, a getter read
once per call and console.log of a 40-level shared graph. The refusal
test covers a wrapper 100 levels deep, a wrapper under a shared graph and
util.inspect of a cyclic value.
A call whose name some class declares only as a getter reads the member first on instances of those classes, and keeps the runtime's method dispatch for every other receiver, so n.toFixed(2) still formats a number when a class declares get toFixed(). This replaces the list of builtin method names.
An unpinned generic declaration compared with `===`/`!==` takes its
signature from the other operand (`held === ident`), which clears the pi
`agent-session.ts:590` shape.
That pin alone would answer `false` where JavaScript answers `true` when
the program also holds the declaration at another signature, so a
whole-program check refuses the comparison instead. It covers the direct
form and values compared through `unknown` (the g5 silent divergence:
two instances of one declaration compared unequal), resolving dynamic
operands' provenance only when some declaration is held at more than one
signature.
- generic-functions: `identityComparisonPin` and
`checkGenericIdentityPins` (runs once at the end of lowering, when every
value instance is known); `GenericFnInfo.valueInstances` records the
instances value references produced.
- Corpus: `generic-identity-pin.ts`, `generic-identity-unknown.ts`.
- Diagnostics: `generic-identity-multiple-pins.ts`,
`generic-identity-unknown.ts`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A partial record assertion's view has no slot for any field the literal
omits. An omitted optional field now reads as undefined and answers false
to in, as in Node, instead of reaching a slot the view does not have. The
in test's join takes its predecessor from the block the field load ends
in. A write of an omitted field evaluates the receiver and the value
before the presence check, so the right side's side effects run before
the TypeError. The lowerer unit tests' program lists its source files,
which the partial-assertion scan reads.