Commit Graph
1486 Commits
Author SHA1 Message Date
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 cb10fc36fc Link the dyn promise unit when a boxed record holds an async function (#1146) 2026-10-16 20:11:05 -05:00
Chris Tate 85439ae670 Merge main 2026-10-16 19:54:32 -05:00
Chris Tate 85a8381475 Follow a boxed function's result type when linking the dyn promise unit
A boxed function converts its result to dyn, which boxes any promise the
result type holds. The feature scan checked only for a result that is
itself a promise, so boxing a function returning `Promise<T> | undefined`,
a record with an async function field, or an array of async functions
still failed to link with an undefined scr_dyn_new_promise_adapting. The
scan now walks the result type like the other boxed types.

Corpus program 5812 and the runtime-features unit test cover these
results.
2026-10-16 19:46:51 -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 98f784e970 Speculate integers only where the static range proof gives up (#1157) 2026-10-16 19:29:53 -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 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 6520cd8223 Call a helper per case in the JavaScript null-byte corpus program
The cases were [label, run] tuples in an array, which JavaScript inference
types as (string | (() => void))[]. console.log(label, ...) then received a
union that includes a function value, so coverage counted two statements as
runtime fences. Each case now calls attempt(label, run) directly; the
output is unchanged.
2026-10-16 19:10:21 -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
Chris Tate d92459fefe Throw Node's errors for createHmac without a key and read Hmac.copy as undefined (#1142) 2026-10-16 19:05:42 -05:00
Malte Ubl 8f5ec2d94f Report record width copies at assertions, assignments and array elements
SC6001 only looked at call arguments and typed initializers, and only at
strict field subsets: a target that adds optional properties, the empty
record type, `r as T`, a later `v = r` into a typed binding, and records
stored as elements of a typed array literal copied silently.

The scan now reports a copy whenever the property sets differ, covers
those sites, and reports a class instance that reaches a plain record
type which does not keep its identity (the remaining boundary: interfaces
of only generic methods). Every corpus program still reports no
divergence.
2026-10-16 14:02:02 -07:00
Malte Ubl 6c9c39b3ab Keep class instance identity through structural record views
A class instance passed, stored, returned or asserted as an anonymous
object type or an interface it does not `implements` was copied into a
native record, so writes, identity checks and inspection through the view
missed the instance (`function add(o: { count: number }, n: number)
{ o.count += n }` left a Counter at 0).

ClassProtocols now scans flow sites (arguments, typed initializers and
fields, defaults, returns, arrow bodies, yields, assignments, array
elements, property values, assertions through `unknown`, and annotated
callback parameters) and marks every program object type a class
instance flows into, then propagates the mark along flows between
structural types. Marked types are class-backed protocols: their values
are checked views that keep the original object, like implemented
interfaces already were. Only sites whose source carries identity query
the checker for their target, and a checker panic skips the site.
Assertions read or written member-wise in place keep their direct field
access, and `typeof C` is not an instance.

An assertion of a class instance to such a view boxes it like an
implicit flow, and `view.n++` on a number member lowers to the checked
expanded form.
2026-10-16 14:02:02 -07:00
Malte Ubl 779f255787 Duplicate only loops whose integers multiply or take remainders
A loop whose speculated integers only count and compare gains nothing measurable from a second copy but grows the binary, so a loop is duplicated only when a speculated local or invariant is a multiplication, division or remainder operand, or a speculated write multiplies or takes a remainder. Sum-overflow corpus loops gain a remainder use so they keep exercising their bails.
2026-10-16 11:40:53 -07:00
Malte Ubl 16cac9a47c Speculate only what the static range proof leaves unproven
Locals whose reads the integer range analysis already proves are never shadowed or checked, and a loop with nothing unproven is not duplicated. The unproven targets that remain gain speculative leaves: charCodeAt results, array elements and record or class fields are read once before any check (they have no effect, so a failed check may restart the statement) and checked like any other integer. Module numbers written only by top-level code act as entry-checked invariants in loops of any function, which covers divisors computed at run time.
2026-10-16 11:40:53 -07:00
Malte Ubl 1ad7293e44 Speculate only integer-consumed locals and defer unobserved globals
A loop-written local now gets an integer shadow only when the loop reads it as an integer (multiplication, remainder, bitwise operators, comparisons, indexes) and every local its writes read is speculated, already an integer, or loop-invariant: accumulators that are only added to keep their cheap double add, and values computed from doubles no longer bail on every entry. Statically proven subexpressions use the ordinary integer emission, and sums with one statically bounded operand check the other operand before adding. In loops that run no other code and leave only through their condition or break, module globals are written back on leaving the fast copy instead of on every write.
2026-10-16 11:40:52 -07:00
Malte Ubl 3be474109a Run integer loops speculatively in checked i64 arithmetic
Innermost loops whose number locals are only written by statement-level integer arithmetic run as two copies. The fast copy keeps those locals (and loop-invariant number inputs) in i64 shadow slots holding safe integers and computes the writes with checked arithmetic, also storing the exact double so exits and calls need no write-back. A failed check (overflow past 2^53 - 1, -0, a fractional or non-finite leaf, a zero divisor) re-executes the same statement in the ordinary double copy. Module-level numbers written only by their file's top-level code qualify too.
2026-10-16 11:40:41 -07: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 9ef649577c Link the dyn promise unit when a boxed record holds an async function
Boxing a value into unknown converts promises through scr_async_dyn.c.
The runtime feature scan linked that unit only when the boxed value was
itself a promise or a function returning one, so boxing a record with an
async function field, an array of such records, or a union with such an
arm failed to link with an undefined scr_dyn_new_promise_adapting. The
scan now follows record fields, array, Set and Map elements and union
arms.

Corpus program 5812 boxes these values and compares the output with
Node.
2026-10-16 11:05:12 -05: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 97fafc4215 Read never-reassigned captured, module-level and field bindings as values (#1132) 2026-10-16 10:25:17 -05:00
Chris Tate 7592372b15 Support socket.address(), resetAndDestroy() and dropMaxConnection in net (#1140) 2026-10-16 10:23:46 -05:00
Chris Tate e1f1e235c2 Append to string array elements and o.f = o.f + x fields in place (#1141) 2026-10-16 10:23:23 -05:00
Chris Tate 2b02f53464 Throw Node's errors for createHmac without a key and read Hmac.copy as undefined
createHmac(algorithm) with no key now compiles and throws Node's
ERR_INVALID_ARG_TYPE ("Received undefined") from the key check, which
runs before the digest check as in Node; it used to be refused at
compile time. Reading copy on an Hmac, which has no copy() method,
answers undefined, so `typeof mac.copy` feature checks see what Node
reports. TypeScript already rejects that read.

Corpus 5764 covers createHmac with missing, undefined, null and number
keys, an unknown digest with and without a key, and the copy read.
2026-10-16 10:22:11 -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 01f25f6b3e Lower util.promisify over the fs callback functions (#1138) 2026-10-16 09:37:58 -05:00
Chris Tate d533fd7ce9 Merge main 2026-10-16 09:33:08 -05:00
Chris Tate 487c6477bd Test every utf8 read form with malformed files (#1137) 2026-10-16 09:25:14 -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 06f69e0d25 Dispatch closed virtual calls directly and reuse Map lookups for read-modify-write (#1134) 2026-10-16 09:17:28 -05:00
Chris Tate daa5cc256f Lower util.promisify over the fs callback functions
promisify(fs.X) over a named or namespace import of node:fs lowers as
the fs/promises member of the same name for access, appendFile, chmod,
chown, copyFile, lchown, link, lstat, lutimes, mkdir, mkdtemp, readFile,
readdir, readlink, realpath, rename, rm, rmdir, stat, statfs, symlink,
truncate, unlink, utimes and writeFile. For these, Node's promisified
callback form settles with the callback's single result and rejects
with the same errors as the fs/promises call. Calls through the bound
function accept the forms the fs/promises lowering accepts and refuse
the others by name.

fs.open (an fd number where fs/promises.open gives a FileHandle),
fs.exists (a custom promisify) and fs.read/fs.write (multi-value
callbacks) remain refused.
2026-10-16 09:14:35 -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 24e3bf7ae9 Test every utf8 read form with malformed files
The program reads the same files through readFileSync with "utf8",
"utf-8" and an options object, the fd form, fs.promises.readFile and
FileHandle.readFile, and compares each with Buffer.toString("utf8"):
valid files (ASCII, 2- to 4-byte characters, a BOM, empty) and invalid
ones (stray continuation and lead bytes, encoded surrogates, overlong
forms, sequences cut at the end of the file).
2026-10-16 09:04:11 -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 2fa733b106 Append to string array elements and o.f = o.f + x fields in place
"Append to strings in place for fields, concat chains and character
codes" made `obj.f += s` linear by handing the field's reference to a
local before the concat. Two neighbouring forms still copied the whole
string on every append, so their loops stayed quadratic:

- `parts[i] += s` on a `string[]`: the old element reached the concat
  through a retained snapshot (the hidden %compoundOld local) while the
  slot still held it. 1.6M appends spread over four slots took 11 s.
- `o.f = o.f + s` written out: the field read retains a snapshot and
  the field keeps its reference until the store.

A statement `a[i] += rhs` on a `string[]` now lowers to
`a[i] = (hole ? "undefined" : a[i]) + rhs` with the array and index saved
first (evaluation order unchanged). The backend recognizes that store and
the direct `recv.f = recv.f + suffix` on a plain string class or record
field. After the snapshot and suffix are evaluated and the store guard
ran, a field unlinks and releases its current value; an array slot in
the hole-free prefix that still holds the snapshot is swapped for the
immortal empty string and released. The concat then appends in place
when nothing else holds the string, and the result is stored as before.
Both paths unlink only when the snapshot is frame-owned (or immortal).

Aliases, interned literals, throwing suffixes, suffixes that read or
replace the field or element, rebound receivers and indices, holes,
appends past the end and appends that grow the array keep their
semantics (tests/corpus/string-field-append.ts, which also pins the
field `+=` forms).
2026-10-16 07:00:37 -07:00
Malte Ubl 4472f34427 Give iterator next() result slots when a field lifts statements
A for-of over a user iterable calls next() once per step, and a next()
that builds its { value, done } record with `this.i++` (or a compound or
nullish assignment) lowers a field through a seqExpr. Record result
entries rejected any seqExpr, so every step allocated and released a
heap record. The result entry splits a returned literal into one store
per field; that is safe for a field's seqExpr as long as no other field
names a hidden local declared inside it, which is now checked. Other
seqExpr placements keep the original rejection.

A 30M-step class-iterator loop: 200.7M -> 63.2M instructions per 3M
steps (callgrind), 150 ms -> 40 ms.
2026-10-16 06:59:43 -07: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 8d7f23ae76 Borrow generator parameters and devirtualize their constant callbacks
A generator frame owns its parameters until its final exit, across every
suspension, so an unchanged, unboxed reference parameter is a stable
owner that reads may borrow (CallLifetimes.generatorParameters). Calling
a callback parameter on every step no longer retains and releases it.

Generator declarations entered only through direct (spawn) calls are now
constant-callback consumers: when every call passes the same function,
the body enters it through the guarded direct path, which inlines it and
drops the dynamic-`this` bracket for callees that cannot observe it.
2026-10-16 06:59:42 -07:00
Malte Ubl 1127b4c7b6 Pin guarded dispatch in the inheritance codegen tests
Both hierarchies construct the base and the subclass, so the base-typed
call sites are closed sets now: they still load the vtable slot, then
call each implementation directly behind a slot test.
2026-10-16 06:54:45 -07:00