Commit Graph
948 Commits
Author SHA1 Message Date
Chris Tate 5c39821726 Merge main 2026-10-17 02:30:43 -05:00
Chris Tate c34dd7898b Keep disposal snapshots out of programs that never read them
The SuppressedError hook was installed by a load-time constructor in
scr_json.c, which kept the checked-dynamic conversion of caught values in
every program that linked that unit. scr_exc_suppress now records the
disposal and body snapshots in a side table in scr_exception.c, the error's
dyn view reads them when scr_json.c builds it, and the SuppressedError's
teardown releases them.
2026-10-16 23:09:58 -05:00
Chris Tate 6d4843ea99 Dispose Disposable-typed values and expose SuppressedError's error and suppressed
`using` and `await using` now accept object literals and other values typed
Disposable, AsyncDisposable or unknown. The declaration reads the value's
[Symbol.dispose] method ([Symbol.asyncDispose] first for `await using`) and
throws Node's TypeErrors for values that are not objects and for missing or
non-callable methods; null and undefined dispose nothing. Scope exit calls the
method with the value as `this`, and `await using` awaits the result of an
async method but not of the sync fallback. A class instance reached through
such a value throws an SC2020 error, because its dynamic view has no methods.

SuppressedError is its own error class: a failing disposal over a pending
error throws one whose `error` is the disposal error and whose `suppressed` is
the pending error, both non-enumerable as in Node. `new SuppressedError(error,
suppressed, message?)` and `instanceof SuppressedError` compile, and
`extends SuppressedError` is refused.
2026-10-16 22:36:18 -05:00
Chris Tate d7a5568726 Revert "Lower child_process.exec's callback forms" (#1161) 2026-10-16 22:05:36 -05:00
Chris Tate 23e92e4cff Match libc++'s copy errors and macOS's PATH_MAX in the fs.cp tests
std::filesystem::copy_file reports a destination that exists but is not
a regular file as invalid_argument in libstdc++ and as not_supported in
libc++, which Node's macOS builds use. cpSync on Apple platforms now
throws ENOTSUP there, as Node does, and the fs errno names include
ENOTSUP.

Corpus 5571 copies a 400-level directory chain instead of 1500 levels,
so its deepest path stays under macOS's 1024-byte PATH_MAX.
2026-10-16 21:53:32 -05:00
Chris Tate db94b2d257 Add fs.cpSync, fs.cp and fs/promises cp
fs.cpSync, fs.cp and fs/promises cp compile with every option:
dereference, errorOnExist, filter, force, mode, preserveTimestamps,
recursive and verbatimSymlinks. They follow Node's two implementations.
cpSync follows cp-sync.js over the fs binding: copy errors are plain
Errors with a code, std::error_code failures throw an ErrnoException,
and links inside a copied directory point at their canonical targets.
fs.cp and fs/promises cp follow cp.js, whose copy errors are
SystemErrors with code, info, errno, syscall and path.

Options and paths validate in Node's order with Node's errors. filter
runs for the top-level paths and each entry in Node's order; an async
filter must return a promise that is already settled, and a pending one
throws SC2020. The callback and promise forms complete the copy before
the callback runs or the promise settles. Windows builds throw SC2020.

The copy lives in the new scr_fs_cp.c. Directory walks keep their frames
on the heap, so deep trees also copy on a fiber's stack. File contents
copy with sendfile on Linux, and COPYFILE_FICLONE uses FICLONE
where it exists. ScrError gains the SystemError context and the
ErrnoException key order, with matching fields in the compiler's Error
layout. The stored fs.cp callback uses the same implementation.

Corpus 5567-5571 cover the sync, callback and promise forms, errors,
symlinks and 1500-level trees.
2026-10-16 21:06:22 -05:00
Chris Tate d8c2b0f96d Lower child_process.exec's callback forms (#1161) 2026-10-16 20:57:36 -05:00
Chris Tate d53c1b8ba0 Merge main 2026-10-16 20:30:22 -05:00
Chris Tate b18a28bdef Parse IPv4 hosts in special-scheme URLs like WHATWG URL
The URL host parser kept a host whose last label is a number as a
domain, so new URL("http://127.1").hostname was "127.1" where Node
answers 127.0.0.1, and hosts such as 1.2.3.256, 0x100000000 and
1.2.3.4.5 parsed where Node throws Invalid URL.

A special-scheme host (file: included) that ends in a number now goes
through the WHATWG IPv4 parser: up to four labels after one trailing
empty label, each a decimal, 0x hexadecimal or leading-zero octal
number, every label but the last at most 255 and the last below
256^(5 - labels). The address serializes as dotted decimal, and any
failure makes the URL invalid. The constructor, URL.parse, URL.canParse,
the host and hostname setters and domainToASCII/domainToUnicode share
the parser. Non-special schemes keep their opaque host as written.
Corpus 5860 covers each form.
2026-10-16 20:27:21 -05:00
Chris Tate 2ab9055d76 Accept readline's history and completer options and add the history members (#1085) 2026-10-16 20:17:26 -05:00
Chris Tate cef618b553 Lower child_process.exec's callback forms
exec(command, callback) and exec(command, { encoding, maxBuffer },
callback) lower like execFile's callback slice. The command line runs
with /bin/sh -c, as execSync runs it, the call returns the live
ChildProcess, and the error-first callback receives the captured utf8
stdout and stderr. A non-zero exit, including the shell's 127 for a
missing command, reports Node's "Command failed: <command>\n<stderr>":
scr_exec_file takes a shell flag so the message shows the command line.

Under @types/node, child_process.ExecException maps to Error, as
ExecFileException does. Its code member is the child's numeric exit
status, which the runtime error's string code slot does not hold, so
reading it, testing it with `in`, or testing its typeof is a named
refusal.
2026-10-16 20:14:33 -05:00
Chris Tate 85439ae670 Merge main 2026-10-16 19:54:32 -05:00
Chris Tate 6e25b6a333 Share one prefault budget among worker threads (#1159) 2026-10-16 19:42:00 -05:00
Chris Tate 451924b3d0 Lower statSync's throwIfNoEntry option without building the error (#1136) 2026-10-16 19:38:44 -05:00
Chris Tate aef767791b Refuse fs paths with a null byte as Node does (#1144) 2026-10-16 19:33:14 -05:00
Chris Tate 2e52dcd35c Add Readable.from over Sets, iterators and class iterables (#1074) 2026-10-16 19:30:21 -05:00
Chris Tate e0692c81c8 Cache constant-key dyn reads per site and follow repeated key sequences in JSON.parse (#1154) 2026-10-16 19:23:30 -05:00
Chris Tate babf217ef5 Share one prefault budget among worker threads
The allocator's helper thread faults in memory just ahead of each
class slice's bump limit: one 256 KiB step on small pages and the next
2 MiB huge page once a slice is on huge pages. Every thread got that
policy. Programs with many worker threads (parse and check pools)
committed faulted-in ranges, up to a huge page per hot class per
thread, that the workers never carved, and all workers queued on the
one helper.

Worker threads now fault ahead only after their slice limits have
advanced 4 MiB, and the ranges faulted ahead of all workers together
stay within 8 MiB. A range counts against that budget until its
class's limit reaches it or its thread parks its arena, and each class
has one range in flight. Steps outside the budget are faulted by the
owner, as with SCR_SA_PREFAULT=0. A program whose allocation runs on
one worker keeps the pre-faulting. The main thread keeps the previous
policy, and executables built without worker support compile only that
policy, so single-threaded programs are unchanged.
2026-10-16 19:12:40 -05:00
Chris Tate a19fa602c9 Return primitive receivers from Object.setPrototypeOf (#1145) 2026-10-16 19:11:45 -05:00
Chris Tate 5a091f050f Run the zlib one-shot forms on the stream engine (#1139) 2026-10-16 19:11:18 -05:00
Chris Tate 26cdf9cc1d Merge main 2026-10-16 19:09:58 -05:00
Chris Tate b93b368625 Propagate errors thrown while reading an iterable's iterator method
scr_dyn_from_iterator answered kind 0 (not iterable) when reading
Symbol.asyncIterator or Symbol.iterator threw, so Readable.from raised
ERR_INVALID_ARG_TYPE in place of the getter's error. It now answers
kind -1 with the error pending, as its declaration states, and
Readable.from throws the getter's error, as Node does. Corpus 5614 covers
a throwing Symbol.iterator getter and a throwing Symbol.asyncIterator
getter.
2026-10-16 19:07:27 -05:00
Malte Ubl a6c6b63cca Follow repeated key sequences when JSON.parse builds objects
Records in a parsed array usually spell the same keys in the same order.
Like V8's JSON parser following a map's expected transition, the dyn
parser now remembers the key sequences it has built in the current parse
as a small tree rooted at the empty object. When the next member's raw
text is exactly `"<expected key>"`, it needs no string scan, no duplicate
search and no key copy, and the entry shares the tree's interned key.

The object holds exactly the keys on its path, and a transition is
recorded only when a put appended a key that was not yet present and
needed no unescaping, so a byte match proves both the decoded key and its
novelty. Duplicates, escaped keys, keys beyond four candidate transitions
and parses past 4096 transitions finish the object on the ordinary path.
The tree is freed at the end of scr_json_parse and scr_json_parse_schema.

The recycled-key check in scr_dyn_obj_put and the true/false/null match
also use the inline scr_key_equal instead of a libc memcmp call, which
took 2.5% of the 31-json dynamic variant.

Sandbox, interleaved medians of 15 runs against the branch base (with the
read-site caches): 31-json as published 2.33 s -> 2.09 s, its
`checksum = checksum + parsed[i].score` variant 2.43 s -> 2.22 s, the
12-key constant-read benchmark 1.68 s -> 1.25 s. Peak RSS is unchanged.
2026-10-16 10:14:11 -07:00
Malte Ubl 455ecc94c5 Cache constant-key reads on checked-dynamic objects per site
Every `value.name` on an any/unknown object went through the shared key
helper's kind tests, scr_dyn_obj_read, scr_dyn_obj_read_receiver and
scr_dyn_find_entry, whose one global hint table is keyed by key length and
first byte. Keys that collide there (k05/k11, or any same-length keys with
the same first letter) evicted each other and fell back to a linear scan.

A constant-key read on an ordinary object now calls scr_dyn_obj_read_ic
with a zero-initialized per-site ScrDynReadIc, thread-local wherever
program globals are. The site remembers the entry position where it last
found the key as an own data property and holds a reference to the
interned key bytes stored there. Objects that spell a key alike share
those bytes, so a hit is one pointer compare at one position; long keys
and share-table collisions compare bytes there instead. Accessors, the
builtin Object.prototype and other receiver kinds keep the previous path.
Sites release their keys with the thread's runtime state (scr_atexit),
and the RC audit lane never fills one, so its live-key count stays exact.

Sandbox, interleaved medians against the branch base: 20k parsed 12-key
records with three constant-key reads each, 300 rounds, 1.66 s -> 1.16 s;
31-json's dynamic loop variant 2.37 s -> 2.35 s (its reads are bound by
the miss on each record's entry array). Peak RSS is unchanged.
2026-10-16 10:14:11 -07:00
Chris Tate 06bcefae17 Return primitive receivers from Object.setPrototypeOf
Object.setPrototypeOf now follows the spec order. A null or undefined
receiver throws "Object.setPrototypeOf called on null or undefined",
then the prototype is checked, and a boolean, number, string, bigint or
symbol receiver is returned unchanged. Previously any receiver that was
not an object or array threw "Object prototype may only be an Object or
null".
2026-10-16 10:49:30 -05:00
Chris Tate 945017d6b5 Refuse fs paths with a null byte as Node does
A string path holding a null byte reached the system call, which saw the
path up to that byte: readFileSync("a\0b") read "a", writeFileSync wrote
it, and existsSync tested it. Node refuses such a path before any file
system access with an ERR_INVALID_ARG_VALUE TypeError naming the argument
("The argument 'path' must be a string, Uint8Array, or URL without null
bytes. Received '...'").

The fs runtime entry points now check their string paths first (each
with Node's argument name: path, prefix, oldPath/newPath, src/dest,
existingPath/newPath, target), so the sync forms throw, the promise forms
reject, and the callback form of rename throws before it queues the
operation. The Received text is util.inspect's quoted string, cut at 128
characters as Node cuts it; the JavaScript lane's validation ladders
share the same rendering, which now picks backticks for a string with
both quote kinds and escapes control characters as inspect does.
existsSync answers false.

Corpus fs-null-byte-paths.ts and fs-null-byte-paths-js.cjs cover the
sync, callback and promise forms in both lanes.
2026-10-16 10:35:19 -05:00
Chris Tate de08ab1920 v0.3.5 2026-10-16 10:33:14 -05:00
Chris Tate fe86c85902 Support socket.address(), resetAndDestroy() and dropMaxConnection in net
socket.address(), socket.resetAndDestroy() and server.dropMaxConnection
were refused at compile time, and address() and resetAndDestroy() also
through untyped socket handles. They now match Node:

- address() answers { address, family, port } while the socket has a
  local address, and {} while it connects or once it is destroyed. The
  declared AddressInfo | {} result is a checked-dynamic value, so a type
  assertion to AddressInfo narrows it.
- resetAndDestroy() closes the connection with an RST (SO_LINGER with a
  zero timeout), so the peer reads ECONNRESET, and 'close' reports
  hadError false. A connecting socket resets once it connects: the reset
  is a once('connect') listener, so it runs at that place among the
  'connect' listeners. A destroyed socket is left alone, a TLS socket
  throws ERR_INVALID_HANDLE_TYPE, and the call answers the socket.
- dropMaxConnection is a stored value (undefined until set), since Node
  reads it only in cluster mode.

Corpus 5657 (typed) and 5658 (untyped handles and the TLS throw) and the
node-types fixture net-reset-address.ts cover each member.
2026-10-16 10:01:22 -05:00
Chris Tate 7942054337 Cut per-value overhead of generators, user iterators, and awaits (#1127) 2026-10-16 09:40:38 -05:00
Chris Tate d533fd7ce9 Merge main 2026-10-16 09:33:08 -05:00
Chris Tate f4445f1540 Keep undeclared keys of checked JSON records; lower compound assignment with untyped operands (#1123) 2026-10-16 09:20:49 -05:00
Chris Tate 84b4d133fd Skip cycle collection and the JSON walk for intact parsed documents (#1124) 2026-10-16 09:19:11 -05:00
Chris Tate ed5ab6796b Run the zlib one-shot forms on the stream engine
deflateSync, inflateSync, gzipSync, gunzipSync, deflateRawSync,
inflateRawSync and unzipSync, and their callback forms, accept Node's
options and run Node's processChunkSync over the zlib stream engine.

- Options (windowBits, level, memLevel, strategy, dictionary, chunkSize,
  finishFlush, maxOutputLength, rejectGarbageAfterEnd) are validated at
  run time with Node's ladder, shared with the stream classes; info is
  refused at run time.
- Errors carry Node's errno and code; maxOutputLength reports
  ERR_BUFFER_TOO_LARGE and rejectGarbageAfterEnd
  ERR_TRAILING_JUNK_AFTER_STREAM_END. The default-options decoders also
  read every gzip member.
- Untyped JavaScript inputs may be strings, TypedArrays, DataViews or
  ArrayBuffers; other values throw Node's ERR_INVALID_ARG_TYPE after the
  options are checked.
- Results are Buffers, as in Node: they convert to their text.

Corpus programs 5378, 5379 and 5415 cover the sync, callback and untyped
input forms.
2026-10-16 09:18:32 -05:00
Chris Tate 6cfdda5c14 Hash SHA-1 and SHA-256 blocks with the SHA extensions on x86-64 (#1135) 2026-10-16 09:05:25 -05:00
Chris Tate 241006cff3 Add fs read and write streams (#1089) 2026-10-16 09:02:48 -05:00
Chris Tate 61de523d3e Lower statSync's throwIfNoEntry option without building the error
fs.statSync(path, options) and fs.lstatSync(path, options) now compile
with an inline options object, where throwIfNoEntry takes any boolean and
bigint must be false, or with a stored options object whose only member
is throwIfNoEntry, read at each call. With throwIfNoEntry: false the
result is Stats | undefined, and a missing entry (ENOENT, or ENOTDIR for a
path through a file, a dangling link's stat included) answers undefined
without building the error object, as Node's binding does. Other
failures still throw. Before, any second argument was refused.

Stats | undefined is a nullable union, so the result is the Stats pointer
itself (NULL for undefined) rather than a union box, and Stats snapshots
come from the small-object allocator. A stat-and-test probe such as
`stat !== undefined && stat.isFile()` therefore makes no system
allocator calls. stat?.isFile() and the other type tests also lower
through optional chains.

existsSync answers false for a path with a null byte, as Node does,
instead of testing the path up to that byte.

Corpus 5747 and 5748 cover the typed and JavaScript forms.
2026-10-16 09:01:01 -05:00
Malte Ubl 0d7e3a77c3 Store and take number and boolean generator values inline
A native synchronous generator body whose yield channel is a number or a
boolean caches its ScrGen's OUT slot at entry and stores each yield into
it directly instead of calling scr_coro_yield_f64/bool; for-of, spread
and yield* drives read such values back inline instead of calling
scr_gen_take_out_f64/bool. The IR validator types every yield operand as
the channel, so OUT never holds a reference at a yield and the store
needs no release. The emitter mirrors the slot as %ScrGenHead and
%ScrGenSlot; scr_async.c pins both layouts and the tags with static
assertions. WASI keeps the call paths.
2026-10-16 06:59:43 -07:00
Malte Ubl afa58342ab Cut the per-hop cost of stackless generator resumes
scr_gen_step, the compiler's own for-of/spread/yield* drive, now enters a
suspended or unstarted stackless generator directly: one function with
the hop inlined instead of resume -> switch_in -> coro_in calls. Its
emitted callers own the generator or lend it from a binding nothing can
rebind during the step, so that hop skips the generator retain/release
(the user-facing next/return/throw paths keep it). The IN slot is reset
only when it holds a value, the stackless hop skips the async-generator
state test (stackless bodies are always synchronous), and the resumer is
parked in the fiber so fewer values stay live across the body.

In lanes without exception alert state (no workers, no thread-instanced
libraries) scr_exc_cell_changed is empty, so runtime units now inline the
fiber switch's exception-cell swap as two pointer moves, mirroring the
existing inline pending test.
2026-10-16 06:59:42 -07:00
Chris Tate c268ec5c8b Hash SHA-1 and SHA-256 blocks with the SHA extensions on x86-64
On x86-64 CPUs with the SHA extensions (and SSSE3 and SSE4.1), SHA-1 and
SHA-256 process their blocks with SHA1RNDS4/SHA256RNDS2 and the
message-schedule instructions, several blocks per call. The CPU check
runs once and is cached; other CPUs, other architectures and wasm32 keep
the portable blocks, and both give the same digests. createHash,
crypto.hash, createHmac, pbkdf2, hkdf and every other caller of the two
digests use the new path.

sha-kernels.test.ts runs both block implementations over known vectors
and every length from 0 to 1100 bytes and 1 MiB, whole and in 1-, 7-,
64- and 100-byte updates. Corpus 5763 compares the digests, HMAC, PBKDF2
and Hash.copy() with Node across block-boundary lengths and update
sizes.
2026-10-16 08:43:18 -05:00
Chris Tate 21c2340ef6 Support the dgram socket options and disconnect() (#1122) 2026-10-16 08:39:23 -05:00
Chris Tate 451e925b12 Merge main 2026-10-16 08:32:21 -05:00
Malte Ubl cec14067bb Make kept-source records cheap to look up and free
The typed 31-json benchmark spent most of its time in the kept-source
table: every record was hashed to a random slot of a multi-megabyte
table, every native object free probed it, and backward-shift deletion
rescanned probe runs.

- Buckets keep keys of one 1 KiB region in order within a 64-slot run,
  so records converted, serialized and freed together touch neighboring
  slots.
- Deletion leaves a marker that insertion reuses; the amortized rehash
  drops markers (in place unless live entries fill a quarter).
- A bit filter over the keys answers the dispose hook for untracked
  objects without probing; large rehashes rebuild it.
- Pooled dyn nodes notify weak collections only.
- Inserting a fresh record skips the presence probe, and acyclic shapes
  serialize through one scr_jb_try_record_source lookup.

Sandbox (8 vCPU, median of 5): typed variant 10.8 s -> 2.5 s.
2026-10-16 06:31:51 -07:00
Malte Ubl dff726715b Keep undeclared keys of checked JSON records
`const parsed: { score: number }[] = JSON.parse(doc)` printed a different
checksum from Node: the checked conversion (and the schema-directed
JSON.parse fast path) stored only the declared fields of each object in
its native record, so JSON.stringify(parsed) silently dropped `id`,
`name` and `active`, and every other reflective surface agreed.

A closed record converted from an ordinary object now keeps the source's
own properties beside it when the object has undeclared keys or holds the
declared ones out of declaration order. The runtime keeps a per-thread
table from the native record to a view of the source's own keys
(placeholders for declared keys, which the record still owns), released
through the native dispose hook when the record is freed:

- the checked record builder notes its source (scr_record_source_note);
- the schema parser parses undeclared members into the view instead of
  declining, so typed parses keep their one-pass fast path;
- typed JSON.stringify writes such records in source key order, declared
  fields through a generated per-shape field writer and the others through
  the dyn writer;
- record capsules adopt the view and refresh it in place, so untyped reads
  and writes see and persist undeclared properties;
- util.inspect and Object.keys/values/entries answer through the live view
  (values/entries check the extra values into their static result type and
  throw the checked-conversion TypeError when one does not fit).

Copies made by object spread, Object.assign, rest destructuring or
conversion to another record type still hold only the declared fields;
the limitations page documents this.
2026-10-16 06:31:51 -07:00
Chris Tate a5da46267b Merge main 2026-10-16 08:31:20 -05:00
Chris Tate a09990192e Add tlsSocket.getProtocol, getCipher, alpnProtocol and tls.DEFAULT_MIN_VERSION/MAX_VERSION (#1054) 2026-10-16 08:23:42 -05:00
Chris Tate bb8af06bd0 Free large cycle-collector work vectors after a pass (#1114) 2026-10-16 08:20:54 -05:00
Malte Ubl cf210c4775 Skip intact documents in compiled dyn releases and new dyn writers
Main now inlines dyn releases in compiled code (shapes.ts). Its fast path
buffered every surviving array and object, so compiled releases of a
parsed document's containers (the `parsed[i]` temporaries) bypassed the
intact-region check in scr_dyn_release. Arrays and objects now keep the
inline already-buffered check and otherwise call
scr_dyn_release_survivor, which skips containers of intact documents
(ScrDynRegion) like scr_dyn_release does. Functions and proxies keep the
inline enqueue.

Two writers that main added install tables or prototypes on existing
objects directly: scr_dyn_symbol_init_own and scr_dyn_dirent_finish. Both
now clear the object's region first, keeping the invariant that intact
containers have no symbol tables or prototypes.
2026-10-16 06:18:56 -07:00
Chris Tate 21c0e17178 Merge main 2026-10-16 08:17:52 -05:00
Chris Tate 4a56c17be7 Release a FileHandle stream's owner when its options are refused
scr_fss_new takes ownership of the FileHandle owner record (and its
reference to the handle), but its first two refusal paths returned before
the shared cleanup, so a FileHandle stream refused by its options, such
as fh.createReadStream({ encoding: "nope" }), leaked both. Those paths
now release the owner as the later ones do.

Corpus 5554 refuses a FileHandle stream and then closes the handle, and
tests/harness/fs-stream-leaks.test.ts runs the fs stream programs under
LeakSanitizer in the sanitized lane: 5554 reports the leak without this
change.
2026-10-16 08:16:46 -05:00
Malte Ubl f985104b75 Tag structuredClone and decoded message documents as intact regions
structuredClone of checked-dynamic values builds a fresh tree (shared
inputs are copied per path, cycles throw), so its containers join one
ScrDynRegion as they complete, exactly like JSON.parse; a BigInt or bytes
leaf clears the region, because intact containers promise JSON.stringify
inert leaves only.

scr_message_decode (worker, MessagePort and BroadcastChannel payloads)
tags its containers before dropping the decoder's references when every
node is a JSON primitive, undefined, array or object, arrays carry no
named properties, and every container edge leads to a higher encoder id.
Ids are assigned parents first, so that proves the graph acyclic; shared
subtrees reached again from a later node and cycles stay untagged.

Region API for other whole-document producers: scr_dyn_region_open,
scr_dyn_region_tag (on each completed container) and
scr_dyn_region_close. Node layout is unchanged: arrays and objects keep
the region pointer added for parse regions (arr.region / obj.region in
the capsule payload space).
2026-10-16 06:12:55 -07:00