Commit Graph
3067 Commits
Author SHA1 Message Date
Chris Tate c5aab2833b Use an exact integer loop limit as the counted loop's endpoint (#2432) 2026-10-27 14:51:49 -05:00
Chris Tate 3c7f47e6aa Lower function values with extra optional parameters into narrower slots (#2433) 2026-10-27 14:31:51 -05:00
Chris Tate cc6ee85fd7 Run JavaScript web streams on a native ReadableStream (#2411) 2026-10-27 14:20:02 -05:00
Chris Tate d76521be94 List the held entries a unit record reads, and leave units that register units or add class fields without one (#2431) 2026-10-27 14:15:52 -05:00
Chris Tate a75cff7ab4 Use an exact integer loop limit as the counted loop's endpoint
A counted for loop converts its limit to an i64 endpoint. A limit that is
not a literal went through the double every time: converted to double,
clamped to 2^53, rounded with llvm.floor or llvm.ceil and converted back.
At the default target CPU the rounding is a libm call, which a loop such as
`for (let i = values.length - 1; i > at; i--)` paid on every entry even
when `at` was an int32 local. A limit that is an exact integer (an int32
local, an integer view, or a proven range) is its own floor and ceiling and
within ±2^53, so it is now the endpoint directly; other limits keep the
double path.
2026-10-27 13:49:48 -05:00
Chris Tate 349feb5e47 Capture stack traces with Error.captureStackTrace as Node does (#1809) 2026-10-27 13:49:06 -05:00
Chris Tate 1feed2a3ec Answer cross-layout record casts with views of the original record (#2419) 2026-10-27 13:47:06 -05:00
Chris Tate 2f636fa93d Keep class instances that stay in their frame in frame storage (#2426) 2026-10-27 13:46:18 -05:00
Malte UblandClaude Opus 5.5 76d08ef73e Lower function values with extra optional parameters into narrower slots
A function value with more parameters than the slot it flows into is
assignable in TypeScript exactly when the extra trailing parameters are
optional, and a call through the slot passes no value for them, so the
callee sees undefined as it does in JavaScript. funcCoerceAdapter refused
every such pair, so a callback parameter forwarded into a narrower slot
fell through to requireExactShape and failed with SC2003 ("union
conversion has no unambiguous layout mapping") even though the checker
accepted the program.

The adapter now supplies each omitted extra parameter with omittedArgFor
(the undefined arm of the parameter's ABI union, or the checked-dynamic
undefined). A pair whose extra parameter cannot hold undefined stays
refused, and an extra required parameter remains a checker error.

Only pairs that previously returned null reach the new code, so programs
that compiled before are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:45:13 -07:00
Chris Tate 4faabded7f List the held entries a unit record reads, and leave units that register units or add class fields without one
A record now has a reads section: the entries of its function tables that the unit's IR and new entries name and that the tables held before the unit, each with a digest of its content (contentDigest). An applier checks that it holds each one with the same content. This catches helpers named in creation order, such as %dyn.class.protocol.<n>, which another process can number differently.

Two lowering effects had no table a record could check. A unit that collects a function-local class registers the class's members as reachable units, and Object.freeze of a class instance adds hidden fields to the class layout. The lowerer now counts both (unitRegistrations, classLayoutChanges), and a unit that changes either count has no record.
2026-10-27 13:31:11 -05:00
Chris Tate 9bdb36ed46 Report published values as frozen, sealed and not extensible
Under Node.js, publish() deep-freezes everything it reaches, so
Object.isFrozen and Object.isSealed return true and Object.isExtensible
returns false for a published object, array, Map or Set. Native builds
already refused writes into published values, but answered these queries
as for an unpublished value: publication marks an object immortal and sets
no frozen bit.

- record.isFrozen, array.isFrozen and the extensibility test of Maps and
  Sets also read an immortal rc word as published, the test of the
  published write guards, for the types the program may publish
  (PublishedTypes). Other types emit the code they did.
- scr_dyn_is_frozen, scr_dyn_is_sealed and scr_dyn_is_extensible answer
  for a published object, array, Map or Set, or a checked view of one,
  before materializing the view.
2026-10-27 12:57:41 -05:00
Chris Tate 82cf32d1cd Convert number | undefined timer delays with ToNumber (#2427) 2026-10-27 12:42:02 -05:00
Chris Tate 5431a8c142 Keep class instances that stay in their frame in frame storage
A class instance could live in its function's frame only when its class
had scalar fields only and CallLifetimes proved the binding was only read.
Per-call state objects whose fields own arrays, maps and other objects,
and whose methods and helpers store into them, stayed on the heap.

frame-escapes.ts proves which locals never leave their frame: every use
keeps the value in the frame (region-ir.ts keepsInFrame), a const binding
of it in the same function shares its fate, and a closure that captures it
stays in the frame and keeps the captured binding there. Parameter and
capture facts are solved optimistically over the call graph with a
worklist; async functions, generators and module code get none.

A frame class is a standalone class whose constructor does not let this
escape. Its instance now lives in frame storage when it is bound to such a
local, when it is passed in place to a parameter that stays in the
callee's frame, or when closures that stay in the frame capture its
binding. The frame owns the reference fields: they start empty, union and
dyn fields start at undefined, and the binding's scope (or the call's
statement frame) releases them on every exit. A cycle-capable class keeps
its collector header in front of the object, never a candidate.
2026-10-27 11:56:10 -05:00
Chris Tate a3ae97c097 Specialize calls that omit a constant default and drop unreached originals (#2423) 2026-10-27 11:37:19 -05:00
Chris Tate 096bf9a364 Map a failing type exactly from its canonical form where localized failure would degrade it (#2338) 2026-10-27 11:32:18 -05:00
Malte UblandClaude Opus 5.5 cdd121405b Convert number | undefined timer delays with ToNumber
setTimeout and setInterval passed their delay to the F64 timer slot
unconverted, so a delay typed number | undefined failed with SC2003. Such
a delay is an optional `ms?: number` passed on, or a number[] element
read, whose storage keeps the out-of-range undefined. Node converts the
delay with ToNumber, so undefined is NaN, the omitted delay. A union
delay now goes through lowerOptionalNumber, as Math arguments already
do. Other unions still reach the lib boundary's refusal, and number
delays are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:11:51 -07:00
Chris Tate fa65a3b385 Specialize calls that omit a constant default and drop unreached originals 2026-10-27 10:49:22 -05:00
Chris Tate 8de39fdd57 Borrow typed-array pages and literal-ending getter returns (#2421) 2026-10-27 10:48:04 -05:00
Chris Tate 06e9672d90 Degrade a canonical form whose union mapped to the checked dynamic
Localized failure's canonical fallback classified a failing type as an exact mapping whenever its canonical form mapped to something other than the checked dynamic. A record or array holding a union that mapCanonical collapses to the checked dynamic (an arm such as {} or unknown) then counted as exact, so its slot got no SC6104 warning and its dynamic operations no attribution.

mapCanonical now notes such a collapse (CanonMapNotes), and canonicalType refuses the form when it happened, so the type degrades through the pushed-down form like any other.
2026-10-27 10:32:32 -05:00
Chris Tate 84f0368f5f Lay out code by context-sensitive profiles (#2420) 2026-10-27 10:31:49 -05:00
Chris Tate 3f81117a2d Record each reachable-emit unit's lowering as plain data, checked by a serial mode (#2410) 2026-10-27 10:07:09 -05:00
Chris Tate aed3e84ff5 Borrow typed-array pages and literal-ending getter returns 2026-10-27 10:04:40 -05:00
Chris Tate f12a2b2a23 Take JavaScript ReadableStreams as fetch bodies and in instanceof by value
A JavaScript ReadableStream given to Response, Request or fetch as a body
was converted to the string "[object ReadableStream]". The fetch unit now
reads such a stream through a reader of its own. A locked or disturbed
stream gives Node's TypeError, and the chunks must be Uint8Array
when the body is consumed ("Received non-Uint8Array chunk").

instanceof against a web stream class held in a variable or passed as an
argument walked no prototype for handles and answered false. It now starts
at the class prototype of the handle. The fetch unit's own streams, readers
and controllers, such as Response.body, are instances of the JavaScript
classes, so `response.body instanceof ReadableStream` is true again.

These paths, and the fetch reader functions' hand-off to web stream
readers, are in SCR_HANDLE_CLASSES variants of scr_json.c and scr_fetch.c,
which only programs with JavaScript web streams link; other programs that
use fetch build to the same size as on main.
2026-10-27 10:00:00 -05:00
Chris Tate d4be651952 Prove module-level number state int32 through every write (#2416) 2026-10-27 09:37:35 -05:00
Chris Tate 18196d4df0 Lay out code by context-sensitive profiles 2026-10-27 09:36:18 -05:00
Chris Tate aed7a20709 Let units without a record change the helpers they name in place
The serial mode's wave-end check threw when a unit changed a held function in place, even when the unit had no record and its change was its own lowering's: two functions that define computed-key properties on one class both rebuild the class's descriptor guard. Each unit's in-place changes are now checked against the names its IR and new entries quote: a unit with a record that changed such an entry has no record, and a unit without one keeps its changes. The wave-end check throws only for a change to an entry no unit names.
2026-10-27 09:03:39 -05:00
Chris Tate 660ddfd90e Keep the view-header test's loops speculative
Module number globals are now proven int32 through their writes, so the test's generator state starts from a value no analysis bounds to keep its loops on the speculative path. Format int32-slots.ts.
2026-10-27 08:41:05 -05:00
Malte UblandClaude Opus 5.5 6a30f02a69 Answer cross-layout record casts with views of the original record
A checked conversion of a capsule of record A into record type B, when A
has all of B's fields with the same types in any order, now answers a view:
a B object whose two trailing words hold A and A's offsets for B's fields.
Every field address of a B value tests the first word (recordViewFieldPtr,
the record clone and the JSON walker), so reads and writes through either
name reach the same storage. The view's record-source entry owns A's
capsule: boxing the view answers that capsule, so identity holds, B's
reflective surfaces and spreads see the whole object, and the entry dies
with the view, which releases A's capsule. The capsule finds its views
again through borrowed cast-list entries.

Only the target types of such conversions carry the words and the test.
They are planned from the IR before emission (record-views.ts): record
types a checked conversion can produce for which another record type in
the module has all their fields. Programs without checked conversions are
unchanged. This replaces the prefix pointer pass, whose narrower release
could not free the source's extra fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:35:26 -07:00
Chris Tate 1a20467889 Find the counter loop's fast copy inside its entry version
The counter loop's remainder by n now versions the loop on n's entry range, so the int32 slots copy ends where the copy for other entry values starts.
2026-10-27 08:31:01 -05:00
Chris Tate c54c4e1d2a Prove module-level number state int32 through every write
- Module globals of type number join the whole-program int32 fixpoint as
  `global:` candidates, starting non-negative. A candidate needs its module
  initializer to assign it at the top level (its declaration), and no
  write by `++`/`--`, for-of or catch; every assignment must evaluate to
  an int32 under the current facts (an integer literal counts in any
  function), and a write that may be negative keeps only the int32 fact.
- Reads take the proven range through IntegerSlotFacts.locals, so a
  generator state reduced by a remainder (`seed = (seed * a + c) % m`)
  takes unsigned integer remainders without the -0 select.
2026-10-27 07:57:22 -05:00
Chris Tate fb58a34ca1 Version for loops on the positions they advance
- Loop-entry versioning covers innermost for loops as well as while loops,
  and a typed array index that the assumed entry ranges prove qualifies a
  loop as a remainder does. In `for (let j = 0; j < line; j++)
  out[o++] = f(src[i++])` the test checks `line`, `o` and `i` once per
  entry, and the fast copy bounds both positions by the loop's passes.
- Proven integer loops step their shadowed bindings in expression position
  too (`out[o++] = v`), so those positions run as i64 counters in the
  fast copy and the typed array accesses use them without a round trip.
- An increment of a binding the enclosing for loop bounds takes the loop
  range where a surrounding statement has discarded its fact.
2026-10-27 07:44:34 -05:00
Malte Ubl 26557112f1 Merge remote-tracking branch 'origin/main' into lf/n2-fallback
# Conflicts:
#	packages/compiler/src/frontend/lowering/lowerer.ts
#	packages/compiler/test/canon-normalize.test.ts
2026-10-27 05:40:00 -07: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 f65c5518bf Record each reachable-emit unit's lowering as plain data, checked by a serial mode
A unit's record holds its IR in the compact encoding, the entries it added
to the helper, lifted-function and other appendable lowerer tables, and the
reachability, generic demand, statement and flag events it made. A unit has
no record when it grew a table no record carries, changed a field, replaced
an entry it did not add, or emitted a diagnostic. Every lowerer collection
and scalar field is classified, and a test fails on one that is not.

SCRIPTC_LOWER_JOURNAL=serial records every unit and replaces its results
with the ones its record carries. scripts/recorded-lowering.mjs
(pnpm test:recorded-lowering) compares corpus programs' LLVM text with and
without the mode. SCRIPTC_LOWER_JOURNAL_DUMP=<dir> also turns the mode on
and appends each unit's record to <dir>/records.jsonl. Without either,
lowering is unchanged.
2026-10-27 06:43:19 -05:00
Chris Tate 7c770d0887 Name the source file in empty split codegen parts
When --split codegen keeps the module whole, the extra outputs are new
empty modules, and their source file name defaulted to the module
identifier: the path of the private copy of the LLVM input, which has a
random name. That name reached each part's STT_FILE symbol, so two
release builds of one program differed in .strtab. The empty parts now
take the module's source file name, as the first part has it.
2026-10-27 03:30:15 -05:00
Chris Tate 773eb15a52 Spread a string into its code points array (#2389) 2026-10-27 01:51:43 -05:00
Chris Tate 331d3804a8 Decode short non-ASCII strings directly in charCodeAt (#2388) 2026-10-27 01:44:17 -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 618243caa8 Generate code concurrently for optimized executables that stay one partition (#2387) 2026-10-27 01:10:09 -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 f64b25d103 Add --large-pages to move executable code onto huge pages (#2386) 2026-10-27 00:36:40 -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 640001b260 Generate code concurrently for optimized executables that stay one partition
Optimized executables with at most 1 MiB of LLVM text were compiled as one
module on one thread. With more than 128 KiB of LLVM text they now pass
several outputs and `--split codegen` to the LLVM helper. The helper
optimizes the module whole, as for a single output, then divides its
functions in module order into runs of about the same instruction count and
generates code for each run on its own thread.

- The output count is 4 or 8 and depends only on the module's size
  (nativeCodegenParts), so artifacts never vary with the host. Node and
  native compiler builds pass the same outputs.
- The helper writes the optimized module to bitcode once with its use-list
  order. Every output after the first parses it and turns the other
  outputs' definitions into declarations; the first output does the same to
  the optimized module in place. Each function's machine code matches a
  single-output build except for jumps to a definition that another output
  also uses.
- Only local definitions that another output uses become hidden. Dead
  constant users are dropped before unused declarations are removed, so no
  output names a thread-local variable it does not use.
- When one run would hold more than three quarters of the instructions, the
  first output is generated from the optimized module as for a single
  output and the others are empty objects.
- Dev builds, partitioned builds and program-object validation are
  unchanged.
2026-10-27 00:22:17 -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 140990d257 Add --large-pages: move executable code onto transparent huge pages
A large program's hot code spans hundreds of 4 KiB pages, more than the
instruction TLB holds. tsc-ts has 12 MB of code, and 90% of its
main-thread samples at --checkers 1 fall in 274 distinct code pages.

`scriptc build --large-pages` links the new runtime unit
scr_text_pages.c, whose constructor moves the executable's code onto
2 MiB transparent huge pages before main. It copies the executable
segment, from its first to its last 2 MiB boundary, into anonymous memory
advised MADV_HUGEPAGE, makes the copy read-only and executable, and moves
it over the original range with one mremap, so addresses and instructions
stay the same. The move runs only on Linux, only when THP is "always" or
"madvise", and only in executables. If a step fails, the code stays
file-mapped. Like Node's --use-largepages, it is off by default: without
the flag the unit is not linked, and executables are unchanged.

The move costs about 3 ms per 10 MiB of code at startup, and all of the
moved code stays resident as private memory. The flag is part of the
executable cache key and the runtime-pack selection, and is refused for
non-executable output, like --detached-exit.
2026-10-26 23:41:46 -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 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 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