A socket's write() answer and writableLength counted the bytes left
unsent. Node counts each write that is not completely sent in full, so a
large write the kernel takes in part still counts its whole length. The
socket now keeps its buffered writes in order with their lengths and
completes each one when its last byte is sent; write() answers false when
those reach the 65536-byte high-water mark.
write() now checks whether the write half is open after the send, so a
send that fails with EPIPE or ECONNRESET and closes the socket answers
false. writableNeedDrain also reads true while a custom lookup or a
deferred dial is pending, since writes then buffer until the socket
connects.
Untyped code reads writableLength and writableNeedDrain, and
writableHighWaterMark and readableHighWaterMark read 65536, Node's
default, instead of 16384.
A TCP socket's write() answered nothing in typed code and true through
untyped values, and a 'drain' listener never fired. Node 24 answers
false when the bytes the write leaves unsent reach the socket's
65536-byte high-water mark (writeOrBuffer computes it after the socket
writes), sets needDrain, and emits 'drain' when the write buffer
empties; writes to an ended or destroyed socket answer false.
The runtime's write entry points now return that answer, record
needDrain, and fire the socket's 'drain' listeners from the flush that
empties the buffer, unless the write half is ending or the socket is
gone. writableLength reads the unsent bytes and writableNeedDrain the
flag. on('drain') and once('drain') register on the typed path and
through untyped values, and socket.write() may be used as a value.
The fallback node:net declarations type write() as boolean, add the
'drain' event and both properties.
RUNTIME_ABI_VERSION goes from 43 to 44: the socket write entry points
return a boolean.
`a?.[i]` on a binding whose type admits undefined, such as a
`string[] | undefined` parameter, threw "undefined is not representable
in the target union" when the binding held undefined. Element access
replaced an identifier receiver whose storage has an undefined arm with
a checked narrow to its present arm before the optional chain ran, so
the chain saw a receiver that is never nullish and lowered `?.[` as a
plain element read.
Element access now leaves the receiver to the optional chain when the
access carries `?.`; the chain tests the undefined arm and lowers the
access again inside its guard. This applies to arrays, tuples and
record keys. Property access (`a?.length`, `a?.at(0)`) already lowered
through the chain.
Corpus 8330 matches Node's output.
readline split redirected input byte by byte, twice per chunk (once to
find whether the chunk ends a line, once to split it). Before splitting
it copied each chunk out of its Buffer and into a scratch buffer, decoded
it into a new string, and copied the unfinished line and the whole chunk
into a joined buffer. Now:
- one scan per chunk: memchr for a CR once, then LF to LF with memchr
when the chunk has none, and the LF, CR and CRLF split otherwise;
- the unfinished line joins only the first line of the chunk;
- an ASCII chunk with no partial UTF-8 sequence held from the previous
one is split where it is, and a chunk that is not shared memory is
read from its own storage. Lines are still copied into their strings
before the first is delivered.
Buffer.from(string, 'latin1' | 'ascii') encoded character by character;
an ASCII prefix, all of an ASCII string, is now copied with memcpy.
Line events, their order and the CRLF delay are unchanged. Corpus 8344
covers lines joined across chunks, a line longer than a chunk, empty
lines, mixed line endings and UTF-8 split across chunks. The new
line-reader workload reads a 67 MB log through readline over a file
stream.
- A guarded declaration of a name the CommonJS wrapper defines (`var
exports = exports || {}`, and `module`, `require`, `__filename`,
`__dirname`, `arguments`) reads the wrapper's value, not undefined, so
it keeps its text instead of becoming a fresh object.
- The write scan counts destructuring assignments, and destructuring
for-in/for-of targets, as writes, so a hoisted function that sets the
namespace before its guarded declaration keeps the declaration as it
is.
- An exported `var` that a later declaration of the same name sets
again is not settled at the export: importers would read the
redeclared value, where Node copies the first one into
module.exports. It stays refused (SC1090), as before. The npm case
var-export-redeclared pins it.
`JSON.parse(text) as T` for a discriminated union of records took the
checked-dynamic route: it parsed the text into a dynamic tree, matched
and copied that tree into records, then freed it. The one-pass schema
parser now builds these unions directly, as record members, array
elements and whole documents. For an object, a shallow scan of its
members finds the discriminant's value, which selects the one record arm
that holds that literal, and the object is parsed with that arm's
schema. An object with a missing, non-string or unknown discriminant, or
any member the arm rejects, declines, and the checked route then gives
the result or the error as before.
The compiler admits unions whose record arms each name their string
literals, every literal naming one arm, plus null and undefined arms.
Since 76735a8d9 (#1975), a `super.m()` call in a JavaScript class method
whose `this` is the call-site receiver reflected the base class's whole
prototype and read `m` from it. An instance's member lookup first tests
its property bag, which inherits from its class's prototype data, so once
the base's prototype was reflected, `child[k]` for a method the subclass
overrides found the base's method there: `value[name](arg)` on a subclass
instance called the base method.
While no code reads a prototype method by name, `super.m()` now calls the
declaring class's receiver-independent method value with the receiver, as
the method's [[HomeObject]] lookup answers, without reflecting the base
prototype. Programs that read prototype methods by name keep the
reflected lookup.
A read site's cache only remembered properties it found on the receiver
or its first prototype. Reads of properties an object lacks, which
validation libraries make constantly (zod reads ctx.skipChecks,
payload.memo, x.aborted and _zod.optin on every check), walked the whole
shape chain and then repeated the lookup on the generic path every time:
7.9 million such reads in one zod benchmark run.
- Read sites call a new probe, scr_js_get_probe, that answers undefined
for a key the receiver and its chain of plain shaped objects (at most
one prototype, ending in the default prototype) lack, and caches that
answer. The entry holds while the receiver's shape, its prototype link,
and that prototype's shape are unchanged. Its tag and placeholder
holder make the inline entry-0 test in compiled code miss, so the
answer always comes from the probe. Method reads keep
scr_js_load_probe, so a missing method still reaches the slow path and
its "is not a function" error. Names of Object.prototype members are
never cached as absent.
- Receivers of one shape with different prototypes (zod's internals
objects) each keep an entry instead of replacing one another.
- A site whose four-way record is full consults a per-thread table keyed
by shape and key before walking shapes, and fills it.
8179-tls-reject-unauthorized-values.mjs called the resolve function of an
untyped new Promise() with no argument. tsgo types that function as taking
one argument and reported SC0001 for each call, while tsc-ts accepted them,
so the checker-identity comparison failed. The calls now pass undefined.
A Map/Set lookup's integer key view was cached for the function by the key
operand's name. When the view was a conversion emitted at a lookup in a
branch, a later lookup of the same operand (a wide integer constant) reused
it from a block the conversion does not dominate, and LLVM rejected the
module. Only the value's own integer or uint32 view and integral literals
are cached now.
Two refusals kept pre-module JavaScript packages from compiling:
- `var ns = ns || {}` (and `?? {}`): the read in the variable's own
initializer made the checker type the namespace `any`, and requirers
then failed to reach the binding (SC1030). In a module, the read sees
the hoisted `undefined` whenever nothing else writes the variable, so a
new package-source normalization respells the declaration as its
default (`var ns = ({})`) when no other declaration, assignment, update,
function, class, parameter or local of that name exists in the file and
no earlier top-level statement mentions it.
- `module.exports = { ns }` with a `var` binding was refused as exporting
a mutable binding by reference. A `var` (or `let`) whose initialized
declaration precedes the export statement at top level and that nothing
reassigns exports exactly the value Node copies, so it now aliases like
a const.
The namespace benchmark (results/design-shape-inference/nsbench) now
compiles and matches Node; the new npm case legacy-namespaces pins a
namespace library with guarded declarations, constructor functions with
prototype methods, enums, a revealing module and a handler registry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A typed array such as a number[] reaches untyped code as a capsule of the
native array. A keyed read `xs[i]` there missed the inline cache, which
answers only dyn arrays, and went to the member helper, which converted
the index to a string; the capsule's key lookup then parsed the string
back into a number for every element.
Keyed sites now read an element of a native number[] capsule in place
when the index is an integer below the array's hole-free prefix, and
`length` sites read the native array's length. Other capsule elements
(holes, out-of-range and fractional indices, arrays of other element
types) are answered by the keyed probe through scr_dyn_index_get without
forming the key, or by the helper as before.
Node's afterWriteTick sets afterWriteTickInfo to null before it runs the
callbacks, whichever tick it is. The runtime cleared the queued batch
only when the running tick was that batch, so a write made from an
earlier tick's callback joined a later queued tick with the same
callback and completed ahead of nextTicks queued between them. Any
afterWrite tick now closes the batch, and such a write queues its own.
The guard proves the views disjoint for one entry of the loop. An
enclosing loop may pass other views on its next pass, such as two
buffers swapped after every pass, so one pass's write and the next
pass's read can be the same bytes under different scopes. Without a
declaration, a scope holds across the whole function, and LLVM could
apply one entry's no-alias facts to another's accesses once it unrolled
the enclosing loop.
Each fast copy now begins with llvm.experimental.noalias.scope.decl for
each of its scopes. A declared scope holds from its declaration, and
LLVM gives each copy of the declaration its own scopes when it unrolls
or duplicates the code around it.
Corpus 8182 adds ping-pong passes over swapped Float64Array and
Int32Array views; the emission test checks the declarations.
A Map or Set operation on a number key that the emitter knows is an
integer (exactInteger: an integer view, a range proof or a literal) now
looks the key up through sc_map_entry_f64_int, which tests the map's
direct index with an unsigned compare of that integer. Before, the
lookup converted the double back to an integer and compared it with the
original to prove it integral. The hashed probe still takes the double,
so hashing and SameValueZero are unchanged.
Node's onwrite queues one afterWrite nextTick for a synchronous write
completion and lets later synchronous completions with the same callback
join it (afterWriteTickInfo.count); writes without a callback share one.
The step then checks 'drain' once, calls the callback once per write and
runs finishMaybe once. The runtime queued a stream tick and a nextTick
entry per write, which ran the callbacks of later writes after nextTicks
queued between them. The queued afterWrite tick now counts the writes
that join it, as Node's does.
`decoder.decode(view!)` on a `Uint16Array | undefined` (or another typed
array that may be undefined) was refused with SC2020: the assertion keeps
the undefined arm for decode, and the decode input only read the
undefined arm of `Uint8Array | undefined`. The undefined arm of any typed
array that may be undefined now reads as an empty array of the same type,
so a defined input decodes its bytes and an undefined one decodes no
bytes, as in Node.
A loop that reads one typed array and writes another kept every element
in memory: separate views can share a buffer, so LLVM had to assume a
store to one could change the other's elements. A loop of eight lanes
held in Float64Arrays (zr[k], zi[k], tr[k], ti[k]) ran as scalar loads
and stores of each lane on every step.
When a typed-array loop version covers at least two views and writes at
least one of them, its guard now also requires every pair of the views'
byte ranges to be disjoint: one ends at or before the other starts. In
the fast copy, each view's element reads and writes carry an alias scope
of their own and no-alias with the others' scopes, so LLVM keeps the
elements in registers and vectorizes across lanes. Views that share
bytes, or one array passed twice, take the checked copy as before. The
scopes are string-named, written inline, and named after the function
and the version, so every copy of a loop has its own. Worker programs
scope only views the guard proves thread-owned.
Corpus 8182 runs such loops over disjoint, adjacent, overlapping and
identical views, views of different element types over one buffer, and
empty views.
A module binding whose initializer only allocates data (a function, an
arrow function, a literal or a registry object) is initialized only when
a reached body uses the binding. Only reads and updates counted as uses,
so a binding that reached code only assigned was never initialized, and
the assignment, which checks that the binding is initialized and
releases its value, threw "Cannot access '<name>' before
initialization" where Node runs. Assignments now count as uses.
Corpus 8320 assigns two such bindings, one through an assignment
expression, without reading them.
Every read or write of a symbol-keyed property on a checked-dynamic value
formatted the symbol's address into a new string to find its storage key,
and every conversion of a symbol to a checked value allocated a new box.
Programs that key hot properties by symbols, such as Effect's type ids
checked on each combinator, spent a large share of their time there.
The storage key is a function of the address, so a 256-entry per-thread
cache by address now returns the existing string, released at exit. A
symbol also keeps a weak pointer to its live box: conversions share it,
and the box clears the pointer when it is freed.
The sanitized nodes build compiles one 1.14 GB LLVM module, and its clang
compile peaks at about 16 GB, more than the CI runners have, so the
fixture failed in every san shard that ran it. It now builds without the
sanitizer there, as in the plain lane; every other three.js fixture keeps
it. The fixture goes back under the sanitizer once sanitized dev builds
compile large programs in shards.