Commit Graph
2193 Commits
Author SHA1 Message Date
Chris Tate 5da70549a0 Keep int32 class fields when a program registers process event listeners (#1160) 2026-10-16 20:36:11 -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 9ede73761b Keep int32 class fields when a program registers process event listeners
process.on/off for signals, uncaughtException, unhandledRejection,
rejectionHandled and warning pass the listener to the runtime as a
dynamic value. The int32 slot analysis treated those calls as possible
dynamic stores into any field, so every class reaching dynamic code
lost its int32 fields. The runtime only keeps each listener and calls
it with fresh event values, so these helpers are read-only.
2026-10-16 20:13:00 -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 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 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 d9229b044f Inline small runtime functions into release executables (#1130) 2026-10-16 19:16:56 -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 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
Chris Tate b0d003e6fd Keep class instance identity through structural record views (#1158) 2026-10-16 19:04:19 -05:00
Malte Ubl f4ce6910c7 Inline small runtime functions into release executables
Release executables now import small runtime functions from runtime
bitcode, as speed executables do, so LLVM can inline them into program
code. Release units keep the runtime compiler's own objects, unchanged,
and additionally ship the bitcode of the same compilation reduced by the
helper's prune-import. Their unit-local symbols stay local, so only
bodies that need none of them can be imported, which keeps the import
small: about 1.3 MB per program.

Only unsanitized programs small enough for one codegen partition import.
Every partition reads the bitcode again; on the Effect fixtures (64
partitions) import cost +2..18% compile time and +4.3..4.9% size, so
partitioned release builds stay exactly as before. Speed is unchanged.

Measured against main in Linux Vercel Sandboxes, same-VM interleaved
medians (bench-runtime --layouts=4 --runs=16), on two VMs:

- bench-runtime (31 workloads): -1.3% (8 faster, 0 slower) and -1.0%
  (1 slower, typed-array-scan-workers +3.2%); size +0.0%
- js-compiled suite (22 programs): -2.2% and -1.8%; size -0.1%
- cold builds of all 53 programs, interleaved: +3.1%; helper time per
  build +40..235 ms
- Effect fixtures: byte-identical to main
2026-10-16 14:03:52 -07: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 3467efac73 Prune runtime import bitcode to importable bodies
Program builds that import runtime bitcode parsed and merged every unit's
whole module, although the import can only ever take small bodies that
need no second copy of unit-local state. The helper now reduces a unit's
bitcode to exactly that set when the runtime pack is built: the importable
bodies within the size limit at their call distance from any public
function, and the constants they copy. Every other definition becomes a
declaration, as the import would treat it. A program's selection starts
from public functions, so it is always a subset of the pruned set, and
programs import the same bodies as before.

`runtime-unit` writes the pruned bitcode for speed units; the new
`prune-import` command prunes a unit's bitcode as compiled, for units
whose objects the runtime compiler emits itself.

Linux x64 speed bitcode shrinks from 17.0 MB to 8.6 MB. Runtime objects
are byte-identical, and import lists match the unpruned bitcode exactly.
Interleaved cold builds of 53 programs in a Linux Vercel Sandbox: speed
went from +21.4% to +12.1% over release.
2026-10-16 13:12:16 -07:00
Malte Ubl 2c048881c9 Ignore products that only bound a loop's condition
A setup loop such as `for (i = 0; i < Math.floor(300000 * scale); i++) text.push(word())` multiplies only in its condition; its body does no integer work, so it keeps one copy. Also records the ts7 baseline of the speculative integer reads fixture.
2026-10-16 11:40:54 -07:00
Malte Ubl 945194919b Keep variable-step loops speculative
A sieve's inner loop `for (j = i * i; j <= limit; j += i)` only counts, but by a step the static proof cannot bound when the limit is a run-time value; its double copy pays a conversion of every index. Such loops keep their integer copy, while counters stepping by a literal still keep one copy.
2026-10-16 11:40:53 -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 52b3e290f0 Record ts7 baselines for the speculative integer fixtures 2026-10-16 11:40:52 -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 1a893ead60 Keep sequence-expression writes out of speculative integer loops
Statements inside a sequence expression run mid-expression, where a failed check cannot restart them; their writes now leave the local unspeculated. Speculative writes also run in their own frame and refuse to own temporaries, since a bailing path would skip their release. Adds planner unit tests.
2026-10-16 11:40:42 -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 973362d0d2 Merge pull request #1143 from vercel-labs/ctate/v0-3-5-031a45
v0.3.5
v0.3.5
2026-10-16 10:57:44 -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 8344af0486 Wrap only the 0.3.5 entry in the release markers 2026-10-16 10:45:57 -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 02decea877 Test that the zlib options forms lower
The one-shot zlib forms now accept any options value and validate it at
run time, so the lowering test's options cases expect no diagnostic, as
does inflateRawSync with options. A Brotli call keeps the SC2020
refusal.
2026-10-16 10:10:02 -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