Commit Graph
1069 Commits
Author SHA1 Message Date
Chris Tate 1663d6d35e Merge main 2026-10-18 08:45:06 -05:00
Chris Tate 1cd06fda4c Read events.defaultMaxListeners from the ES module namespace copy
Reads of defaultMaxListeners through an ES module namespace import or a
named import answer the namespace's copy of the export, which keeps its
value from program start until module.syncBuiltinESMExports() refreshes it,
as in Node. The default import, the EventEmitter class and CommonJS module
bindings keep reading the current default. A destructured CommonJS binding
of defaultMaxListeners, which held a placeholder string in JavaScript
files, is refused.
2026-10-18 07:44:23 -05:00
Chris Tate 4d95b28dde Add t.assert.snapshot and t.assert.fileSnapshot (#1274) 2026-10-18 06:55:39 -05:00
Chris Tate 14c1cddffe Merge main 2026-10-18 06:17:56 -05:00
Chris Tate 04da3a6177 Run a full cycle pass when a structure dies outside passes (#1277) 2026-10-18 06:08:04 -05:00
Chris Tate 0dac6c5094 Serialize checked-dynamic members of typed records like JSON.stringify (#1275) 2026-10-18 05:59:12 -05:00
Chris Tate 74702ac4be Run a full cycle pass when a structure dies outside passes
Full passes are scheduled by heap growth, and the fraction they wait for
widens while growth-triggered passes find nothing, up to 16 times the heap
after the last full pass. That keeps a large live heap that keeps growing
(a compiler checking a large program) from being walked again and again. But
growth alone cannot tell such a heap from one that keeps garbage: a program
that drops a large structure and builds another, as a compiler rebuilding in
watch mode does, holds the dead structures' cyclic parts until the heap has
grown to 17 times its size. Ten rebuilds of benchmarks/regions/compiler.ts
without lifetime regions reached 1.3 GB that way.

The acyclic parts of such a structure go by reference counting, and that
shows: the live count falls between passes. The collector now sums those
falls since the last full pass. Once they reach 1/32 of the heap that pass
left and have stopped (a structure torn down by reference counting falls
over several checks, and a pass in the middle would find its rest still
referenced), a full pass is due as soon as the heap has grown the base
quarter past it, whatever the growth fraction. One that finds fewer older
objects than had died doubles the fall the next one needs, up to 256 times,
and a productive one restores it.

A full pass that the growth rule did not choose (this one, a backlog pass, a
scheduled-age pass) now raises the growth baselines of the levels it walked
but does not lower them. After such a pass reclaims a dropped structure, the
heap regrowing to where it was is the program rebuilding what it had. From
the low point, growth passes at both levels would walk the rebuilt live heap
and find nothing. On the ten rebuilds that was a third of the collector's
added time. A heap that only grows never falls, so its schedule is
unchanged. The checks run where the release path and the event loop already
decide which pass to run.
2026-10-18 05:42:55 -05:00
Chris Tate 85fe87ffbf Convert Date values into unknown slots as Dates
A TypeScript Date placed in an `unknown` slot (an annotated binding, an
object or array literal, a record or class field) becomes a Date holding
the same time value. Before, a Date member of an `unknown` object literal
became a function, and other conversions were refused. util.inspect prints
these Dates in their ISO form or as Invalid Date, and their methods can be
read as values through untyped references. Comparisons with a Date operand
keep their fence: each conversion creates a new Date.
2026-10-18 05:40:08 -05:00
Chris Tate b779ef71de Return a worker's memory once it and the other workers are done (#1273) 2026-10-18 05:38:03 -05:00
Chris Tate d09b380098 Serialize checked-dynamic members of typed records like JSON.stringify
The undeclared properties a checked record keeps from its source, and the
entries of an `unknown` index signature, now follow SerializeJSONProperty:
toJSON runs with the member's key, getters are read, a Buffer becomes its
{ type, data } form, undefined, function and symbol results are dropped,
and a circular structure throws the circular-structure TypeError instead of
overflowing the stack. Members whose writing cannot run program code are
written directly; the others go through the JSON.stringify protocol walk.
Index-signature entries are written from the overflow map in place,
without building a key array.
2026-10-18 05:35:23 -05:00
Chris Tate 89b5baedd7 Add t.assert.snapshot and t.assert.fileSnapshot
node:test changes, matching Node 24.22:

- t.assert.snapshot(value[, options]) serializes the value with Node's
  default serializer, JSON.stringify(value, null, 2), or with an
  options.serializers array literal applied in order, and compares it
  with assert.strictEqual against the snapshot stored under
  "<test full name> <n>" in "<entry file>.snapshot".
- t.assert.fileSnapshot(value, path[, options]) compares the serialized
  value with the contents of path.
- Snapshot files are read in the form Node writes them, exports[`key`] =
  `value`; statements, with template escapes and CRLF line endings
  cooked as in a template literal.
- Missing snapshots, unreadable snapshot files and throwing serializers
  throw Node's ERR_INVALID_STATE errors, with Node's messages.
- Running the binary with Node's --test-update-snapshots flag as its
  first argument records the values and writes the snapshot files after
  the last test, keys sorted as Node sorts them. The flag is kept out of
  process.argv and appears in process.execArgv, which is otherwise [].
- Snapshot assertions count toward the test's plan.

The snapshot fixture compares the reporter output with Node, plain and
with SCRIPTC_SAN=1, against a snapshot file Node wrote. The update-mode
harness test runs Node and the binary with --test-update-snapshots and
compares the files they write. The node:test normalization also drops
the "... N lines matching cause stack trace ..." line Node prints for an
error cause, with the stack frames it summarizes.
2026-10-18 05:31:40 -05:00
Chris Tate 4fe392cd25 Return a worker's memory once it and the other workers are done
On Linux, a worker thread that exits without having made any object it
allocated reachable from another thread gives back all of its memory: the
pages it carved, including ProgramLifetime instances and other objects it
never freed, and the system blocks it still holds. The memory goes back
once no other worker thread is running, so that returning it does not slow
the workers that are still running.

A worker carves past each slice's frontier while the arena's free lists and
returned regions are set aside, takes from those free lists only once a
slice has no bump room left, and records the system blocks it allocates.
Messages and intern tables that share published graphs by pointer, and the
process-wide key arrays of typed records, mark the thread as sharing; so
does a remote free of one of its blocks. A worker that did not share parks
with its arena claimed and its discard queued: returning pages makes the
kernel interrupt every running thread of the process to flush its TLB, and
glibc's trim locks the arenas other threads allocate from. The last worker
to park starts one helper that returns each queued worker's pages, frees
its system blocks, and restores its arena to its state at the claim, with
the free blocks the worker took free again. A thread that finds every arena
claimed runs the queued discards itself. A worker that shared keeps its
memory as before.

SCR_SA_DISCARD=0 keeps every worker's memory; SCR_SA_DISCARD=poison makes
the discarded pages inaccessible instead, for tests.
2026-10-18 05:15:17 -05:00
Chris Tate ffd760bfa2 Merge main 2026-10-18 04:55:11 -05:00
Chris Tate 5093ab9a7a Add markAsUncloneable and worker[Symbol.asyncDispose] (#1269) 2026-10-18 04:41:04 -05:00
Chris Tate c88981a466 Describe why mock.timers' tick leaves a fired timer queued
scr_mt_tick removes a fired entry only while it is still the queue's
first entry, as Node's MockTimers.tick does. A callback that queues an
entry sorting before it, such as an immediate due at now - 1, leaves the
fired timer queued, so it runs again, as it does in Node. The comment
now says so and names the fixture that checks the order against Node.
2026-10-18 04:32:24 -05:00
Chris Tate 58096a768c Define scr_uncloneable_count with an initializer
packages/runtime/test/common-symbols.test.ts requires runtime globals to
be definitions, not tentative definitions that some toolchains emit as
common symbols; scr_uncloneable_count now starts at 0 explicitly.
2026-10-18 04:19:12 -05:00
Chris Tate a3feec9b35 Add ThreadSlots, per-thread values keyed by objects, to @scriptc/experimental
ThreadSlots<K, V> holds one value per key and thread, like a WeakMap that
each thread has its own copy of; under Node.js it is a WeakMap per name
and thread. Setting a value never writes the key, so values can be set on
published keys.

Classes used as keys carry a 32-bit slot word after their header words,
assigned on the first set for a key (with a compare-and-swap for a
published key) and never reused. Each family has one block per thread
that maps slot indices to values through pages of 1024 entries. get and
has are inline reads of the slot word, the page and the entry; set and
delete call scr_slots.c, which holds one reference to each value. A key
class's free paths release the key's values, and thread exit releases the
rest.
2026-10-18 04:11:02 -05:00
Chris Tate 6d80207e4c Convert errors held as any with Error.prototype.toString
Errors that reach checked-dynamic code are objects carrying the reserved
"%error" marker, with name and message either own or on their prototype.
Several string conversions handled them differently from Node:

- `"" + error`, `error + ""` and `error == "TypeError: ..."` converted
  through the plain-object path and answered "[object Object]".
- Template literals and Array join read name and message as own members
  only, so a builtin error whose name is inherited printed its message
  alone.
- String(error) used the native error, which did not see name and message
  assigned through the dynamic view.

scr_dyn_error_string implements Error.prototype.toString over the
encoding, reading both members through the prototype, and the template
renderer, Array join, ToPrimitive, ToString and the numeric conversion use
it. Assigning a string name or message through the dynamic view now also
updates the native error, as assigning stack and cause already did.
2026-10-18 04:07:51 -05:00
Chris Tate ab1c271dde Support the regex v flag (#1268) 2026-10-18 04:01:38 -05:00
Chris Tate a18335e6c4 Revert "Return a worker's memory when it exits without sharing any of it" (#1267) 2026-10-18 04:00:54 -05:00
Chris Tate a0b1e93dec Merge main 2026-10-18 03:52:20 -05:00
Chris Tate 51777dee63 Add markAsUncloneable and worker[Symbol.asyncDispose]
markAsUncloneable(value) marks a plain object, a record, an error or a
class instance (scr_json.c); structuredClone and postMessage then throw
DataCloneError "Cannot clone object of unsupported type." for it, also
where it is nested. As in Node, the mark has no effect on primitives,
arrays, typed arrays and ArrayBuffers. Marked values are retained until
the thread exits; with nothing marked, the clone paths add one branch.

worker[Symbol.asyncDispose]() terminates the worker and resolves with
undefined after its 'exit' event, so `await using` disposes a Worker at
the end of its block. The Worker handle's symbol_get op answers the
well-known symbol: the symbol unit registers Symbol.asyncDispose with the
dyn core when the program names it, and scr_dyn_is_async_dispose_symbol
tells the worker unit, so worker programs that never name the symbol do
not link the symbol unit.

static-support.json adds rows for markAsUncloneable, isInternalThread
(already lowered as false and declared) and worker[Symbol.asyncDispose]().

Corpus 5990-5991 match Node v24.22.0; corpus-costs.json records their
costs.
2026-10-18 03:43:06 -05:00
Chris Tate d375212a32 Add BroadcastChannel and the remaining Node 24 worker_threads surfaces (#1189) 2026-10-18 03:38:36 -05:00
Chris Tate 84916f25e4 Support the regex v flag
Regex literals and new RegExp calls accept the v (unicode sets) flag, which
the vendored engine implements: class subtraction and intersection, nested
classes, \q{...} string literals and properties of strings. The flag uses
unicode mode for pattern decoding and string iteration, and flags read in
getter order (dgimsuvy). Regex literals now also report their flags in
getter order, as Node does for /x/yg, and the flag getters (global,
unicodeSets and the rest) compile on statically typed regexes.

In v mode the engine rejected `-` and doubled punctuation outside a
character class, and the `\-` escape inside \q{...}. It now applies the
class-only checks inside classes and \q{...} only, and parses \q{...}
contents with the class escapes, matching Node. The d flag is still
refused.
2026-10-18 03:37:40 -05:00
Chris Tate 3fd1d2bde6 Revert "Return a worker's memory when it exits without sharing any of it (#1258)"
This reverts commit 2385fc4fff.
2026-10-18 03:37:07 -05:00
Chris Tate d7ac6d3165 Merge main 2026-10-18 03:17:25 -05:00
Chris Tate f7a9fa988d Free large dead cycle sets in address order
A cycle pass tears down its dead set in the order the walk found it, so
the LIFO free lists handed the blocks of a large dead structure to the
next allocations scattered over every page it used, and the structures
built from them missed the cache on most objects. Sets of at least 16384
objects are now torn down in descending address order
(scr_mem_order_for_free, a bitmap of block indices per size class), so the
free lists hand the blocks back in ascending order, the order the bump
pointers carve fresh memory in.

A full pass takes every candidate buffer, so markGray now clears each
reached object's buffered mark in the header write it makes anyway,
instead of a separate loop over the candidates.
2026-10-18 02:24:53 -05:00
Chris Tate fe47b3861f Add Float16Array (#1249) 2026-10-18 02:06:17 -05:00
Chris Tate 2385fc4fff Return a worker's memory when it exits without sharing any of it (#1258) 2026-10-18 01:54:28 -05:00
Chris Tate 24096c6e67 Remove the leftover always-false isFloat16Array probe
The util.types brands answered isFloat16Array with false for every value.
Float16Array now answers it from the value's storage kind, ahead of that
probe, so this drops the unreachable entry and the declaration that
returns boolean.
2026-10-18 01:41:19 -05:00
Chris Tate 3999311319 Give the discard test's trim run an array of its own
check_discard ran trim_allocate again for its returned-regions case, which
writes the blocks into trim_blocks, the array check_trim had freed. Where
glibc maps an 8 MB block by itself (its adaptive mmap threshold, which the
allocator's heap policy leaves alone unless transparent huge pages are in
madvise mode), the free unmaps it and the worker thread's first write
faults. The case now allocates the array again and frees it.

The check that a worker takes the free blocks set aside compared the
highest block it carved with that of the worker before it, which took its
blocks from free lists below the slice's frontier, and it passed when a
worker never took them. Each run now counts the blocks it allocates past
the highest block of the class so far: at most the bump room the slice had
left, against all of them when a worker does not take the free blocks. The
residency bound after a discard allows for pre-fault requests still in
flight.
2026-10-18 01:30:10 -05:00
Chris Tate 6cdff93888 Merge main 2026-10-18 01:24:45 -05:00
Chris Tate 9750d3994a Merge main 2026-10-18 01:21:52 -05:00
Chris Tate dc1cb7c1d9 Merge main 2026-10-18 00:53:58 -05:00
Chris Tate b06e06f871 Merge main 2026-10-18 00:53:19 -05:00
Chris Tate 8077a9bfff Add DisposableStack (#1248) 2026-10-18 00:50:48 -05:00
Chris Tate 3f84d6475d Merge main 2026-10-18 00:50:24 -05:00
Chris Tate add07dba3d Return a worker's memory when it exits without sharing any of it
On Linux, a worker thread that exits without having made any object it
allocated reachable from another thread gives back all of its memory: the
pages it carved, including ProgramLifetime instances and other objects it
never freed, and the system blocks it still holds.

A worker carves past each slice's frontier while the arena's free lists and
returned regions are set aside, takes from those free lists only once a
slice has no bump room left, and records the system blocks it allocates.
Messages and intern tables that share published graphs by pointer, and the
process-wide key arrays of typed records, mark the thread as sharing; so
does a remote free of one of its blocks. When a worker that did not share
parks, the trim helper returns its pages, frees its system blocks, and
restores the arena to its state at the claim, with the free blocks the
worker took free again. A worker that shared keeps its memory as before.

SCR_SA_DISCARD=0 keeps every worker's memory; SCR_SA_DISCARD=poison makes
the discarded pages inaccessible instead, for tests.
2026-10-18 00:46:59 -05:00
Chris Tate 58e6396a1d Merge main 2026-10-18 00:42:37 -05:00
Chris Tate 7804e62041 Merge main 2026-10-18 00:39:44 -05:00
Chris Tate d6bb761ede Merge main 2026-10-18 00:37:03 -05:00
Chris Tate 6774fe5f08 Merge main 2026-10-18 00:28:33 -05:00
Chris Tate 86f5bc34bc Merge main 2026-10-18 00:26:05 -05:00
Chris Tate 5eb65cf64e Merge main 2026-10-18 00:24:03 -05:00
Chris Tate ca23c11d10 Add the cstring class to library profiles (#1253) 2026-10-18 00:04:01 -05:00
Chris Tate 87abacc400 Merge main 2026-10-18 00:01:05 -05:00
Chris Tate 370bf5962d Add the cstring class to library profiles
Library exports and callbacks passed text only as a pointer and length.
profile_format 2 adds the cstring class, a NUL-terminated `const char *`
(an i32 pointer in Wasm reactors), for export parameters, export results
and callback parameters. Parameters are copied in and decoded as UTF-8,
with U+FFFD for malformed bytes; a NULL pointer is the SC4012
host-contract trap. Results live in the result arena like string
results. A result or callback argument containing U+0000 traps instead
of being truncated.

Closes #615
Co-authored-by: ailuoku6 <23209876+ailuoku6@users.noreply.github.com>
2026-10-17 23:40:06 -05:00
Chris Tate db18d45dd4 Merge main 2026-10-17 23:39:11 -05:00
Chris Tate aa19f948c5 Settle async generator completions on Node's microtask turns
An async generator that fell off its end, with a return type other than
void, settled its last next() one microtask turn after Node, so for await
loops over it also ended a turn late. The appended implicit return now
marks the completion as settled, like a bare `return;`.

return() at a suspended yield ran the generator's finally blocks before
awaiting the return value. The request now awaits the value first and
resumes the body on the next turn, and the completion that follows settles
without a further turn, also when a finally block yields or awaits first.
An explicit `return x` reached during that unwind still takes its own turn
to await x.

Array.fromAsync accepts typed async generator sources: each next() result
is awaited and its value used as is, a throwing or rejecting mapping
function closes the generator, and a rejected next() rejects the result on
the turn Node rejects it.
2026-10-17 23:19:15 -05:00
Chris Tate ed817c017d Name the rejected value in the Object prototype TypeError
Object.setPrototypeOf and Object.create threw "Object prototype may only be
an Object or null" without the value Node appends. The message now ends
with the value's string form, as in Node: "...: 7", "...: undefined",
"...: Symbol(tag)".

A prototype or receiver that is a valid object of a kind these functions
do not handle yet (a function or Map prototype for Object.create, a Map
prototype or receiver for Object.setPrototypeOf) threw the same TypeError.
It now throws an Error that says the operation is not supported for that
value.
2026-10-17 22:50:45 -05:00