844 Commits
Author SHA1 Message Date
Chris Tate bf050d1077 v0.3.4 2026-10-16 01:29:54 -05:00
Chris Tate 97afb3f281 Convert cyclic dyn values to recursive shapes (#1098) 2026-10-16 01:19:00 -05:00
Chris Tate a09af4f721 Report what keeps a region with SCRIPTC_REGION_DEBUG (#1095) 2026-10-16 01:12:28 -05:00
Chris Tate c7c21fe4a5 Convert cyclic dyn values to recursive shapes
Checking or converting a dyn value whose objects or arrays refer back to
each other against a recursive interface (`raw.next = raw; raw as Node`)
recursed without end and overflowed the stack. Matchers and builders of
record and array types that reach themselves now track the dyn values
they visit:

- a matcher answers true for a (value, type) pair it is already checking
  further up the stack, so the outer check decides;
- a builder records the record or array it allocated for a dyn value
  before converting its members, and a later visit of the same value in
  the conversion answers it, so `n.next === n` holds after the cast and a
  value reached twice converts once.

Only values with more than one reference are tracked, since a value
reached twice has at least two; the reference a member read holds is not
counted. Capsules materialized during a conversion keep their first view,
so a nested visit cannot refresh a view an outer visit is reading, and
getters and proxy traps that a member read calls run outside the visit. The
cycle analysis treats record shapes that reach themselves and are targets
of a dynCheck as mutable, so the cyclic records a conversion builds are
collected.
2026-10-16 00:52:19 -05:00
Chris Tate 2e3ae368d1 Deliver fs callbacks without a timer turn (#1093) 2026-10-16 00:49:02 -05:00
Chris Tate a8d154f370 Report what keeps a region with SCRIPTC_REGION_DEBUG
A released region that something outside still refers to is kept, and
SCRIPTC_REGION_STATS only said how many references the walk could not
explain. In a build with --optimization=dev (or --sanitize), setting
SCRIPTC_REGION_DEBUG=1 (or a larger number, the line limit) makes each
attempt that keeps a region also print what keeps it:

- per chunk, the references to its objects that the walk did not find
  (from variables, other regions or objects it does not walk), with the
  classes the chunk holds, since counts are kept per chunk;
- each ordinary object the region owns that is referenced from outside,
  named by the region class and field it was reached through, with its
  reference count and the references the walk found.

The emitted region class descriptors gain the names of the fields their
visits report, in visit order. Debug walks give every owned object an
entry, so that each one records where the walk first reached it. The
bookkeeping is compiled only into unoptimized and RC-audit runtimes:
compiled in, it made the release walk 6.5% slower even with the variable
unset. Optimized runtimes walk as before and, when the variable is set,
say once that the reports need a dev build. Retains, releases and
allocation are unchanged.

Adds corpus program 5703.
2026-10-16 00:35:06 -05:00
Chris Tate 8d8e7a7737 Run Atomics on shared storage without a lock (#1090) 2026-10-16 00:27:39 -05:00
Chris Tate 3005acc14f Deliver fs callbacks without a timer turn
The fs error-first callbacks (readFile, writeFile, stat, access, mkdir and
the other callback forms that complete synchronously) and fs.exists
delivered their callback through a zero-delay timer. Timers have a 1 ms
floor, so every callback waited about 1 ms: 5000 sequential readFile
callbacks took 5.5 s, where Node takes about 0.35 s.

The callbacks now go on a FIFO queue that the event loop drains like the
worker-pool completions: one callback per checkpoint, with nextTicks and
microtasks drained between callbacks. A turn delivers the callbacks that
were queued when its timers ran, as libuv's poll phase does, so a
callback that queues another leaves room for timers and immediates.

Corpus 5555 covers the order against the synchronous code and its
microtasks, and timers and immediates during a chain of 2000 callbacks.
2026-10-16 00:26:28 -05:00
Chris Tate 7b83558f52 Run Atomics on shared storage without a lock
Atomics operations on 8-, 16- and 32-bit integer arrays are sequentially
consistent atomic instructions on the element, and no longer take the
process-wide shared-memory lock. Other reads and writes of shared storage
are relaxed atomic accesses: element reads and writes access the element,
and copies, fills, conversions, searches and sorts read shared bytes into
a private copy and write back only the range they change, so concurrent
accesses to the same bytes are free of data races. Non-shared storage
keeps its plain memory accesses. Atomics.wait and Atomics.notify queue
waiters in buckets chosen by location, each with its own lock and
condition variable, so waiting threads no longer serialize other Atomics.

Atomics.store answers +0 for a value that truncates to -0, as Node does.
The shared storage runtime test also runs under ThreadSanitizer.
2026-10-16 00:06:31 -05:00
Chris Tate de0917284d Merge main 2026-10-16 00:05:07 -05:00
Chris Tate 93dcd9865e Merge main 2026-10-16 00:01:13 -05:00
Chris Tate 681f5ceee7 Add TestContext hooks, plan and fullName, and Node's todo and only rules
node:test changes, matching Node 24.22:

- t.before, t.after, t.beforeEach and t.afterEach lower. t.before runs
  when it is called; a throw fails the test and the subtests that follow,
  which skip their body and beforeEach hooks and run their afterEach hooks.
  t.beforeEach and t.afterEach wrap every descendant subtest. t.after runs
  after the test's afterEach chain, also when the body throws, and a throw
  fails the test.
- t.plan(count) counts the test's t.assert calls and subtests. A mismatch
  fails the test with Node's "plan expected N assertions but received M"
  string, unless an assertion failed first.
- t.fullName joins the names from the outermost suite or test with " > ".
- Tests and suites under a todo suite or test are todo, with a bare TODO
  directive, unless they are skipped or carry their own todo message. A
  todo suite reports its directive.
- describe.only, suite.only and { only: true } on a suite mark it. At each
  level, when a child is marked or contains a marked test or suite, only
  those children run.
- The failing-tests section lists failures in completion order, so
  subtests come before the test that ran them, and prints thrown strings
  quoted, as Node inspects them.

The fixtures ctx-hooks, ctx-plan, todo-suites, suite-variants,
only-nested and thrown-values compare the reporter output and exit code
with Node.
2026-10-15 23:16:04 -05:00
Chris Tate dcd98ae5e5 Walk owned objects with a single reference without an entry (#1082) 2026-10-15 23:01:00 -05:00
Chris Tate 63b4dddeca Support array keys in InternTable and free replaced indexes (#1072) 2026-10-15 22:50:08 -05:00
Chris Tate 893de9f4f8 Retry a kept region only when its counted references change (#1081) 2026-10-15 22:44:02 -05:00
Chris Tate 3caf73f814 Take published objects out of the candidate buffers during the walk (#1080) 2026-10-15 22:41:30 -05:00
Chris Tate 26f965c198 Walk owned objects with a single reference without an entry
The release walk gave every ordinary object it reached an entry in an
open-addressing table, to count the references it found to it and the
edges between entries. A region of 1.2M objects that each own an array
spent most of its 200 ms walk inserting into that table.

An object with a single reference, found from one of the region's own
objects or from an object walked like them, has no other referrer, so it
can never be referenced from outside the region. It now gets no entry and
no edges and is walked like the region's own objects, its references into
the region counted as explained. RC-audit builds still enter these objects
and stop when the walk reaches one twice.

Arrays and maps also resolve their elements' release entry point once per
container instead of once per element.
2026-10-15 22:35:54 -05:00
Chris Tate 1b4103889b Add assert.deepEqual, partialDeepStrictEqual and the Assert class (#1079) 2026-10-15 22:22:57 -05:00
Chris Tate 2f510beccf Retry a kept region only when its counted references change
Every region creation, release and pressure retry walked every condemned
region again. A released region that something still refers to was
re-walked on each of them, two or three times per build in a watch loop,
and each walk visits every object of the region.

An attempt that keeps a region now records the references counted to it.
Retries at creation, release and region entry attempt a kept region again
only when that count has changed, or once the thread has allocated 8 MiB
(or the region's size, if larger) in regions since the attempt. That
volume doubles after each attempt that finds the count unchanged, up to
64 times. The volume covers references from ordinary objects the walk
reaches, which can go away without changing the count. The schedule is
per region, so a region that another region's release unblocks is
attempted at once, and the full cycle collection before an attempt runs
only when some region is due.

Adds the region-builds runtime benchmark and a region test for the
schedule.
2026-10-15 22:22:32 -05:00
Chris Tate a87bf8b4b0 Take published objects out of the candidate buffers during the walk
publish() used to remove each published object from this thread's
cycle-collector candidate buffers after the walk, which loaded every
object's collector header a second time. On graphs larger than the
cache that second pass took about a third of the publish.

scr_pub_mark now takes a buffered object out of its buffer when it marks
it, while the header is in cache: the slot is cleared and its index
recorded. When the publish ends, scr_cyc_pub_fill closes the empty slots
with entries from the ends of the buffers. A refused publish puts each
taken object back in its slot first. After the walk, the log is read only
for dyn containers, the RC audit and LeakSanitizer.

The emitted walkers test null and immortal references (parent links,
values published before) before calling scr_pub_push, and the string
walker checks for ASCII 32 bytes at a time.

Adds the published-graphs runtime benchmark, a cycle collector test for
the take, restore and fill hooks, and corpus program 5702.
2026-10-15 22:19:05 -05:00
Chris Tate cfc6ebb340 Add source functions, promise destinations and { signal } to pipeline stages (#1077) 2026-10-15 22:14:49 -05:00
Chris Tate 7c09e1c020 Add assert.deepEqual, partialDeepStrictEqual and the Assert class 2026-10-15 22:01:38 -05:00
Chris Tate 087af09210 Wake the event loop when worker-pool jobs complete (#1073) 2026-10-15 21:51:45 -05:00
Chris Tate 647bc2a492 Add source functions, promise destinations and { signal } to pipeline stages
pipeline() accepts a source function (an async generator function called
with { signal }) and a destination async function, following
pipeline.js: the destination's promise value resolves the
stream/promises form and reaches the callback's second argument, and
pipeline() answers a PassThrough for it. A rejected destination destroys
that PassThrough with the reason and fails the pipeline.

Stages may take the { signal } argument: the pipeline creates its own
AbortController when one does, and aborts it when the pipeline finishes
or fails, as finishImpl does. Stage functions take the source and
{ signal } through the closure ABI, and the PassThrough and the value
are released with the pipeline.

The stream unit now links the emitter unit whenever it links, so a
pipeline of functions builds without constructing a stream. An async
generator destination and reading the PassThrough a function destination
returns are refused at compile time.
2026-10-15 21:50:01 -05:00
Chris Tate 77c9a2ff23 Merge main 2026-10-15 21:48:48 -05:00
Chris Tate 8dc5dc1249 Refuse InternTable array keys that are too long to encode
Encoding an array key added each element's size to a running length
with unchecked arithmetic, and wrote into a fixed stack buffer whenever
the sum fit. A sparse array with a large length (2^29 elements or more)
wrapped the sum on 32-bit targets, so the write landed before the buffer;
on every target the encoder also visited every index of such an array.

Array keys now have at most 65536 elements, and their encoding at most
2^31 - 1 bytes. The size is computed first with sums that cannot wrap,
the buffer is sized to it, and keys over either limit make the lookup or
insert throw a TypeError before anything is read or written. The Node.js
implementation throws the same TypeError for keys of more than 65536
elements. Sparse elements are walked in one pass in index order, and the
index growth computation checks its size arithmetic too.
2026-10-15 21:44:38 -05:00
Chris Tate 81ed97c5a7 Lower timers/promises resolve values and setInterval under @types/node (#1058) 2026-10-15 21:33:25 -05:00
Chris Tate d30d2619d7 Support crypto.createCipheriv and createDecipheriv (#986) 2026-10-15 21:30:30 -05:00
Chris Tate 0ebc146405 Merge main 2026-10-15 21:07:04 -05:00
Chris Tate 1c3a0202a5 Give small exact arrays room for one append (#1070) 2026-10-15 21:02:46 -05:00
Chris Tate 4035c7019c Wake the event loop when worker-pool jobs complete
The shared worker pool (fs.rename callbacks, zlib callback codecs) had no
handle the event loop could wait on, so the loop noticed a completed job
on a sleep capped at about 1 ms. Every round trip through the pool could
wait up to that long.

A worker now signals a completion wake when it publishes a job: one byte
on a pipe on Linux and macOS, which the loop's poll(2) sleep watches, and
an auto-reset event on Windows, which the loop's sleeps wait on. The
capped sleep remains for a failed pipe or event and for Windows sleeps
that also wait for child processes. WASI runs pool work on the next loop
turn, as before.
2026-10-15 20:57:48 -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 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 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 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 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