Commit Graph
4679 Commits
Author SHA1 Message Date
Malte Ubl 7f0488a411 Merge remote-tracking branch 'origin/main' into perf/static-objects 2026-10-27 05:35:56 -07:00
Malte UblandClaude Opus 5.5 041565d7f2 Split the slowest sanitized suites by case and raise the sanitized bootstrap's stack
Under ASan, self-hosting-llvm-emitter, three-library, npm-static and npm
each take 20 to 40 minutes as one Vitest file, so a sharded run cannot
finish before them.

- self-hosting-llvm-emitter splits into its emitter-cases-and-programs
  and fixed-point halves; a shard builds the seed once for the halves it
  owns. three-library skips the optimization its shard does not own.
  Both put their tests in a describe block, which skipUnownedTest
  partitions.
- CI runs both in every shard's case-sharded side queue
  (scripts/ci-test-plan.mjs); pnpm test:sandbox case-shards them with npm
  and npm-static.
- npm.test.ts keeps a skipped placeholder in a shard that owns no
  program, so an empty shard does not fail the file.

The sanitized native-driver bootstrap ran its instrumented compiler with
the default 8 MiB stack. Its frontend phase overflows it (SC3004) on main
as well, outside CI's ulimit for the emitter phase. The test now raises
the soft stack limit to the hard one for every sanitized compiler process
it starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 05:32:27 -07:00
Chris Tate 635f732a34 Free a worker's group-arena pool when the worker exits
Each thread keeps up to 8 MB of arena chunks and up to 64 dropped arenas
for reuse (scr_arena.c). Nothing freed them when a worker thread finished,
so every worker that used a group arena left its pool behind: 40 workers
run one after another, each dropping a 12 MB arena, kept 328 MB resident.

scr_arena_thread_exit frees the calling thread's pooled arenas and chunks.
The worker thread calls it after its context cleanup, before it posts the
exit event. The new harness test runs 24 such workers in sequence and
checks that the resident set grows by less than 32 MiB over the last 20
(160 MiB before this change, 1 MiB after).
2026-10-27 07:17:08 -05:00
Chris Tate 9ca9c7af9d Run JavaScript web streams on a native ReadableStream
In JavaScript sources, ReadableStream, ReadableStreamDefaultReader,
ReadableStreamDefaultController, CountQueuingStrategy and
ByteLengthQueuingStrategy (globals and node:stream/web exports) are class
values over a new runtime unit, scr_webstreams.c, which follows Node 24's
webstreams step by step: underlying sources, queuing strategies, default
readers, tee, ReadableStream.from and async iteration settle their promises
in Node's order. scr_dyn_handle_class.c gives a dyn handle tag a
constructor value with a prototype, brand checks and Node's constructor
errors. Stream state carries cycle headers, so the collector reclaims
streams whose callbacks close over their controllers.

Promise steps run as native promise reactions, a variant of scr_async.c
that only programs with web streams link; every other program links the
default object. TypeScript sources keep their static web stream lowerings.
Byte streams, BYOB readers, pipeTo and pipeThrough throw SC2020.

