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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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.