Commit Graph
2626 Commits
Author SHA1 Message Date
Chris Tate 3c7db70375 Return unions with reference arms in registers (#1065) 2026-10-15 21:15:36 -05:00
Chris Tate 9e81333bbc Add Readable.from over Sets, iterators and class iterables
Readable.from accepts any iterable or async iterable of strings or
Buffers, following from.js: the iterator is taken when the stream is
built, before the options are read, next() is looked up and called on
each pull, a sync iterator's promise values are awaited, and destroy
calls the iterator's throw() and return() only when it has them, reading
each method when close() runs. A close() that fails with a second error
passes an AggregateError of both to the destroy callback, as
aggregateTwoErrors does.

In typed code this covers Sets of strings, Map and Set iterators,
classes whose [Symbol.iterator]() or [Symbol.asyncIterator]() is a
generator method, iterator classes, and async iterator classes, whose
return() runs at the end of the stream and on destroy. In JavaScript it
covers every iterable; an object whose iterator method is a generator
method throws a not-supported error when the stream is built.
2026-10-15 21:10:22 -05:00
Chris Tate 0ebc146405 Merge main 2026-10-15 21:07:04 -05:00
Chris Tate 293370e2e7 Merge main 2026-10-15 21:06:45 -05:00
Chris Tate 1c3a0202a5 Give small exact arrays room for one append (#1070) 2026-10-15 21:02:46 -05:00
Chris Tate 6b53ab8570 Reject events.once when a newListener listener throws
When a 'newListener' listener threw while events.once registered its
listeners, the exception escaped the call, the 'error' listener's closure
was never registered or released, and the shared state and its promise
leaked with it. Node registers both listeners inside the promise's
executor: the throw rejects the returned promise, and the listener whose
announcement threw is not added (the event's listener stays when only the
'error' announcement throws).

scr_events_once now takes the pending exception, takes back the listener
whose announcement threw without a 'removeListener' event, releases the
'error' closure when it was never registered, and rejects the promise
with the exception.
2026-10-15 20:54:30 -05:00
Chris Tate ee49946d32 Support array keys in InternTable and free replaced table indexes
InternTable keys can be arrays of numbers or of strings, compared by their
elements: numbers by SameValueZero, strings by content, and holes equal to
each other only. The runtime stores an array key as one byte string that
encodes its elements; the Node.js implementation encodes it into a Map key
the same way.

Replaced table indexes are now freed. Every thread that uses a table
records the epoch it read at the start of each lookup and insert, and an
index retired in an earlier epoch than every recorded one is freed. Worker
threads stop holding retirements back when they exit.

Number indexes keep a tag byte per slot ahead of the key and value pairs,
so a lookup of an absent key reads the dense tag bytes instead of pairs.

getOrInsertComputed lowers an arrow callback with an expression body in
place, with its parameter bound to the key, so interning through
`(key) => new T(...)` creates no closure.
2026-10-15 20:53:19 -05:00
Chris Tate 6bb9a3e93d Add InternTable, a shared append-only table of published values (#1069) 2026-10-15 20:50:14 -05:00
Chris Tate 439a4fee93 Run the event loop for timers/promises calls
A timers/promises timer that nothing awaited did not keep the process
alive: the loop only ran when the program used the global timers, async
functions or generators. Node keeps a pending ref'd timer's process open,
and the timer heap's exit teardown releases an unref'd one only when the
loop runs, so a pending { ref: false } timer stayed live at exit. Any
timers/promises call now runs the loop.

The node-types fixture no longer attaches .then to the never-firing
unref'd timer, whose suspended continuation the sanitizer's RC audit
reports at exit. Corpus 5620 no longer orders a 1ms timeout against an
immediate, which Node itself does not order from the main module.
2026-10-15 20:42:37 -05:00
Chris Tate a3b6b05a4c Merge main 2026-10-15 20:42:11 -05:00
Chris Tate 60aa76ee47 Drain process.nextTick before promise jobs after every callback (#1051) 2026-10-15 20:41:24 -05:00
Chris Tate e1e3a7354d Balance the corpus shards by each program's measured cost (#1068) 2026-10-15 20:33:02 -05:00
Chris Tate 46f6d7372e Emit no union result pair after a return value that always throws
A return whose value always throws, such as a call of a function
returning never, ends its block before the union result pair is built.
The payload retain then opened new blocks that referred to values the
ended block never defined, and a conditional's join took an edge from
an arm that never reaches it. The retain now emits nothing in an ended
block, and the join takes edges only from arms that reach it.
2026-10-15 20:27:35 -05:00
Chris Tate 5f954cfbb0 CI: run the macOS, Effect and bootstrap lanes on pull requests only when their areas change 2026-10-15 20:27:04 -05:00
Chris Tate feeba0a166 Add assert.AssertionError with Node's properties and constructor (#1022) 2026-10-15 20:18:29 -05:00
Chris Tate ab0d4f0cd3 Give small exact arrays room for one append and keep growth on constant slot sizes
Exact constructions (literals, slice, concat results) round their request
to 16 bytes before the size class, so arrays of 2, 4 and 6 slots get one
spare slot and an append right after a copy no longer reallocates. Arrays
of 8-byte slots with their own storage and no sparse entries grow below
the dense cutoff on a path with a constant slot size, and dense copies of
8-byte slots use a constant size.
2026-10-15 20:14:59 -05:00
Chris Tate 94b38475b1 Add InternTable, a shared append-only table of published values
@scriptc/experimental exports InternTable<K, V>: a named table that every
thread of a scriptc program shares and that each thread has its own copy
of under Node.js. Keys are strings or numbers with Map equality; values are
published (@scriptc/threads) when stored, and the first stored value of a
key is kept. Members are get, has, size, getOrInsert and
getOrInsertComputed.

The runtime table (scr_itable.c) splits each table into 64 segments.
Lookups take no lock and do not write; inserts take a segment's spin lock
for a probe and a few stores, and callbacks run without a lock. Number
tables keep keys and values in the index; string tables keep entries in
segment-owned chunks behind tagged slots. Replaced indexes are kept.

The compiler lowers the class to a synthetic class with one f64 handle
field and five intrinsics, with interned helpers per member and type and
an in-place lowering of getOrInsertComputed that creates a function
expression callback only on a miss. Stored values go through the publish
intrinsic, so their types get the write guard. scriptc refuses names that
are not string literals, one name opened with different type arguments,
and key or value types a table cannot store.

scr_publish walks small graphs in inline buffers and reads
SCRIPTC_PUBLISH_TRACE once, instead of allocating and calling getenv on
every call.
2026-10-15 19:59:56 -05:00
Chris Tate b00d65b8be Merge main 2026-10-15 19:57:06 -05:00
Chris Tate eb90bf7eee Merge main 2026-10-15 19:50:35 -05:00
Chris Tate a08abf4abc Balance the corpus shards by each program's measured cost
The differential and llvm-differential corpus split their programs across the ten Linux test shards by a hash of
the program's name, so a shard's share of the measured work varied from 565 to 913 s (plain) and 868 to 1358 s
(sanitized). corpusSelect assigns programs longest first to the lightest shard by their measured cost per flavor
(tests/harness/corpus-costs.json: the differential plus llvm-differential milliseconds on a CI runner, medians of
four green main runs), giving 740-741 s and 1098-1099 s. Both harnesses still select identically, so a shard's
compile cache serves both. Programs without a measurement weigh as much as the median one.
scripts/ci-corpus-costs.mjs refreshes the table from downloaded CI shard logs.
2026-10-15 19:49:01 -05:00
Chris Tate a8cdf18090 Chain corpus 5615's sections instead of timing them
Each section of 5615 now starts from the previous section's last
callback. Before, the sections started at fixed delays (20, 100 and 200
ms), so a slow first callback could delay the timers section's report
past the start of the I/O section and print its readFile callbacks on
the timers line. The program's output is unchanged.
2026-10-15 19:48:48 -05:00
Chris Tate 6d7ed1bfbf Add async generator stages to pipeline (#1064) 2026-10-15 19:34:35 -05:00
Chris Tate 17c9148e47 Read named properties of unknown values with one receiver retain (#1063) 2026-10-15 19:27:06 -05:00
Chris Tate 346a7c0a70 Match Node's http keep-alive, header and framing behavior (#1046) 2026-10-15 19:21:26 -05:00
Chris Tate f6dc19ad98 Merge main 2026-10-15 19:13:52 -05:00
Chris Tate f8d6b64a68 Collect dropped cyclic graphs while they are in cache (#1062) 2026-10-15 19:13:19 -05:00
Chris Tate f0151d8c17 Merge main 2026-10-15 19:06:52 -05:00
Chris Tate 5af3287c62 Report generatedMessage for empty custom messages as Node does
An empty string message follows Node 24.22's rules for each assertion:

- equal, notEqual, strictEqual, notStrictEqual, deepStrictEqual,
  notDeepStrictEqual and fail pass it to the AssertionError constructor,
  so the message stays empty, or the diff header is the default one, and
  generatedMessage is true (`!message`).
- match, doesNotMatch, and throws with a regex, a class or an object shape
  treat it as no message: the generated message is built and
  generatedMessage is true.
- throws with nothing thrown and doesNotReject end their message with "."
  instead of ": ", and keep generatedMessage false.
- ok and assert() keep the empty message with generatedMessage false.

assert.fail computes generatedMessage from the message value at run time.
Corpus 5521 covers each method with a literal and a computed empty
message.
2026-10-15 19:04:23 -05:00
Chris Tate 9bff898603 Store number-key Map.set and Set.add after a read into the found entry (#1059) 2026-10-15 19:01:03 -05:00
Chris Tate f9cd11e124 Return unions with reference arms in registers
Directly called functions returning a union of numbers, booleans, unit
values and class instances, strings, records or arrays now return its tag
and payload in registers, as scalar unions already did. A reference
payload carries the reference the heap box held. A wrapped return value
moves its reference into the pair, and a returned call with the same
convention passes its pair through.

Callers that only project the result keep it in a private stack box whose
payload the statement frame releases by tag. A discarded result releases
its payload from the registers. Locals bound to such results keep the
tag and payload: with one reference arm through the existing owner slot,
and with several through the box itself, released by tag on rebinding and
scope exit. Other callers, and the boxed entry used by closures, dispatch
and the runtime, box the pair. Functions with a finally block keep the
boxed result.
2026-10-15 18:52:54 -05:00
Chris Tate 982ebac139 Accept typed-ref arguments in createCipheriv and the cipher methods
createCipheriv, createDecipheriv, update(), setAAD() and setAuthTag()
now read the value a typed-ref box holds when it is a string, view,
ArrayBuffer or KeyObject. ERR_INVALID_ARG_TYPE names the class of a
class instance that arrives through an untyped value, as Node does,
instead of Object. An unknown output encoding that is not a string is
rendered as Node's %s formats it (util.inspect at depth 0, or the
object's own toString); a function, or an instance of a class that
defines toString, throws a coded SC2020 error. Cipher programs link the
inspect unit for that rendering.

5250 passes each argument through `any`, including class instances,
records and arrays that Node rejects, and 5254 covers output encodings
that are not strings from JavaScript.
2026-10-15 18:47:02 -05:00
Chris Tate ba3fcfb7f9 Add async generator stages to pipeline
pipeline() accepts a run of async generator function stages between
streams, following pipeline.js: each stage is called during setup with the
previous stream (a Readable is async iterable) or the previous stage's
generator, and the last generator is pumped into the next stream with
pumpToNode's write, drain and end waits. An error in the pump loop returns
the generator before the pipeline finishes, so the stage's finally runs.

A stage's source parameter is typed as what the pipeline passes it and may
only head for await loops. Function sources and destinations, stages that
take the signal argument, and stages separated by streams are refused.
2026-10-15 18:46:41 -05:00
Chris Tate 16d687a42d Read dyn array elements and lengths by number without a key string (#1015) 2026-10-15 18:41:08 -05:00
Chris Tate 0c353a4ae2 Read named properties of unknown values with one receiver retain
A named property read on a value typed unknown goes through the property
helper the class dispatch generates. Its read compared the property bag
with the receiver and then read from it, and the bag expression was
evaluated at each use: three calls that each retained and released the
receiver. Releasing a record capsule whose fields can form cycles also
buffers it as a cycle candidate, so a single `u.id` paid that three times.
The helper now evaluates the bag once into a local.

Typed records seen as unknown also find a field by comparing the key
pointer with the interned field names first, since compiled reads pass
the same literal, before comparing bytes, and records whose fields are all
required skip the presence check.
2026-10-15 18:40:22 -05:00
Chris Tate 5968aec818 Add Readable.from over generators and match Node 24.22 stream timing (#1057) 2026-10-15 18:39:48 -05:00
Chris Tate a8ae330957 Release the in-progress client response when its trailers overflow
A chunked response whose trailers ran past the header limit failed the
request with "Parse Error: Header overflow" and marked the exchange done
while the parser still held the response. The socket-close cleanup then
skipped it, so the response, its socket and the parser kept one another
alive, and every such response leaked. The overflow path now settles the
response ('aborted' and 'close', as a premature close does) and drops the
parser's reference before the exchange completes.

Corpus 5652 repeats response trailer and head overflows and request
trailer overflows; under SCRIPTC_SAN=1 the RC audit fails it without this
change.
2026-10-15 18:35:02 -05:00
Chris Tate 05dabea9a6 Merge main 2026-10-15 18:31:17 -05:00
Chris Tate ce2e567922 Collect dropped cyclic graphs while they are in cache and skip redundant walks
A candidate release now also runs a pass once the live cycle-headered
count has grown by 4096 objects since the last pass. A dead graph is
usually reachable from one candidate (its root), so the candidate count
alone let a few hundred dead graphs pile up before a pass, which then
walked them after they had left the cache.

markGray records the objects it grays and counts the ones still
externally referenced after the trial decrements. When none is, every
gray object is garbage, so the pass whitens the recorded set and skips
scan. markGray also records each edge it skips into an older generation,
so a restricted pass pins those targets without tracing the white set a
second time.

Adds the cyclic-graphs runtime benchmark and corpus programs 5700 and
5701.
2026-10-15 18:26:48 -05:00
Chris Tate 816c664058 Lower events.once over EventEmitter instances
events.once(emitter, name) was refused. The emitter dispatches statically
emitted events to typed listeners through invokers built per argument
tuple, so the lowering takes the event's tuple from the program-wide event
table and builds a capture-free function that boxes that tuple into the
any[] Node fulfills with, and one for the 'error' tuple.
scr_events_once registers both as one-shot typed listeners behind emitted
adapters (emitterOnceAdapter). The first to fire settles the promise
(fulfilled with the arguments, or rejected with the error) and removes
the other, as in Node; awaiting 'error' itself registers only the one
listener. The shared state holds the emitter and the listener closures
weakly, so a never-fired once creates no cycle.

Options, computed or meta event names, EventTarget and stream receivers,
and events emitted with computed names or checked-dynamic listeners stay
refused. The fallback declarations type the result Promise<unknown[]>;
under @types/node (Promise<any[]>) TypeScript code narrows it to
unknown[].
2026-10-15 18:26:31 -05:00
Chris Tate 6103cf5667 Overwrite present number keys in the lookup that checks for growth (#1056) 2026-10-15 18:24:49 -05:00
Chris Tate fb161771f4 Lower util.format %f
%f was refused because parseFloat had no static lowering; the global
parseFloat lowers now. Node formats parseFloat(String(arg)): a number
keeps its value except -0 (String prints "0"), a string parses with the
global parseFloat, a checked-dynamic value converts with String first,
and booleans, undefined and null format as NaN. Other argument types are
refused with their type named.
2026-10-15 18:22:52 -05:00
Chris Tate f8697111b5 Store number-key Map.set and Set.add after a read into the found entry
A number-key write that follows a get or has of the same collection and
key, inside its own arguments (m.set(k, (m.get(k) ?? 0) + 1)) or in the
statement just before it, now looks the key up through the read-only
sc_map_entry_f64 helper. A found entry takes the new scalar value in its
slot (a Set entry already holds its unit value); a new key appends
through the runtime set. LLVM merges that lookup with the read's when no
store intervenes, so the write does not hash and probe the key again.

Other writes, including those guarded by an absent key, keep the single
runtime call, which finds the insertion bucket in the same probe.
Reference values keep the runtime call, which releases the old value.
2026-10-15 18:21:06 -05:00
Chris Tate 54dc43ed14 Lower timers/promises resolve values, the ref option and setInterval under @types/node
timers/promises setTimeout(delay, value) and setImmediate(value) were
refused: only the void forms lowered. The value now travels as an
already-resolved promise of its own type (the Promise.resolve lowering),
and scr_tp_set_timeout_value / scr_tp_set_immediate_value copy its
settlement into the result when the timer or immediate fires, so the
result keeps the value's type. An options object literal takes `ref`;
`ref: false` unrefs the timer, so it no longer keeps the process alive.
AbortSignal options and other option forms stay refused, with their own
messages.

@types/node types setInterval as NodeJS.AsyncIterator<T>, which maps to
no IR type, so a for await loop over setInterval(delay, value) was refused
in projects with @types/node. The interval's async generator type now
comes from the iterator's type argument, and a for await loop whose head
is that call consumes it directly.

The fallback declarations type the new forms.
2026-10-15 18:18:45 -05:00
Chris Tate 4743578e20 Probe hashed number-key Map/Set lookups inline (#1055) 2026-10-15 18:14:17 -05:00
Chris Tate 8e5a0d1cfd Merge main 2026-10-15 18:07:59 -05:00
Chris Tate ae5d9bf81f Add Readable.from over generators and match Node 24.22 stream timing
Readable.from accepts sync and async generators of strings and Buffers,
following from.js: values are pushed while push() accepts, an async
generator's next() promise is awaited once per value, and destroy runs
the generator's throw() and return() before the destroy callback.

for await over a stream follows createAsyncIterator: the loop registers
its 'readable' listener, reads through read(), and a parked loop wakes
through the 'readable' tick. push(null) before the first read ends the
stream after the next read, finished() answers an already settled stream
with its result on a nextTick, read(0) on an object-mode stream reads
nothing, and write(string, 'buffer') throws ERR_UNKNOWN_ENCODING.
2026-10-15 18:07:26 -05:00
Chris Tate 38974c01eb Merge main 2026-10-15 18:03:29 -05:00
Chris Tate cdf0598e6e Overwrite present number keys in the lookup that checks for growth
When appending one more entry would grow a hashed number-key map's
index, Map.set and Set.add first look the key up to decide whether the
map should move to the direct index. A key that lookup finds was then
looked up again by the generic set. The found entry now takes the new
value directly, so overwriting a present key in a map at its growth
boundary probes the table once.
2026-10-15 17:51:48 -05:00
Chris Tate da27496ea9 Probe hashed number-key Map/Set lookups inline
Number-key reads of a map without the direct index called
scr_map_entry_f64 for every lookup. The internal sc_map_entry_f64 helper
now calls sc_map_entry_f64_hashed for such maps instead: it hashes the
key as scr_map_find_f64 does (SameValueZero key word, scr_key_mix, the
absent-key filter), scans the entries of a small map or probes a compact
bucket table, and calls the runtime lookup only for a table wider than
the compact limit. It stays out of line so the direct-index path stays
small at every call site.

The runtime fallback is called as a read-only call, so both helpers only
read memory and LLVM merges lookups of one key when nothing is stored in
between, such as a has() followed by get().

The runtime benchmark suite gains number-maps: a node table indexed by
dense ids, scope tables keyed by sparse ids, and get/has/set mixes.
2026-10-15 17:47:34 -05:00
Chris Tate 079a9d1eb5 Add tlsSocket.getProtocol, getCipher, alpnProtocol and tls.DEFAULT_MIN_VERSION/MAX_VERSION
- getProtocol() answers the negotiated version ("TLSv1.2" or
  "TLSv1.3"), the highest version on offer before the handshake as
  OpenSSL does, and null after close.
- getCipher() answers { name, standardName, version }: OpenSSL's name
  for the negotiated suite, its IANA name, and the suite's minimum
  protocol version. A suite without a known OpenSSL name throws the
  "not supported yet" error.
- alpnProtocol is null before the handshake, then the negotiated
  protocol or false.
- tls.DEFAULT_MIN_VERSION and DEFAULT_MAX_VERSION are 'TLSv1.2' and
  'TLSv1.3', and the bundled declarations type the server's
  secureConnection socket as a TLSSocket.
- The limitations page notes that a typed getCipher() read before the
  handshake or after close throws where Node answers undefined or null.
Corpus 5651 covers each case.
2026-10-15 17:46:09 -05:00