Commit Graph
6750 Commits
Author SHA1 Message Date
Chris Tate 62ef7cafd9 Merge pull request #1045 from vercel-labs/ctate/allow-45-minutes-195b3c
Allow 45 minutes for native release builds
2026-10-15 12:48:04 -05:00
Chris Tate 8fded2ce9a Allow 45 minutes for native release builds 2026-10-15 12:47:20 -05:00
Chris Tate 8c88a8f87f Add fs.opendirSync, fs.promises.opendir and fs.Dir (#1044) v0.3.3 2026-10-15 12:24:47 -05:00
Chris Tate 7bc9c78f00 Keep corpus 5407's requests within one read (#1043) 2026-10-15 12:24:25 -05:00
Chris Tate c53f864e61 v0.3.3 (#1042) 2026-10-15 12:13:13 -05:00
Chris Tate 9377dab890 Add fs.opendirSync, fs.promises.opendir and fs.Dir
opendirSync(path[, options]) and fs.promises.opendir(path[, options])
return a Dir that reads one Dirent at a time in OS order, as libuv's
uv_fs_readdir does. Dir supports path, readSync(), read(), closeSync(),
close() and for await. readSync and read answer null at the end. A
closed Dir throws ERR_DIR_CLOSED from readSync and closeSync, and
rejects with it from read and close.

for await over a Dir reads with read() until null and closes the Dir
in a finally, as Node's Dir.entries() does: normal exhaustion, break
and a throw all close it, and a loop whose body closes the Dir ends
with ERR_DIR_CLOSED.

A Dir also crosses 'unknown' slots as a native handle (Dir), so a
closure returning one can be passed to untyped JavaScript functions.
Through such a slot, path and closeSync() work, and other members throw
the "not supported yet" Error.

Open failures carry the opendir syscall and no path. options.bufferSize
is checked after the directory opens, with Node's ERR_OUT_OF_RANGE
messages. The options must be an object literal with a numeric
bufferSize and a utf8 encoding. Recursive walks, buffer encodings and
the callback forms are not lowered. TypeScript sources get a compile
diagnostic for them; in JavaScript sources, recursive walks and the
callback forms throw the fence error when the call runs.

Corpus 5544 covers both forms against Node 24.22.
2026-10-15 11:34:00 -05:00
Chris Tate b03600d328 Find fs callbacks where Node's signatures put them (#1041) 2026-10-15 11:29:40 -05:00
Chris Tate d7a7409277 Keep corpus 5407's requests within one read
The header-limit program sent heads of up to about 19 KB with long field
names. macOS loopback delivers that in more than one segment, so Node's
server rejected the head with bytes still unread and its close reset the
connection; the client socket had no 'error' listener, so Node exited on
the ECONNRESET after printing the first 431 case.

Every field is now a minimal `h:` line, which keeps each request under
7 KB, and the client socket ignores a reset so the case still ends. The
cases and their output are unchanged.
2026-10-15 11:28:15 -05:00
Chris Tate 2f8aea3279 v0.3.3 2026-10-15 11:23:16 -05:00
Chris Tate edda7b99cd Merge main 2026-10-15 11:19:22 -05:00
Chris Tate 374f9c51cd Inline string retains and dynamic value reference counting in release builds (#1030) 2026-10-15 11:18:39 -05:00
Chris Tate c2940acacc Merge main 2026-10-15 11:13:49 -05:00
Chris Tate 8f350a5441 Find fs callbacks where Node's signatures put them 2026-10-15 10:56:41 -05:00
Chris Tate b9169a86a3 Limit inline dynamic value releases per function and in sanitized builds
A function body with more than 256 inline dyn release sites now calls
scr_dyn_release at every site. Generated dispatch tables and the module
initializers and exit paths of large packages have thousands of release
sites, and LLVM's optimization time grows faster than their number:
published Effect schema programs took over 9 minutes to compile.

Sanitized builds compile the program in one AddressSanitizer pass, so
they define the inline reference counting helpers noinline and call
them at each site.
2026-10-15 10:44:20 -05:00
Chris Tate b706f2cef8 Merge main 2026-10-15 10:03:55 -05:00
Chris Tate 14bbb00d79 Partition large wasm32 programs like native ones
wasm32-wasi executables always compiled their program as one LLVM module,
so the object step of a large program ran the whole O2 pipeline and code
generation on a single thread. Native targets divide a module of more than
4 MB of LLVM text into up to 64 partitions that the helper optimizes and
compiles concurrently, importing small functions across partitions for
inlining. Nothing in the helper or the link plan is specific to native
objects: partitions link as ordinary objects with wasm-ld.

nativeProgramPartitions now applies the same size rule to every target and
no longer takes the target. A WASI test emits a program whose internal
callees, state and table entries land in different partitions, at release
and dev optimization, and checks that the partitioned objects are
deterministic and link to the same program as one object.
2026-10-15 09:59:04 -05:00
Chris Tate 27db9f46e0 Give dynamic Dirent values Node's prototype and inspect form (#1037) 2026-10-15 09:56:23 -05:00
Chris Tate 29d5fafa84 Add the missing fs/promises functions and Node's fs argument checks (#1039) 2026-10-15 09:42:05 -05:00
Chris Tate 36d45882bd Close Node 24 gaps in util.types keys, HTTP header limits and unread requests (#1021) 2026-10-15 09:29:01 -05:00
Chris Tate d92b6c4ec8 Merge main 2026-10-15 09:23:58 -05:00
Chris Tate e779d212d8 Merge main 2026-10-15 09:21:42 -05:00
Chris Tate 59cdda3930 Merge main 2026-10-15 09:20:01 -05:00
Chris Tate cffc2ee71f Merge main 2026-10-15 09:18:26 -05:00
Chris Tate f325b5f116 Add the missing fs/promises functions and Node's fs argument checks 2026-10-15 09:13:46 -05:00
Chris Tate 1996d250c9 Compute remainders by powers of two without fmod (#1038) 2026-10-15 09:06:36 -05:00
Chris Tate b7dddbbb39 Compute remainders by powers of two without fmod
`x % c` for a constant c whose magnitude is a power of two of at least 1
now computes `x - trunc(x / c) * c`, which is exact for every double, and
gives a zero result the dividend's sign. Divisors written as `2 ** k`
count as constants. Other divisors keep frem, and a fractional constant
divisor skips the integer check it can never pass.

The inline integer path for unproven operands now accepts dividends up
to 2^62 instead of 2^53. The remainder of two doubles is a double, so
the conversion back is exact, and products such as
`seed * 1103515245 + 12345` no longer call fmod.
2026-10-15 08:36:53 -05:00
Chris Tate 51c2420a78 Inline record result-slot entries and keep array headers out of loops (#1035) 2026-10-15 08:36:39 -05:00
Chris Tate 97cd12d98a Give dynamic Dirent values Node's prototype and inspect form
A Dirent row that becomes a dynamic value (untyped JavaScript, unknown
slots, rows passed from a typed module) was a plain object: its type
probes were missing, so `readdirSync(dir, { withFileTypes: true })
.map((d) => d.isFile())` in a .mjs program threw "d.isFile is not a
function", and Object.keys, JSON.stringify and util.inspect showed the
internal %dtype field.

Both record-to-dyn converters now finish Dirent rows with
scr_dyn_dirent_finish. The row gets a shared Dirent prototype that holds
isDirectory, isFile, isBlockDevice, isCharacterDevice, isSymbolicLink,
isFIFO and isSocket as non-enumerable methods. The entry kind is stored
under an own Symbol(type) key, as in Node, and %dtype becomes
non-enumerable, so a dynamic Dirent still converts back to the typed
row. Live record ops are not emitted for Dirent rows, so key walks read
the finished view. A method called with a nullish receiver throws Node's
TypeError.

util.inspect of a dynamic object now skips non-enumerable properties and
lists own enumerable symbol keys after the string keys, as Node's
default view does. Typed Dirent rows render through the dynamic view, so
both print `Dirent { name: 'a.txt', parentPath: '.', Symbol(type): 1 }`.

The finisher lives in scr_symbol.c. Programs that have the Dirent record
shape link that unit.

Corpus 5565 and 5566 check the untyped and cross-module forms against
Node.
2026-10-15 08:35:14 -05:00
Malte Ubl 0e943fedb0 Bound while counters by their loop condition
While loops discarded every loop-carried fact, so the escape-time counter
in `while (x * x + y * y <= 4 && it < MAX) { ...; it++ }` stayed a double:
its increment, its bound compare and the `it === MAX` test after the loop
all ran in floating point.

guardedLoopCounterFacts bounds a counter that the loop changes only by
increments of positive integer literals outside nested loops, switch and
exception regions, when a conjunct of the condition compares it below a
literal or a binding the loop never writes, and the condition itself never
writes it. The body runs only after that conjunct held, so every pass
starts at most at the bound and climbs by at most the sum of the body's
increments; the counter starts at its entry fact and only grows. The range
holds at every read inside and after the loop, so it also feeds the
loop-carried closure as an invariant.

The mandelbrot counter becomes an i32 slot. Corpus 5333 pins escape-time,
reversed, inclusive, multi-increment, early-exit and module-level counters,
and every case that must stay double (counters written by their condition,
decrements, fractional and unknown steps, nested increments, written
bounds, disjunctions, fractional, -0, NaN and infinite starts, limits past
2^31, 2^32 and 2^53) against Node.
2026-10-15 06:34:13 -07:00
Chris Tate c4e5c3d451 Add fs.globSync, fs.glob and fs/promises glob (#1034) 2026-10-15 08:30:46 -05:00
Chris Tate f43504e0c4 Accept KeyObject keys in the fallback CipherKey type
The bundled node:crypto declarations typed CipherKey as BinaryLike only,
so a program compiled without @types/node could not pass a secret
KeyObject to createCipheriv or createDecipheriv, although the runtime
accepts one. CipherKey is now BinaryLike | KeyObject, as in @types/node.

5250 covers keys, IVs, AAD, tags and data that reach the cipher through
records, untyped values, nested objects, Maps, arrays, generics and
KeyObjects.
2026-10-15 08:21:44 -05:00
Chris Tate 1121fe20c2 Merge main 2026-10-15 08:18:16 -05:00
Malte Ubl afe372da5b Pin the localized checksum in module byte-loop emission
Every write of the module-level checksum in module-byte-loops.ts is a proven uint32, so its initializer now keeps it in an i32 local instead of a double global with an integer view. The emission test now pins the local and the absence of double loads; state keeps its integer view.
2026-10-15 06:12:23 -07:00
Chris Tate 2c00672bca Merge main 2026-10-15 08:09:02 -05:00
Chris Tate aecd35b6c2 Merge main 2026-10-15 08:03:25 -05:00
Chris Tate 6b7e9539fb Inline record result-slot entries and keep array headers out of loops
A function that returns a record its caller only projects fills the
caller's frame storage through a %into.<name> entry. These entries are
small and called once per use, but LLVM kept them out of line in loops:
record-boundaries' pathLength called %into.pointAt twice per iteration.
They now carry inlinehint, like the indexed-read and interface-dispatch
helpers.

Once inlined, the scalar array reads in them still blocked hoisting the
arrays' header loads: the slow path's scr_arr_state and getter calls had
no memory attributes. They are now read-only cold calls, as the dense
read fallbacks already are (the getter runs only for a present element,
so it does not trap).
2026-10-15 08:02:39 -05:00
Chris Tate ad9934ebe1 Read dyn array lengths through the property helper again
Remove the `length` fast path. A `length` read on a value typed unknown
that the caller checks as a number goes back to the `length` property
helper the class dispatch generates, and scr_dyn_array_length is removed.
The numeric index path (scr_dyn_index_get) is unchanged, and
IrFunction.fastRead now has only the `index` value.
2026-10-15 08:00:27 -05:00
Chris Tate 0b468e40a1 Add fs.globSync, fs.glob and fs/promises glob
The three glob entry points run a port of Node 24's
lib/internal/fs/glob.js walk in scr_glob.c: the same LIFO queue of
(path, patterns) items over the sorted directory listings, the same
lstat, stat, readdir, realpath and seen-pattern caches, the same result
set in insertion order, and the same exclusion checks, so matches come
out in Node's order. Patterns compile with the existing minimatch port.
glob_match is split into a compile step and a test step so the walk can
reuse each compiled pattern and test it segment by segment.
nocaseMagicOnly is now an option, because exclude patterns turn it off.

Patterns (a string or string array) and options (cwd as a path or
file: URL, exclude as patterns or a function, followSymlinks,
withFileTypes) are validated in Node's order with Node's errors,
including the internal assertion Node hits for a non-string cwd with
exclude patterns. withFileTypes results are Dirent rows: name,
parentPath and the type probes. parentPath follows Node's rule for each
form: the joined path for globSync, the resolved path for fs/promises
glob.

fs.glob checks the callback and validates synchronously, then delivers
string[] or the walk's error through the filesystem callback queue.
fs/promises glob lowers as the source of a for await loop. The walk runs
when the loop starts, and the matches found before an error bind before
the error is thrown, as Node's async iterator yields them first. The
older glob(3)-based fs.glob branch is removed.

Corpus 5448-5452 and a node-types fixture check the behavior against
Node.
2026-10-15 07:59:19 -05:00
Malte Ubl 746c757c79 Clone only functions a recursive member reaches
Every pure number function got an integer clone, but clones are entered
only through the dispatchers of recursive members. A non-recursive pure
helper that no recursive member calls produced a dead internal function:
LLVM deleted it, after the frontend and optimizer had paid for it (the
self-hosted compiler emitted 11 such clones, e.g. isDigitCode and
hexValue).

Members are now the recursive members plus the members they reach, and a
module without recursive members gets no clones, so programs without
pure number recursion compile exactly as without clones.
2026-10-15 05:54:57 -07:00
Chris Tate 31ba299f87 Complete fs.Stats in both lanes (#1025) 2026-10-15 07:54:30 -05:00
Malte Ubl 3eaadd3c6e Skip integer-clone overflow checks the static ranges prove
The clones checked every `+` and `-` for int32 overflow, even where the
static integer-range analysis (ir/integer-ranges.ts) already proves the
result is an int32, such as fib's `n - 1` and `n - 2`: callers pass
int32-slots-proven arguments and the `n < 2` guard bounds n from below.

The clone host now exposes the function's IntegerRanges, computed under
the same parameter facts the clone assumes, and a sum or difference the
analysis proves within int32 is emitted as a plain `add`/`sub nsw`. Clone
values are never -0, so neither is such a result. Unproven sums, products
and the accumulator keep their checks. The clones thus use the static
proofs first and speculate only on what the analysis cannot bound.
2026-10-15 05:26:05 -07:00
Malte Ubl cae3e3bd7e Extend integer clones to arrays, mutual tail calls and proven ranges
Clones now cover more recursive number shapes:

- Recursion over array indices: a function may take number arrays it
  borrows (the caller keeps them alive for the call) and read `length`
  and elements. An element inside the array's hole-free prefix that is
  exactly an int32 is read inline; holes, other indices and other numbers
  bail to the ordinary body. Borrowed entries get dispatchers too.
- Mutual tail recursion: tail calls between members of a recursive
  component return the callee's result as is, so LLVM inlines them into
  one loop (isEven/isOdd), as it already does for the ordinary body.
- The proven int32-slots ranges of number parameters (valid at every
  call site, recursive ones included) become `llvm.assume`s in each
  activation, so `%` of non-negative values divides unsigned without -0
  handling (gcd).

A clone now overflows the stack where the ordinary body would, instead of
bailing after 2^14 loop iterations. LLVM turns the ordinary body's plain
tail calls into loops, so the clone's plain tail calls (self or mutual)
run unchecked too: endless tail recursion runs forever on either path,
as it did before clones. An accumulating call (`1 + f(n - 1)`, fib's
`f(n - 1) + f(n - 2)`) is real recursion in the ordinary body, because a
double sum is not associative. Its clone loop carries a virtual stack
pointer that starts at the real one (an inlined activation one frame
below its parent's) and drops 32 bytes, about an ordinary frame of these
functions, per iteration; passing the stack guard throws the
stack-overflow RangeError. Deep finite chains stay on the fast path, and
`1 + down(n - 1)` with n = 1e7 still throws like Node
(4984-generator-exceptions, corpus 5342).

Corpus 5343 covers array-index recursion, binomials, holes and
non-integer elements.
2026-10-15 05:26:05 -07:00
Malte Ubl 9b084b5881 Run pure number recursion through speculative integer clones
Functions whose parameters, locals and results are numbers or booleans,
and whose bodies only compute with them, branch, and call other such
functions, get an integer clone (`<symbol>.int`): number parameters
arrive as i32, `+ - * % -x` are overflow checked (a result outside int32,
or -0, bails), bitwise operators are exact, and the result returns as
`{ i32, i1 }` (value, bailed) in registers. Calls between clones need no
pending-exception test: a stack overflow leaves its RangeError pending and
bails, and bails pass through callers unchanged.

Recursive members get a dispatcher with their direct-call ABI. Callers
outside the recursion enter it: it runs the clone when every number
argument is bit-exactly an int32 (never -0, NaN, a fraction or out of
range), and otherwise, or after a bail without a pending exception, reruns
the unchanged double body, which is sound because the clone had no
effect. Calls inside the recursion keep the ordinary entries, so one bail
costs at most one more full run.

Self tail calls and accumulating self calls (`x + f(...)`) become a loop
with an overflow-checked int32 accumulator. Every partial sum and the
result being int32 keeps each double addition of the ordinary body exact.
The loop bails after 2^14 iterations, so recursion too deep for the stack
reruns as ordinary recursion and throws there. One level of non-tail self
calls is inlined into the clone, which removes the call, frame and stack
check of leaf calls.

Programs without clones compile exactly as before.
2026-10-15 05:26:04 -07:00
Chris Tate 54bc586764 Convert nullable records into widened record slots (#1033) 2026-10-15 07:21:16 -05:00
Malte Ubl 5223ec65ad Keep proven module numbers in an i32 local of their initializer
A module-level number that only its file's top-level code reads and
writes (an initializer binding) was already analyzed like a local, so
its loop arithmetic was proven int32, but it kept its double global. A
loop-carried `v = step(v, n)` at top level therefore converted v to a
double and back on every iteration: LLVM promotes the global to a
register, but cannot fold fptosi(sitofp) through the double phi.

When every write is a proven int32 (or uint32), the initializer now
keeps the binding in an i32 local that starts at the global's zero
value. Nothing else can observe the global: no other function reads or
writes it, the guard runs the body at most once, library builds have no
initializer bindings, and import-cycle bindings (checked through their
initialization flag elsewhere) keep their global. Debug builds keep the
global for the variable view.

The top-level closure kernel's v becomes an i32 local. Corpus 5324 pins
localized call results, inline remainders, uint32 hashes, labeled exits
and ++/--, and the bindings that must stay doubles or globals (growth
past int32 and 2^53, -0, fractions, NaN, reads by other functions and
closures) against Node.
2026-10-15 05:07:10 -07:00
Malte Ubl 5053cedc5b Narrow whole-program int32 facts to the values written
Parameter and result facts stopped at their level: non-negative int32 or
int32. A remainder by a parameter therefore never proved its result
(the divisor may be 0), and `(x * 31 + n) % m` kept i64 arithmetic in
the callee because x might be as large as 2^31 - 1, even when every call
passes a divisor of 1000003 and a fed-back result below it.

narrowSlotRanges runs after the optimistic fixpoint as a verified
descending iteration. Each round assumes a range per parameter and
result slot, reanalyzes the functions that read a changed assumption,
and joins every write into each slot. The assumptions are kept only when
they are inductive: every write lands inside its slot's assumed range.
A failing slot falls back to its last verified range, a reanalyzed
function must still prove its field and box writes, and each bound
narrows at most once, so recursion that shifts its argument cannot walk
a bound down one step per round. Results the level dropped are revived
when every return becomes an int32 under the narrowed parameters. The
first round reuses each function's last fixpoint analysis.

`hash(n, m)` with `h = (h * 33 + i) % m` now keeps h in an i32 slot with
urem i32, and `step(x, n)` multiplies in i32. Corpus 5332 pins narrowed
and revived facts and every case that must stay double (zero, negative,
fractional and runtime divisors, -0 remainders, doubling and
counting-down recursion, mutual recursion, results past int32) against
Node.
2026-10-15 05:02:50 -07:00
Chris Tate e10ea93fd3 Support socket and server ref() and unref() (#1032) 2026-10-15 06:54:33 -05:00
Chris Tate 3b379bf623 Convert nullable records into widened record slots
A record parameter of a function the record-flow analysis does not track
widens to the fields its own arguments can leave undefined, and since the
flow-class change those arguments include object literals and call
results. Another call can pass that parameter a value stored as
`R | undefined`: a for-of element whose loop the element analysis cannot
prove free of holes binds `R | undefined` while the checker types it `R`.
Coercing that union into the widened layout of R found no mapping and
failed with SC2003, where the declared parameter used to take the checked
extraction of the record arm.

A union of one record arm and unit arms flowing into a record that arm
lifts to now takes the same checked extraction, then the record's width
lift into the destination layout.
2026-10-15 06:50:27 -05:00
Chris Tate 167101b4fa Append to strings in place for fields, concat chains and character codes (#1031) 2026-10-15 06:48:21 -05:00
Malte Ubl a56fd8ef4d Pin regex and file-read text surfaces against Node
A corpus program feeds UTF-8 file contents (sync, fd, promise and Buffer
reads, BOM and CRLF lines) through regex test, exec span and full
results, match, matchAll, replace/replaceAll with substitutions and
callbacks, and split by regex, so the faster exec results and in-place
UTF-8 reads are covered beyond exec and readFileSync.
2026-10-15 04:47:05 -07:00