Commit Graph
3704 Commits
Author SHA1 Message Date
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
Malte Ubl b99e2d2558 Visit only selected captures when building regex exec results
A spans result (the compiler proved only some captures are read) used to
run the full per-capture decision for every group. Slots now start as
present undefined with a zero payload, and only the selected and lazy
capture bits are visited; full results visit every capture as before.
The hole-free prefix is measured after the fill.

String-array teardown keeps its storage pointers in registers across the
release calls (releasing a string runs no user code), and regex group
parseInt takes the short-decimal path inline (shared with
scr_parse_int_bytes as scr_parse_int_short_digits) with signed
conversions where the values are known to be far below 2^53.
2026-10-15 04:47:04 -07:00
Malte Ubl b275f47fb3 Read UTF-8 files straight into string storage
readFileSync(path | fd, "utf8") (any ASCII-case utf8/utf-8 spelling and
the empty default) used to read into a Buffer, scan it for UTF-8
validity, then copy it into a new string while scanning it again for
ASCII. It now reads directly into the final string allocation, scans
each read chunk for ASCII while it is still in cache, and validates the
remainder in place (scr_str_adopt_utf8_lossy). Only malformed input is
repaired into a new string, with the same WHATWG maximal-subpart U+FFFD
replacement as Buffer#toString; a BOM is kept.

The raw-string readers behind fs.promises.readFile(path, "utf8") and
readFileSync(fd, "utf8") now decode the same way, so malformed bytes and
encoded surrogates become U+FFFD as in Node instead of passing through.

The ASCII prefix scan tests 32 bytes per step (scr_bytes_ascii_span),
which also speeds up Buffer and TextDecoder UTF-8 decoding of long ASCII
runs.

The corpus program covers empty files, BOMs, every malformed-subpart
class, truncation at EOF, chunk-boundary offsets, large ASCII and
multibyte files, /proc pseudo-files, fd and promise reads; the runtime
unit test checks the in-place decode under ASan and the RC audit.
2026-10-15 04:46:55 -07:00
Malte Ubl c863eaa5a7 Speed up regex exec results and parseInt of numeric captures
exec/match results now allocate their slots inline with the array header
(scr_arr_new_inline): one allocation and one free per match instead of a
header plus a separately grown dense block. The capture fill loop keeps
the storage pointers in registers instead of reloading them after every
slot store.

parseInt(digits, 10) of a short all-digit string (1-15 digits, the shape
of numeric fields and regex captures) now converts directly. The general
path no longer divides per digit to detect u64 overflow (one bound per
call; the exact bignum fallback is unchanged) and skips fmod for integral
radixes in 2..36.

The new corpus program pins match-array mutation past the captured length
(push, length, unshift, sort, splice, reverse, slice) with index/input,
and parseInt across lengths, signs, prefixes, overflow and radix forms.
2026-10-15 04:46:38 -07:00
Malte Ubl 796fcbb9ce Start whole-program int32 slot facts at non-negative int32
The optimistic int32-slots fixpoint assumed exactly int32 for parameter,
result and captured-box candidates. That is too weak for remainders:
with x and n only known to be int32, `(x * 31 + n) % m` may have a
negative dividend (and a -0 result), so a kernel called as
`v = step(v, n)` could not prove step's result, v stayed a double, and
the parameters fell back to doubles too.

Unpinned candidates now start one level lower, at non-negative int32. A
write that is an int32 but may be negative demotes the slot to int32 and
reanalyzes its readers (a parameter that seeds a box demotes the box as
well); any other write drops it as before. Slots only ever widen, so the
fixpoint still ends with every remaining assumption proven at every
write. Int32 contract pins keep their int32 facts. The call-site range
retry is unchanged for slots that fail both levels.

The direct-call kernel now passes i32 to step.i32 and keeps v an i32
slot (sandbox: 2563 ms -> 964 ms, node 1256 ms). Corpus 5323 pins the
non-negative facts and every widening path (negative arguments, results
and box writes, -0 remainders, count-down recursion) against Node.
2026-10-15 04:46:13 -07:00
Malte Ubl 52ed742ae1 Keep the integer-range closure inside the self-hosted subset
The self-hosted compiler lowers nested functions as closure values, and
closure values with optional or defaulted parameters are refused
(SC1090), so the closure's defaulted `invariant` parameter broke the
self-hosting integer-range, emission, LLVM emitter and native driver
builds. closedLocals now takes the invariant ranges explicitly, havoc
keeps its three parameters, and havocWith passes a loop counter's range.