node24-web-stream-readers and node24-web-stream-readable-lifecycle now
match Node.
2026-10-27 06:53:26 -05:00
Chris Tate f4a4cf8652 Merge main 2026-10-27 04:32:00 -05:00
Chris Tate 7bd12ea89f Add a representation report and a heap census for ranking value-layout work (#2390) 2026-10-27 04:01:41 -05:00
Chris Tate cb572a7b67 Pad ASCII strings without indexing their UTF-16 units
`toUpperCase` and `toLowerCase` now record that a result of their ASCII
path is ASCII. `padStart` and `padEnd` take the UTF-16 lengths of the
receiver and the fill the way `length` does: a proven-ASCII or short
string is counted directly instead of claiming a string-index entry. A
one-unit fill repeats without a division, a one-byte fill with memset,
and a result padded from an ASCII receiver and fill is recorded as ASCII.

`word.slice(0, 3).toUpperCase().padStart(4, "-")` previously built an
index entry for the three-byte receiver on every call.
2026-10-27 02:44:56 -05:00
Chris Tate 723975d01c Prove that the arrays InlineArray fields hold are never written
The constructor copies the array it is passed into the instance, and
other reads of the field give a new array over the elements, while under
Node.js the field holds that array itself. The contract check now proves
the two agree, and reports SC7009 where it cannot:

- The arrays fields hold are never written. A whole-program graph of the
  places arrays are held (locals, parameters, results, class and record
  fields, and the elements of arrays, Maps and Sets by where those are
  held) follows every array a field read may give, and reports a write to
  one, or one that reaches a dynamic value, a call through a function
  value or a library call other than printing and JSON.stringify without
  a replacer. Functions that return their argument give each caller its
  own argument back. The report names the field read.
- The array passed to the constructor is one nothing else keeps: a new
  array (a literal, a copy, or the result of a function that returns only
  such arrays or its argument), a local array at its last use (after calls
  that only read it or write it in place, or in a loop that then assigns
  the local a new array), a parameter whose every caller passes such an
  array, an array a field already holds, or a const array that is never
  written, which is then followed like a field's.

The review fixes from before stay: `new` evaluates its arguments into
locals unless they are all locals and constants, so that the allocation
reads the length after every argument has run; the constructor uses the
array parameter only to store it; and the dynamic view of an instance
reads the field as an array of its elements.
2026-10-27 02:38:15 -05:00
Chris Tate 659a74acc4 Merge main 2026-10-27 02:27:10 -05:00
Chris Tate 5a1bc2a6d5 Drain a compressed fetch body that is still buffered when the response ends
Since the transfer drops its socket at the response's 'end', resuming a
read no longer reached the decoder: sf_response_resume_if_ready returned
before sf_inflate_resume once the transfer held no paused socket. A
compressed response whose whole body arrived before it was read then kept
its remaining decoded output buffered, and later reads stayed pending. The
buffered input and decoder output now drain on demand after 'end' as well
as while the socket is paused.
2026-10-27 01:56:31 -05:00
Chris Tate 773eb15a52 Spread a string into its code points array (#2389) 2026-10-27 01:51:43 -05:00
Chris Tate 8b256639f4 Add a representation report and a heap census for ranking value-layout work 2026-10-27 01:31:00 -05:00
Chris Tate 4b8ed48a8a Spread a string into its code points array
`[...text]` lowered to an array literal that spread the array of text's
code points: the literal's array copied the code point strings, retaining
each one, and the code point array then released them. An array literal
whose only element is a spread of a string is now the code points array
itself, as `Array.from(text)` already was. Both are fresh arrays.
2026-10-27 01:19:32 -05:00
Chris Tate 47904df3c2 Decode short non-ASCII strings directly in charCodeAt
charCodeAt on a non-ASCII string mapped the UTF-16 index to a byte offset
through a string index entry, which the first read claimed and the
string's release purged. For a string of one character, such as each
character of a for-of loop that codePointAt(0) classifies, the entry cost
more than the read.

A non-ASCII receiver of up to 16 bytes is now decoded from its start on
each read, as scr_str_utf16_len already counts short receivers. Longer
receivers keep the index entry and its cursor.
2026-10-27 01:07:00 -05:00
Chris Tate 31fe5c5a8d Convert the base of an offset typed-array index once (#2385) 2026-10-27 00:56:23 -05:00
Chris Tate 72732110ff Check offset typed-array indexes against the sum for fractional bases and keep the base the sum read
An index `base + c` converted `base` and took a fractional base as an
invalid index. A fraction smaller than the sum's precision is lost in the
sum, as in `1e-20 + 1`, which is the index 1. A base that is not an
integer now goes to the exact check of the sum itself.

The conversion also read the base local again when the access was
emitted. A write evaluates its right-hand side between the index and the
store, so `values[at + 1] = ((at = 5), 31)` wrote at the new base. The
sum now records the base value it was computed from, and the index
converts that value.

The corpus program covers both cases, and the emission test checks that
a write converts the base its sum read.
2026-10-27 00:28:04 -05:00
Chris Tate 97850ea5d2 Give the production frontend lowering test's child process an 8 GB heap (#2384) 2026-10-27 00:16:15 -05:00
Chris Tate 6d2e72367c Keep element reads through xs[xs.length - 1]! borrowed (#2383) 2026-10-26 23:54:36 -05:00
Chris Tate 7c295a2b32 Version while loops on the integer range of locals they enter with (#2382) 2026-10-26 23:32:40 -05:00
Chris Tate e6eebcc517 Convert the base of an offset typed-array index once
A typed array read or write at `base + c`, `c + base` or `base - c`,
where `base` is a number local and `c` an integer literal of magnitude
below 2^31, now converts `base` to an integer and adds `c` as an
integer, instead of converting the double sum. Accesses at several
offsets from one base, such as `x[at]` through `x[at + 7]` with a number
parameter `at`, then share one conversion and integer test. Below a
length of at most 2^52 the sum is an integer index exactly when `base`
is an integer, so the accesses read and write the same elements as
before.
2026-10-26 23:32:08 -05:00
Chris Tate a2dd09679b Give the production frontend lowering test's child process an 8 GB heap
The lowering of the complete frontend holds about 3 GB of heap. Node sizes
its default heap limit from the host's memory, and on hosts with less
memory the limit is about 2 GB, where the child runs out of heap.
2026-10-26 23:31:17 -05:00
Chris Tate cb5882b97c Run the macOS arm64 no-clang contracts in their own job (#2378) 2026-10-26 23:28:57 -05:00
Chris Tate 775a317edd Keep element reads through xs[xs.length - 1]! borrowed
A member read through a non-null asserted element read evaluates the
element's presence and then reads it, so a missing element throws Node's
TypeError. An index other than a local or a field read was held in a
hidden local first, which turned the receiver into a sequence: the
element was then read with a reference (retain and release) instead of
borrowed, and the fallback `genes[genes.length - 1]!.sc` of a search
made fasta's lookup loop 12% slower.

An index computed only from locals, field reads, number literals and the
lengths of arrays read through them (`xs.length - 1`, `i + 1`, `-i`)
has no effect and yields the same value when evaluated again with
nothing in between, so it is evaluated for the presence test and again
for the read, and the element stays a borrowed read. Other indexes keep
their hidden local.
2026-10-26 23:20:59 -05:00
Chris Tate df77671e33 Merge main 2026-10-26 23:13:34 -05:00
Chris Tate 4046b17c65 Version while loops on the integer range of locals they enter with
A while loop whose remainder reads number locals that enter it without
range facts, such as copies of module globals, parameters or fields
(`const base = column;` before `(count + base - breaks) % 60`), now runs
as two copies. On entry, a test checks that each such local holds an
integer in [-2^48, 2^48] other than -0. The fast copy uses the facts the
integer range analysis proves when the function is analyzed again with
those entry ranges assumed: the loop's counters become bounded, and the
sums and remainders run as i64 arithmetic. When the test fails, the loop
runs as it did before.

- ir/loop-entry-ranges.ts picks innermost while loops with a remainder
  that the assumed ranges prove and the function's own analysis does
  not. The test covers only the locals the remainder depends on, directly,
  through the loop's writes or through its condition.
- ir/integer-ranges.ts takes the assumed entry ranges per loop, and keeps
  no fact about the assumed locals or the loop's writes after it.
- backend/llvm/loop-entry-versions.ts emits the test and both copies.
  The test is marked likely. A function called only from the loop gets
  `alwaysinline` on the fast copy's call, because the second copy would
  otherwise cost it the inlining of a single call site.

reverse-complement (25M bases): 2.08 s -> 1.19 s; Node 3.97 s.
2026-10-26 23:02:32 -05:00
Chris Tate 80b851d65f Write untyped stores into live records and keep one object across prefix casts (#2366) 2026-10-26 22:56:27 -05:00
Chris Tate a9fc5f50ee List the macOS arm64 contracts job in the macos lane test 2026-10-26 22:46:49 -05:00
Chris Tate acf4c39f73 Keep the elements of InlineArray fields in their instances 2026-10-26 22:38:40 -05:00
Chris Tate 6131540bb2 Trace Array subclass instances in array-typed slots and test their class on element reads (#2372) 2026-10-26 22:36:57 -05:00
Chris Tate 79f456d665 Keep the static hello binary small under GNU ld (#2373) 2026-10-26 22:36:03 -05:00
Chris Tate fefbf3ac35 Reuse keep-alive connections across fetch() requests
The native fetch dialed a new connection for every request and closed it
after the response, so a loop of requests to one origin paid a TCP
connect, and for https a TLS handshake, per request. Node's fetch keeps
connections alive through undici's dispatcher. A direct request now goes
through a keep-alive agent private to fetch (scr_http_fetch_request): an
idle connection for the same host, port and bare hostname serves the next
request, there is no limit on connections per origin, and an idle
connection closes after 4 s or the server's Keep-Alive hint less one
second, without keeping the process alive. Proxied requests still dial
their own connection.

When a response ends, the transfer resumes its socket if it had paused it
and stops pausing and resuming it, since the agent may hand the socket to
the next request. scr_http_request_agent_ex and http.Agent creation share
their agent logic with the new entry point.
2026-10-26 22:25:51 -05:00
Malte UblandClaude Opus 5.5 70c39cca49 Keep frozen records frozen for untyped code and ignore refused sloppy writes
Object.freeze of a fresh record or array literal now sets the native
frozen bit instead of compiling to the literal, so an untyped reference to
the value sees it frozen: Object.isFrozen answers true and strict-mode
writes throw Node's TypeErrors.

Untyped property writes in sloppy-mode code (IrModule.sloppyDynWrites,
decided per write by isStrictCode) call scr_dyn_key_set_sloppy and its
computed form, which drop exactly the TypeError a refused assignment raises
(a read-only or frozen property, a non-extensible object, a frozen array
element) while a setter's exception still propagates. Computed-write
dispatch helpers are shared only by writes of the same mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:16:30 -07:00
Chris Tate 55cf42e9c0 Install ld.lld in the Sandbox toolchain so Sandbox links match CI (#2375) 2026-10-26 22:14:48 -05:00
Chris Tate 31f07e2d73 Read class instance fields inline at dynamic-tier property sites (#2374) 2026-10-26 22:13:38 -05:00
Chris Tate f7168d3fca Merge main 2026-10-26 22:04:34 -05:00
Chris Tate c9585ae48f Fail type checking when the generated IR field readers miss a field (#2370) 2026-10-26 21:52:07 -05:00
Chris Tate 324f89de83 Give 6809's island and native timers room on a loaded host
6809-node-island-stdio armed a 5 ms island timer and a 10 ms native timer
next to immediates. On a loaded host the main script can run past 5 ms
after arming, so Node runs the island timer before the immediates and the
output order depends on load. The timers are now 50 ms and 100 ms, well
after the script and the immediates finish; the expected order is
unchanged.
2026-10-26 21:50:42 -05:00
Chris Tate ad29d598f5 Merge main into the untyped-store branch 2026-10-26 21:44:47 -05:00
Malte UblandClaude Opus 5.5 9c42c8d74b Install ld.lld in the Sandbox toolchain so Sandbox links match CI
CI's pinned LLVM ships ld.lld, and the compiler links Linux executables
with it whenever clang finds it. The Sandbox image and the managed-image
bootstrap installed clang-22 without lld-22, so Sandbox builds linked
with GNU ld: static hello came out at 99 KB instead of 65 KB, and
runtime-tree-shaking failed only in Sandboxes. Install lld-22 in both
and check that clang finds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:40:54 -07:00
Chris Tate 6c63d8cd10 Run a constructor getter that this.constructor reads in Error.captureStackTrace
Inside a class constructor, Error.captureStackTrace(this, this.constructor) now reads this.constructor before the capture when the class or a base class declares a constructor getter, as V8 evaluates the argument: the getter runs and its exception propagates.
2026-10-26 21:39:37 -05:00
Chris Tate 2cbce33cd3 Read class instance fields inline at dynamic-tier property sites
An instance of a compiled class reaches untyped code as a capsule, which
a property site sent to its helper on every read: the helper normalizes
the capsule, tests each class's key in turn, and loads the field.

A read site now emits the helper's plain field branches inline. A
capsule whose key pointer is one of those classes' keys loads the field
from its instance and shares the inline hits' conversion. A branch is
emitted only when its field holds a plain dyn cell and every subclass
the helper's normalization can rebox the key to reads the same field
from the same slot. Getters, bag properties, unset fields and other
capsules keep the helper.
2026-10-26 21:39:18 -05:00
Chris Tate d99f355182 Give the native layout and metadata emission test 600 seconds (#2368) 2026-10-26 21:36:39 -05:00
Malte UblandClaude Opus 5.5 019fc51e31 Keep the generator tag reader and the glibc math unit out of hello
The static hello binary grew past the 96 KiB ceiling of the runtime
tree-shaking test on Linux hosts that link with GNU ld (99,040 B). CI does
not see it: its pinned LLVM ships ld.lld, which the compiler prefers, and
hello links to about 65 KB there. The Sandbox gate installs Ubuntu's
clang-22 without lld, so it links with GNU ld.

dd418b067 crossed the ceiling (98,032 -> 98,960 B). Its scr_glibc_math.c
defines floor, fmod, frexp and other glibc names as hidden functions so a
program can start without libm. With GNU ld that cannot work: ld decides
--as-needed before section GC, so the runtime's unreachable code keeps
libm needed, and it keeps every regular definition of a name a needed
shared library defines, hidden or not. Every executable carried the whole
unit (about 2.5 KB of code and unwind data) and still loaded libm. Links
without lld now leave the unit out; its functions answer what glibc's do.

192ea3247 installed scr_gen_tag_reader from a load-time constructor in
scr_async.c. A constructor is a link root on every format, so each
executable kept scr_gen_tag and, with GNU ld, all of scr_async.c's string
literals (about 1.1 KB). The generator runtime now installs the reader
when it creates a generator, the only time a reader can be needed.

Hello is now 93,928 B with GNU ld and 65,472 B with lld.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:35:52 -07:00
Chris Tate e3bd44d65b Trace Array subclass instances held in array-typed slots and test their class on element reads
An Array subclass's type maps to its element array type, so a field such
as TreeNode.kids holds a Kids instance in a slot typed as an array of
TreeNode. The collector visits such a slot as an array, which has a
collector header when its element ops trace, but the class only got a
header when its own class unit was on a cycle, and the collector read a
header the instance did not have. An Array subclass now carries a header
whenever an array of its element type does, and its collector descriptor
visits its elements instead of naming no children.

An element read of an array of Array subclass instances is a union with
undefined, and 'instanceof' against the class answered false for it. A
union now tests its array arms' class at run time.
2026-10-26 21:28:56 -05:00
Chris Tate 2c09b499b2 Check a nested record of scalar fields by its own fields
The generator listed a field as a scalar when its walk read nothing, so
a nested record holding only numbers and booleans got a ValuesNotRead
check on the record itself, which fails because the record is an object,
even after regenerating. A field is now listed as a scalar only when its
type holds no string and no object; such a record is checked through its
own fields, so a field added to it or a scalar in it that starts to hold a
string still fails type checking with its name.

The generator exports generateIrFields(irText) for the new test, which
generates the readers for an ir.ts with such a record and type-checks
them. The generated file for the current ir.ts is unchanged.
2026-10-26 21:18:38 -05:00
Chris Tate e58def0299 Update tests for the record-ops store hook and the untyped-writes benchmark
- forin-keys.test.ts matches the record-ops table with its eighth field, the
  store hook, which is null for a record no untyped code writes.
- bench/record-untyped-writes writes typed records through `unknown` and
  `Object.assign` views, so it links dynamic-tier symbols; DYNAMIC_WORKLOADS
  lists it.
- The sanitized self-hosting emission test gets a 600 s timeout. It takes
  about half of the default 300 s on an idle 16-core host, with or without
  these changes, and exceeded the default on a loaded CI shard.
2026-10-26 21:04:31 -05:00
Chris Tate 87565abc16 Merge main 2026-10-26 20:57:59 -05:00
Chris Tate 9d0ca5f641 Give the native layout and metadata emission test 600 seconds
The sanitized run of "native LLVM layout and metadata emission (llvm)"
takes 170 to 300 seconds in its CI shard beside the shard's other tests,
against the suite's 300-second test timeout. On a quiet 8-vCPU machine it
takes 130 to 160 seconds.
2026-10-26 20:50:23 -05:00
Chris Tate cd606e96ff Key baseline cache entries by their program's inputs
A recorded baseline entry now holds a hash of its program's inputs: the
entry file and its relative imports (paths and contents) and the
optimization level. --baseline-cache uses an entry only when the program
still hashes the same, so a program or an import that changed since the
record builds its baseline, instead of a recorded build error or artifact
hash of the old sources settling the comparison as both-fail or identical.
Records take schema 2; older records are ignored.
2026-10-26 19:53:49 -05:00
Malte UblandClaude Opus 5.5 d0047c813f Shard the zero-cost gate and reuse baseline work across runs
The full gate takes 30 to 75 minutes on one Sandbox, which made it the
longest step of a validation round. It can now be split and merged:

- --shard=i/n splits the bench and corpus programs (the self-hosted
  compiler runs in shard 1, or alone with --set=self), and
  scripts/zero-cost-merge.mjs merges the shards' --json reports into one
  verdict, recomputing the compile-time intervals over every shard.
- --record-baseline-cache records the baseline's artifact hashes once;
  --baseline-cache skips the baseline build of every program whose
  candidate artifacts hash the same. The record is tied to the built
  baseline checkout, toolchain and paths.
- The candidate-source self build overlaps the comparison builds on
  hosts with room for it.

pnpm test:sandbox can reuse a prepared snapshot's LLVM helper and runtime
pack (SCRIPTC_SANDBOX_NATIVE_REFERENCE) when their inputs are identical,
and retries rate-limited Sandbox creation with backoff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 17:43:44 -07:00