1939 Commits
Author SHA1 Message Date
Chris Tate 4ee477d11e Prepare v0.4.5 release (#2226) 2026-10-25 13:33:57 -05:00
Chris Tate e24ed12933 Decode string storage within its length in Buffer encodings
String storage can hold bytes that are not well-formed UTF-8: an HTTP
header value keeps the raw bytes a client sent. Encoding such a string
read past its end.

- scr_bytes_next_cp decoded any high byte as the lead of a sequence and
  read up to three bytes after it without a bound. It now takes the
  storage length, decodes only complete, well-formed sequences, and
  returns any other byte as U+0000..U+00FF, advancing by one. That is
  each such byte's value in Node, which decodes header values as latin1.
  This covers latin1, ascii, binary and utf16le encoding, hex and base64
  of non-ASCII text (which narrow through latin1), Buffer#write and
  TextEncoder#encodeInto.
- base64 decoding read one byte past its input when the eight-character
  groups ended exactly at the end.

The runtime's bytes unit test (built with ASan) encodes ill-formed
storage at every offset within a word and random high bytes, and
compares latin1, ascii and utf16le with a scalar reference. A corpus
program sends such header bytes to a server and encodes them as Node
does.
2026-10-25 12:48:21 -05:00
Chris Tate 7a175c374f Prepare v0.4.5 release 2026-10-25 11:26:13 -05:00
Chris Tate 8a22e0f515 Answer net.Socket write() with Node's backpressure boolean, and fire 'drain' (#2198) 2026-10-25 11:04:01 -05:00
Chris Tate f0add87f10 Crypto parity: RSA key generation lengths, RSA-PSS keys and encrypted private keys (#2103) 2026-10-25 09:45:21 -05:00
Chris Tate 64e350edb6 Drop a socket's 'drain' listeners when it closes
The close step drops a socket's listener lists so that a listener which
captures its socket does not keep it alive; the 'drain' list was not
among them. A once('drain') listener still pending when the peer
disconnected, or an on('drain') listener, held its socket and the
socket's buffers after 'close'. The close step now drops the 'drain'
listeners with the others.
2026-10-25 09:25:45 -05:00
Chris Tate 0488bef1a9 Describe RSA-PSS AUTO verification against restricted keys as Node does
RSA_PSS_SALTLEN_AUTO recovers the salt length from the signature and
does not enforce the key's minimum salt length, as in Node (OpenSSL).
The runtime's exemption now says so, and corpus 8178's header, which
said the minimum was still enforced, describes what it checks.
2026-10-25 09:00:40 -05:00
Chris Tate fba4d6553e Add no empty chunk to a byte-mode Readable's buffer (#2217) 2026-10-25 08:55:59 -05:00
Chris Tate 1c73f7735f Miss non-object receivers early in the property probes and inline the entry check (#2214) 2026-10-25 08:55:01 -05:00
Chris Tate adf97e900f Merge main 2026-10-25 08:50:59 -05:00
Chris Tate 3f013df4f9 Skip the weak-key table when freeing objects that were never keys (#2215) 2026-10-25 08:50:03 -05:00
Chris Tate c6679d43fb Add no empty chunk to a byte-mode Readable's buffer
push() of an empty Buffer outside object mode put a zero-length entry
into the read buffer, and the chunks pushed after it stalled behind it:
a 'data' listener saw nothing more and a readline interface never
closed. Node's readableAddChunk adds no empty chunk outside object mode;
it clears the reading flag and may read more. The runtime now does the
same, as it already did for an encoded push that decodes to nothing.
2026-10-25 08:18:46 -05:00
Chris Tate 3e1af48ea3 Convert a loop's string receiver to UTF-16 at once only when the loop can read enough of it (#2212) 2026-10-25 08:17:39 -05:00
Chris Tate 4d27b1a000 Call replace callbacks with unannotated parameters directly (#2207) 2026-10-25 08:05:35 -05:00
Chris Tate b28af56ba8 Skip the weak-key table when freeing objects that were never keys
Once a program creates a weak collection, every object freed afterwards
called into the weak-key table to remove entries keyed by it, although
almost none are keys: zod's validation freed 16.6 million objects against
23 keys. A per-thread bit filter indexed by the object's address now marks
every address that entered the table (cleared when the table empties),
and the free paths test it inline, so an object whose bit is clear makes
no call.
2026-10-25 07:53:02 -05:00
Chris Tate 2823840134 Miss non-object receivers early in the property probes and inline the entry check
Since the probes began caching absent properties (#2186), an untyped
property read that reaches the load or get probe costs more than before:

- Receivers that are not ordinary objects, such as class instances, have
  no shape and always miss, but they set up the probe's frame first. The
  exported probes now return the miss for them before calling the probe
  body, which stays out of line.
- Each matching entry was answered through an out-of-line call
  (scr_js_entry_answer). It is inlined into the probe again.

dynamic-properties executed 3.1% more instructions than before #2186; it
now executes 0.4% fewer.
2026-10-25 07:49:06 -05:00
Chris Tate 63a99cc4d9 Convert a loop's string receiver to UTF-16 at once only when the loop can read enough of it
A versioned loop over a non-ASCII string passes the range of indexes it
reads: a counted loop's start and limit, or a while loop's counter and
bound. The runtime converts the string at once when that range has at
least one unit per 16 bytes of it, and otherwise counts the loop as one
indexed read, so a loop over a line of a long source text no longer
keeps a UTF-16 copy of all of it. The publish call is weighted as
unlikely, since loops re-entered over one string skip it.
2026-10-25 07:37:50 -05:00
Chris Tate c8b40bd73e Move small number-key maps to the direct index from the second key (#2204) 2026-10-25 06:34:30 -05:00
Chris Tate 1a0d58d1eb Merge main and move to ABI 45 2026-10-25 06:31:07 -05:00
Chris Tate 627ebd86a5 Call replace callbacks with unannotated parameters directly
TypeScript types the parameters of an inline replace callback after the
match `any`. An unannotated parameter that is never reassigned, whose
every read means the same for a string or number as for `any`, and that
receives a capture every match sets, the offset or the subject now takes
that value's type. String-search callbacks get the same treatment for the
offset and the subject.

scr_regex_replace_fn calls the closure directly in more cases:
- checked-dynamic parameters, which receive the boxed value Node passes at
  their position (a capture or undefined, the offset, the subject, the
  groups object, then undefined);
- `string | undefined` capture parameters, which receive NULL when the
  match leaves the capture unset;
- a checked-dynamic result, converted with ToString;
- up to six parameters.

Callbacks of the earlier typed form compile to the same call, with a
kinds argument of 0, and run the same per-match work.
2026-10-25 06:16:32 -05:00
Chris Tate cac5a19fc0 Fill Buffers with multi-byte values, swap byte order and decode utf16le a word at a time
- fill with a string or Buffer value longer than one byte: writes the
  value once, then doubles the filled prefix with memcpy, as Node does.
  The value is copied with memmove, so a value that is a view of the
  buffer being filled gives Node's result; the previous per-byte loop
  read such a view while overwriting it. The loop also computed a
  division per byte.
- swap16, swap32 and swap64: one load, byte swap and store per group,
  through a local data pointer.
- toString("utf16le" | "ucs2"): four units below 0x80 copy as four bytes,
  and other units outside the surrogate range are written inline (a
  three-byte sequence as one four-byte store). Surrogates keep the
  previous path. The result records its UTF-16 length (one unit per
  input unit), or is marked ASCII, so `.length` does not walk it.
2026-10-25 05:58:42 -05:00
Chris Tate b3c98139e4 Move small number-key maps to the direct index from the second key
A number-keyed Map or Set with keys the direct index allows now moves to it
at its second entry rather than its fifth, so a map over a few small
integer keys (a histogram over 0..3) reads through the inline direct index
instead of the out-of-line linear scan.
2026-10-25 05:48:40 -05:00
Chris Tate edae637526 Support named properties and Symbol.species on typed arrays held as untyped values (#2068) 2026-10-25 05:48:27 -05:00
Chris Tate 1e9afec1d4 Reclaim completed socket writes before growing the pending-write queue
The queue of writes with buffered bytes dropped completed entries only
when the buffer emptied. On a socket whose buffer stays nonempty while
old writes complete and new ones arrive, it kept every write since the
buffer last emptied. Appending to a full queue now first moves the
pending entries to the front, so its size follows the writes still
pending.
2026-10-25 05:42:02 -05:00
Chris Tate cbc062a35f Skip NULL children and key-cache walks when disposing dynamic values
Disposing a checked-dynamic node released each entry's getter and setter
and the node's prototype and symbol tables through scr_dyn_release, though
almost all of them are NULL: in a zod validation loop, over half of all
dyn releases. Dispose now tests each child first.

A pooled object also walked all its slots again on reuse, to take the key
bytes it kept back from the pool's key budget. It now records that total
when it is pooled, and reuse subtracts it.
2026-10-25 05:03:04 -05:00
Chris Tate 50c46bb9de Count socket writes as Node's state.length, and read write state from untyped code
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.
2026-10-25 04:50:20 -05:00
Chris Tate 03b754e891 Convert dynamic values into nested unions in one pass (#1914) 2026-10-25 04:18:21 -05:00
Chris Tate 45171ea02c Use a built-in typed-array constructor as its own species before the species symbol exists
A typed array whose constructor property is set to another built-in
typed-array constructor now creates that kind from map, filter, slice
and subarray when the program has not evaluated Symbol.species.
2026-10-25 03:59:10 -05:00
Chris Tate 1e799cb60a Answer net.Socket write() with Node's backpressure boolean, and fire 'drain'
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.
2026-10-25 03:40:30 -05:00
Chris Tate 9cb9750f92 Set a trusted build aside while a getter runs outside a visit session
A getter or proxy trap that runs during a trusted build outside any
visit session now runs with the build's state set aside, as inside a
session. A conversion the getter starts, such as a recursive union
that opens and ends its own session, no longer clears the mark that
keeps the rest of the outer build from trusting its earlier match.
2026-10-25 03:34:33 -05:00
Chris Tate 975270cef5 Decode base64 eight characters at a time
Buffer.from(text, "base64" | "base64url") decoded each four-character
group with four lookups and a sign test, then stored its three bytes one
at a time. The decoder now first takes complete groups two at a time:
each character's value comes from a table that holds it already shifted
into place within its group's 24 bits, a single bit test covers all
eight characters, and each group's three bytes go out in one four-byte
store whose last byte the next store overwrites. A group with any other
character (padding, whitespace, an invalid character) takes the
previous path, and the two-group step resumes after it, so line-wrapped
text keeps most of the gain. The destination already holds two spare
bytes, which cover the extra store byte.
2026-10-25 03:02:53 -05:00
Chris Tate db2cb5c99e Split readline input in one scan and copy ASCII strings into buffers whole
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.
2026-10-25 01:35:28 -05:00
Chris Tate 25d0ad9193 Parse JSON into discriminated unions in one pass (#2189) 2026-10-25 01:16:43 -05:00
Chris Tate 5d3e0c7a8b Support X509Certificate fields and methods, TLS certificates and tls.Server contexts (#2052) 2026-10-25 01:00:24 -05:00
Chris Tate 4963dcf8ec Cache absent properties in the dynamic tier's property caches (#2186) 2026-10-25 00:46:10 -05:00
Chris Tate e5ba073468 Parse JSON into discriminated unions in one pass
`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.
2026-10-25 00:44:25 -05:00
Chris Tate 713f28dcd2 Merge main 2026-10-25 00:27:44 -05:00
Chris Tate 3e54e0edb3 Read fs.promises as a module and check fs read and write arguments as Node does (#2098) 2026-10-25 00:23:19 -05:00
Chris Tate 2269fd9e1f Merge main 2026-10-24 23:48:53 -05:00
Chris Tate 3e115ce08c Cache absent properties in the dynamic tier's property caches
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.
2026-10-24 23:47:41 -05:00
Chris Tate 37031fa7c2 Convert a loop's string receiver to UTF-16 at once, and publish it only when the scan holds another string (#2180) 2026-10-24 23:34:50 -05:00
Chris Tate 6a29947998 Run the native compiler's nursery passes every 1024 candidates
The native compiler now starts with the cycle collector's nursery trigger
at 1024 new candidates instead of 256. scr_cyc_set_nursery_threshold sets
the trigger that SCR_CYCLE_THRESHOLD sets, on the pattern of
scr_cyc_set_growth_cap; the environment variable still takes precedence.

The compiler sets it from a constructor in its host object, before its
modules initialize. Set from main, the trigger leaves the build's
collection work where it was: the cost depends on the collector's first
passes. Programs keep the default trigger.

A cold tsc-ts build takes 4% less wall time (12 paired runs) and its peak
RSS goes from 1.41 GB to 1.31 GB. The program's IR is unchanged.
2026-10-24 22:59:02 -05:00
Chris Tate add5f4869b Convert a loop's string receiver to UTF-16 at once, and publish it only when the scan holds another string 2026-10-24 22:53:15 -05:00
Chris Tate be878b4eb9 Freeze class instances held in untyped values (#2061) 2026-10-24 22:43:12 -05:00
Chris Tate 0fbb47382d Read elements of native arrays passed to untyped code by index (#2171) 2026-10-24 22:32:09 -05:00
Chris Tate b08efe27e5 Complete consecutive synchronous writes in one afterWrite step (#2169) 2026-10-24 22:24:33 -05:00
Chris Tate 4241ca533f Read elements of native arrays passed to untyped code by index
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.
2026-10-24 21:59:28 -05:00
Chris Tate d6c8b0f260 Close the afterWrite batch whenever an afterWrite tick runs
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.
2026-10-24 21:52:18 -05:00
Chris Tate 7b946ff610 Read Math, JSON and Reflect as real objects in JavaScript (#2006) 2026-10-24 21:50:17 -05:00
Chris Tate 492d1558be Complete consecutive synchronous writes in one afterWrite step
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.
2026-10-24 21:22:54 -05:00