The self-hosting range cases pin the narrowed closed range: x enters as 3
and the region writes 11, so the closures keep [3, 11]; the for loop's
`x = x` initializer still evaluates to the whole assumed bound.
2026-10-15 04:46:13 -07:00
Malte Ubl 79c9c596c1 Bound counters stepped by a loop-invariant positive binding
boundedIntegerLoopFacts only accepted constant positive steps, so the
sieve's inner loop `for (let j = i * 2; j <= LIMIT; j += i)` kept j a
double: addsd, a floating bound compare, and an fptosi/sitofp exactness
check on every typed-array index.

A step may now be a binding the loop never writes (stable: not boxed,
TDZ or captured) whose entry fact is a positive integer range up to
2^31 - 1, added (or a negative range subtracted). Each update then adds
an exact integer in that range, so the counter climbs from its start to
at most the limit plus the largest step; the iteration budget for
secondary cursors uses the smallest step. j becomes an i32 slot updated
with add nsw i32.

Corpus 5322 pins zero, -0, negative, fractional, NaN and infinite steps,
steps written by the body, early exits, and counters crossing 2^31, 2^32
and 2^53 against Node.
2026-10-15 04:46:13 -07:00
Malte Ubl ea8cd32016 Close loop-carried locals under exact integer arithmetic
analyzeIntegerRanges discarded loop-carried facts except for closed locals,
whose writes only qualified syntactically (bitwise results, literals,
imul/clz32). A local like `v = (v * 31 + n) % 1000003` stayed a double, so
every iteration paid fmul/fadd plus the checked floating remainder
(fptosi, exactness and 2^53 range checks, srem, -0 select, sitofp).

The closure now evaluates each write by the same interval rules as
`expr` (exact safe integers, never -0: `*` refuses a zero times a
negative, `%` needs a non-negative dividend and positive divisor, unary
minus refuses ranges containing 0) while every candidate is assumed
inside a fixed bound: int32, uint32, or the new non-negative int32 bound
that `%` closes under. The greatest fixpoint keeps candidates whose every
write lands inside the bound; their range then narrows to the entry fact
joined with the writes' values (sound by induction over the region), and
the closures intersect. A for loop's counter, which only the update
writes, contributes its header range to reads inside the region, so
`h = (h * 131 + i) % 65521` closes too.

The kernel's v becomes an i32 slot with mul/add nsw i32 and urem i32.
Corpus 5321 pins the closed forms and every case that must stay double
(negative dividends and -0, products above 2^53, NaN/Infinity/fraction
entries, zero and fractional divisors) against Node.
2026-10-15 04:46:12 -07:00
Malte Ubl 2f23fff40d Emit 32-bit remainders of non-negative int32 operands
LLVM does not narrow the i64 urem of two extended int32s, so a proven
non-negative int32 remainder by a constant took the 64-bit magic-number
sequence (with its fixup). Emit urem i32 when both operands are proven
int32 ranges.
2026-10-15 04:46:12 -07:00
Malte Ubl 61a192919b Pack promises into a smaller block and allocate them inline
Every pending await allocates a promise. The four flags now share the
word after `state`, so ScrPromise is 128 bytes and its block with the
cycle header is 144 rather than 160 bytes. Promises are allocated and
freed through the inline cycle-allocator twins, where the constant size
folds the size class and the zeroing, and teardown skips free() for the
side arrays that were never allocated.

js-compiled 42-async: 0.83-0.85x, peak RSS 93 -> 85 MB.
2026-10-15 04:46:11 -07:00
Chris Tate 740bd38e0f Support socket and server ref() and unref()
socket.ref()/unref() and server.ref()/unref() decide whether the handle
keeps the event loop alive, on typed handles and through checked-dynamic
handles. The loop's net liveness now counts only ref'd sockets and
servers; unref'd ones still deliver their events while other work keeps
the loop running, because the sleep polls the net unit's fd whenever any
handle exists.

Corpus 5595 covers the server, client and accepted sockets.
2026-10-15 06:24:48 -05:00
Chris Tate 416311f2c5 Append to strings in place for fields, concat chains and character codes
The runtime appends to a string in place when the left operand is
uniquely owned, but three common forms never got there:

- `obj.field += s` on a plain class or record field kept a second
  reference to the old value, so every append copied the whole string.
  The compound assignment now reads a stored string field into a mutable
  local, stores "" into the field (no code runs in between: both operands
  are already strings), runs the self-append `old = old + rhs` and stores
  the result.
- `s = s + a + b` copied per append: the self-append recognized only
  `s = s + x`. A concat chain whose leftmost operand is the target now
  takes the remaining operands as the suffix, in the same order.
- `s += String.fromCharCode(c)` built a one-character string first.
  scr_str_concat_move_char_code appends the character, in place for ASCII.

Self-appends also borrow the suffix instead of retaining it, and a
discarded compound assignment no longer retains and releases its result.
2026-10-15 06:17:38 -05:00
Chris Tate b86d83f525 Merge main into createCipheriv support 2026-10-15 05:22:43 -05:00
Chris Tate 97a9257fa5 Inline string retains and dynamic value reference counting in release builds
Release builds now emit the string retain and the dyn retain and release
fast paths inline. The dyn release decides the cases scr_dyn_release
decides by kind and calls scr_dyn_release_survivor for capsules and
handles. String releases and the other families stay runtime calls, as
string-heavy programs have many release sites. The speed posture keeps
every fast path and gains the dyn release. The runtime ABI version moves
to 21 because program objects call the new runtime entry point.
2026-10-15 05:15:33 -05:00
Chris Tate 42e15185f2 Split the Limitations page into partials (#1029) 2026-10-15 05:00:30 -05:00
Chris Tate 9c11a8d1ff Merge main 2026-10-15 04:54:28 -05:00
Chris Tate bbc1d6d0b0 Merge main 2026-10-15 04:52:43 -05:00
Chris Tate af67284e3a Merge main 2026-10-15 04:50:57 -05:00
Chris Tate 7e99752246 Split the Limitations page into partials
docs/content/docs/limitations.mdx now holds the frontmatter, the
headings, the anchor spans and one <include> line per partial. The text
moves unchanged to docs/content/limitations/: one file per labeled
bullet (<section>/<label>.mdx) and one per section's other text
(<section>.mdx). Edits to different bullets no longer touch adjacent
lines of one file.

remarkDocsConventions splices the included partials into the page and
joins the lists that consecutive bullet partials contribute, so the page,
its Markdown twin and the search index render as before. The route tests
read the page with its partials inlined. A page that includes partials
takes its last-modified date, used by the sitemap, from the newest commit
to the page or any of its partials.

docs/scripts/limitations-partials.mjs joins the partials back into one
file, splits a single-file page, and merges a branch's edits to the
single-file page into the partials.
2026-10-15 04:31:49 -05:00
Chris Tate f3cd85ea17 Pass results from fs.mkdir, readFile and write callbacks (#1020) 2026-10-15 04:29:45 -05:00
Chris Tate 54b240266c List readdir entries in byte order on POSIX hosts (#1023) 2026-10-15 04:25:48 -05:00
Chris Tate 7a96feb962 Merge main 2026-10-15 04:11:04 -05:00
Chris Tate 9c205c69fc Merge main 2026-10-15 04:04:17 -05:00
Chris Tate c85f08cb7b Search arrays of objects and strings inline (#1027) 2026-10-15 04:03:09 -05:00
Chris Tate 0df4436960 Merge main 2026-10-15 04:02:33 -05:00
Chris Tate 3cd856b61b Merge main 2026-10-15 03:59:47 -05:00
Chris Tate fd69a3046d Merge main 2026-10-15 03:56:22 -05:00
Chris Tate d9c71fe72b Check corpus 5442's fractional-offset fill over eight runs (#1026) 2026-10-15 03:54:33 -05:00
Chris Tate 1df4778052 Merge main 2026-10-15 03:54:12 -05:00
Chris Tate 1dedd5eff0 Search arrays of objects and strings inline
indexOf and includes on arrays of class instances, records, arrays, maps,
sets, typed arrays and strings call an internal helper that LLVM inlines.
When the array's hole-free prefix covers its whole length, the helper
scans the dense slots itself: references compare as 64-bit slots, four
per branch, and strings compare by pointer, then length, then bytes, with
strings of up to 16 bytes compared as two words and longer ones through
scr_str_eq. Arrays with holes or sparse entries, and every other element
type, keep the runtime search.
2026-10-15 03:35:00 -05:00
Chris Tate a06112c82d Split scr_runtime.h into per-module runtime headers (#1019) 2026-10-15 03:34:00 -05:00
Chris Tate 6232b532c7 Check corpus 5442's fractional-offset fill over eight runs
randomFillSync(new Uint16Array(32), 8.5, 8) fills bytes 17 to 32, so the
last 4-byte group the check prints holds one random byte, which is zero
1 time in 256, in Node as well. The check now ORs the bytes of eight
fills before it reports the groups. The expected output is unchanged.
2026-10-15 03:19:50 -05:00
Chris Tate a08fbd2746 Merge main into createCipheriv support 2026-10-15 03:12:04 -05:00
Chris Tate 9a1effd421 Complete fs.Stats in the typed and callback lanes 2026-10-15 03:03:32 -05:00
Chris Tate b249cc6bae Regenerate the runtime header split on current main 2026-10-15 03:01:21 -05:00
Chris Tate 15202442be Merge main 2026-10-15 02:52:25 -05:00
Chris Tate 0b27e07e7a Merge main 2026-10-15 02:38:31 -05:00
Chris Tate 5d3315e268 Merge main 2026-10-15 02:36:37 -05:00
Chris Tate 72cd278f17 Run the Mach-O library and arm64 string oracle tests in the pull request macOS suite (#994) 2026-10-15 02:34:39 -05:00
Chris Tate 04ca0618dd List readdir entries in byte order on POSIX hosts
libuv's uv_fs_scandir sorts entries with strcmp on POSIX hosts, so Node's
readdirSync, readdir and fs/promises readdir list names in byte order
(with and without withFileTypes). The runtime returned the directory's
raw order, which differs from Node on most filesystems. The POSIX
scandir listing now sorts its names and entry kinds together, and
scr_fs_readdir reads names from that listing as the Windows path already
did. Windows keeps the directory's own order, as libuv does.

Corpus 5447 checks the listings against Node.
2026-10-15 02:31:42 -05:00
Chris Tate 278be10650 Compare ASCII localeCompare strings eight bytes at a time (#1018) 2026-10-15 02:30:41 -05:00
Chris Tate 4819818cdf Add assert.AssertionError with Node's properties and constructor
Assertion failures throw an assert.AssertionError with Node 24.22's
generatedMessage, code, actual, expected, operator and diff properties,
in Node's key order. new AssertionError(options) validates its options
and builds Node's message from operator, actual and expected.
AssertionError type annotations, instanceof and assert.throws(fn,
AssertionError) compile.

Failure paths record actual, expected and operator before they throw;
passing assertions do no extra work. The deep forms lower through a
per-type function that returns the verdict, so the operands stay call
temporaries. assert.equal and notEqual inspect their operands only when
they fail.

Values without a checked-dynamic form, and values whose type can be
cyclic, record undefined.
2026-10-15 02:27:13 -05:00
Chris Tate 07ce602de5 Close Node 24 gaps in util.types keys, HTTP header limits and unread requests
- Object.keys(util.types) lists Node's 43 keys in Node's order, and
  Object.keys over the node:util/types namespace lists them sorted with
  "default". Object.values and Object.entries over util.types are refused,
  because some members do not compile yet.
- The HTTP server answers 431 Request Header Fields Too Large when a head
  or trailer block exceeds 1000 fields or the header byte bound, and the
  trailer block counts its fields on its own, like Node. Parser errors send
  Node's fixed responses without a Content-Length header.
- Server requests start paused like Node's IncomingMessage: body bytes and
  'end' wait for a 'data' listener, pipe() or resume(). When the response
  finishes, an unread request is dumped and emits 'end', before the
  response's 'close' for an empty body and after it otherwise. A body that
  is still arriving when the response finishes keeps reaching its
  listeners. req.complete reads true once the body has arrived.
- Document that small Buffers own exact-size ArrayBuffers instead of
  sharing Node's 64 KiB pool.

Corpus 5405-5409 cover each case.
2026-10-15 02:26:31 -05:00
Chris Tate 161bd43adc Initialize the worker exception alert and check macOS COMMON symbols on every host (#1017) 2026-10-15 02:25:28 -05:00
Chris Tate 97b1a14c53 Add secret KeyObjects: createSecretKey, generateKey and generateKeySync (#1016) 2026-10-15 02:22:30 -05:00
Chris Tate 5336966743 Merge main into createCipheriv support
Keeps the cipher support together with the hash, random and
timingSafeEqual surface added on main.
2026-10-15 02:22:02 -05:00
Chris Tate 1843399f25 Pass results from fs.mkdir, readFile and write callback forms 2026-10-15 02:18:22 -05:00