Commit Graph
6750 Commits
Author SHA1 Message Date
Chris Tate fa660eddf9 Carry interned unions in unit records (#2425) 2026-10-27 12:25:38 -05:00
Chris Tate ad3b4f9e12 Record a linker job again when a path it names changes
A Linux build that links with lld records the driver's ld.lld job with the
identities of the files and directories it names, and later links run that
job directly while those identities match. When one of them changed, for
example when a system update replaced libc or the linker, the link went back
to the driver, but the entry stayed: record() kept the existing entry, so
every later link of that shape went through the driver until the entry was
pruned.

An entry whose job names a changed path now counts as missing. A listed link
records the job again with the driver's -v -### and runs it; a traced link
through the driver records it from its -v output. Entries without a job,
for links that cannot run as one ld.lld job, are kept as before.
2026-10-27 12:22:03 -05:00
Chris Tate 3b330b850a Release a frame object's nullable class field as the class pointer it holds
A nullable class field (Tag | null) stores the class pointer itself, whatever form the union takes elsewhere. When the same union was also a Set element or a Map key, it was a boxed union outside class fields, and the frame's cleanup of a frame class released the field's raw pointer with scr_union_release. classReferenceFields now gives each field its storage type (NullableRefFields.storageType), as the heap teardown does, so the scope exit calls the class's release.
2026-10-27 12:21:27 -05:00
Malte UblandClaude Opus 5.5 551e9adcef Keep personality routines off split LLVM declarations
A shard declares every function another shard defines. The declaration
copied the definition's personality routine, which LLVM rejects on a
declaration, so on Linux (where functions with landing pads carry one)
every shard compile failed and large dev builds silently fell back to one
single-threaded translation unit. The sanitized bootstrap seed spent about
5.5 minutes in that one clang process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:59:46 -07: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 6d0cb98d7b Carry interned unions in unit records
A unit's record carries the unions it interned, as their definitions. Applying one interns the union under the key its arms give and checks that its name matches. Recursive unions still leave the unit without a record, and the arm-tag index is a memo.
2026-10-27 11:39:51 -05:00
Chris Tate bb0513c03c Collect the native compiler's profile in two phases
`scripts/refresh-cli-profile.mjs --context-sensitive` runs the training twice. The second run uses a compiler that the first profile optimizes and that is instrumented again after inlining, and it merges those counts into the first profile. Builds with the result also order each function's blocks by the counts and split cold blocks off (context-sensitive profiles). `--base-profile` runs only the second phase. `scripts/build-native-cli.mts` builds such a compiler when `SCRIPTC_NATIVE_PROFILE_GENERATE` and `SCRIPTC_NATIVE_PROFILE_USE` are both set. The release workflow's cli-profile job collects the profile this way, with a 120-minute timeout.
2026-10-27 11:39:28 -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
Chris Tate 3e2bd16a36 Reserve fewer worker arenas before smaller class slices on hosts that limit address space (#2422) 2026-10-27 11:25:09 -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 b11bc54a2b Expect the collapsed canonical forms as dynamic slots in the coverage tests
The tools field (Tool[]) and the all global ((number | Callbacks)[]) hold unions that canonical mapping maps to the checked dynamic, so the canonical fallback now degrades them: they are dynamic slots again, with SC6104 warnings and attributed operations, as these tests expected before the fallback classified them as exact.
2026-10-27 10:46:54 -05:00
Chris Tate 0c954ebfd6 Reserve fewer worker arenas before smaller class slices on hosts that limit address space
Worker runtimes reserve one region for 32 arenas of 40 class slices. When
the host refused the full region (32 arenas of 1 GiB slices, 1.28 TiB of
address space), the allocator kept 32 arenas and shrank every slice. Under
valgrind, which maps at most 56 GiB, slices dropped to 16 MiB, and every
class whose live blocks outgrew 16 MiB took the system allocator.
Callgrind profiles then counted system allocations that a native run
never makes: tsc-ts checking VS Code made 10.3 M small calloc calls under
valgrind and 0.06 M natively.

The reservation now tries arena counts from 32 down to 4 at each slice
size, from 1 GiB down, before smaller slices, and 2 and 1 arena only when
no slice size fits 4. Arenas that are not reserved stay claimed, so
threads beyond them use the system allocator, and the region bounds of
foreign frees use the arenas reserved. Hosts that map the full region keep
32 arenas of 1 GiB slices.

A runtime test limits RLIMIT_AS to 60 GiB and checks that the first thread
gets 256 MiB slices for 400,000 blocks of 56 bytes, that three more
threads get arenas and a fourth uses the system allocator, and that blocks
freed across threads keep their contents.
2026-10-27 10:43:05 -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 8f3a8e3e63 Merge main 2026-10-27 09:58:30 -05:00
Malte UblandClaude Opus 5.5 0927bfd295 Expect exact canonical mappings in the dynamic-slot coverage tests
With main's canonical-mapping gap fixes (#2340), stage N2 maps the
Tool[] field and the (Callbacks | number)[] global exactly, so they are no
longer dynamic slots. The hot-slot fixture's Hook gets an intersection arm,
which has no canonical form, so it stays a dynamic piece of Plugin[].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 07:42:33 -07: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
Malte Ubl 195ed79f00 Merge remote-tracking branch 'origin/main' into perf/static-objects
# Conflicts:
#	tests/harness/npm.test.ts
2026-10-27 07:21:50 -07:00
Chris Tate ddea35322b Check with the shared tsc-ts server by default on Linux and macOS (#2417) 2026-10-27 09:20:35 -05:00
Chris Tate df912c4b4b Reuse the supplied files' diagnostics across Node module sets (#2418) 2026-10-27 09:20:16 -05:00
Chris Tate 80bfc5fada Version for loops on the positions they advance (#2415) 2026-10-27 09:16:42 -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 85d2a63410 Keep a view's viewed record in its record-source entry instead of trailing words
View target structs no longer grow: every stack copy of such a record grew
by the two words, which enlarged unoptimized frames enough for the
sanitized self-rebuild to run out of stack. The guard's cold path looks the
slot up through the view's record-source entry (scr_record_view_slot),
which now holds the viewed record and its offsets, and stack objects of
target types are allowed again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:39:05 -07:00
Malte UblandClaude Opus 5.5 47dfa71ab1 Keep the view guard to one value in unoptimized builds
Unoptimized builds give every value a frame slot, so the inline guard
grew the frames of large recursive functions enough for the sanitized
self-rebuild to run out of stack. Development builds now call
scr_record_view_field for the slot address; optimized builds test the view
mark as one range (the frozen bit plus a count) before the cold lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:39:04 -07:00
Malte UblandClaude Opus 5.5 3f772c0ee6 Look up view slots out of line behind a cold branch
The view guard's slot lookup moves into scr_record_view_slot, and the
branch to it is weighted as unlikely, so a field access of a view target
type adds one rc-word test to its hot path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:39:04 -07:00
Malte UblandClaude Opus 5.5 ae74868e7a Plan record views only where a checked target meets a live capsule source
Targets come from the closure of checked conversion types and sources from
the closure of types boxed as live capsules; a conversion anywhere else
keeps its copy. Fewer types carry view guards, the compiler's own among
them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:39:04 -07:00
Malte UblandClaude Opus 5.5 7b91c5fe5c Plan record views before any host copies the shape host
The walkers take a narrower host type than the emitter's shape host; in
the self-hosted compiler that conversion copies the host, so a plan
assigned afterwards never reached them and the native emitter left view
guards out. The plan is now made before the copies, and arena shapes leave
its shared target set once arenas are planned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:39:03 -07:00
Malte UblandClaude Opus 5.5 10185c2147 Mark record views with the frozen bit instead of testing a trailing word
A value of a view target type can point at another storage (a shared
field read of a shape union, for one), whose block ends before the view
words. The view mark is now the rc word's frozen bit, which no other value
of a view target has (targets are never freezable; an immortal word is no
view), and the trailing words are read only after it. View targets release
like freezable shapes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:35:26 -07:00
Malte UblandClaude Opus 5.5 fe34846a54 Keep tagged record shapes out of record views and declare the view entry point
A tagged shape's values share field reads with the other arms of its shape
union, which carry no view words, so tagged shapes keep their copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:35:26 -07: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 5d3a234d07 Reuse the supplied files' diagnostics across Node module sets
The stored diagnostics of the supplied declaration files are keyed by the
whole supplied set, which includes the fallback declarations of each Node
module a program imports. A program that imports a different set of modules
checked every supplied file again, even when the cache held an answer for
the same libs, scriptc declaration files and options without module files.

The module fallback files declare only ambient modules, and the TypeScript
libs declare only globals and name no module, so adding module files cannot
change what checking the libs reports. Nor can it change what checking
scriptc's declaration files reports when the set without module files
reports nothing in them and no module file declares a module they declare.
On a miss, a program of modules now reuses the answer of the set without
module files, stored or prebuilt:

- for every file of that set when both conditions hold, and checks only its
  own files and its module files;
- for the libs alone otherwise, and checks its own files, scriptc's
  declaration files and its module files.

The answer for the whole set is then stored as before. Programs with
scripts, and sets whose module files declare a global, check in full. The
content digests behind prebuilt answers are kept per file identity, so a
miss hashes each supplied file once.
2026-10-27 08:34:10 -05:00
Chris Tate f69ff780e5 Give a stop command that matches the shared checker server
The server's command line puts --cwd and --callbacks between --api and
--listen, so `pkill -f 'tsc-ts --api --listen'` matched no server that a
build started. The docs now give `pkill -f 'tsc-ts --api .*--listen'`, and
name the flags instead of writing them as one command.
2026-10-27 08:33:04 -05: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 3b374f3201 Check with the shared tsc-ts server by default on Linux and macOS
Builds on Linux and macOS now connect to a shared `tsc-ts --api --listen`
server by default, which keeps the files it parsed and bound, so a later
build in the same working directory does not parse the TypeScript libs and
scriptc's declaration files again. SCRIPTC_CHECKER_SERVER=0 turns it off.
When CI is set to a value other than 0 or false, builds use their own tsc-ts
process unless SCRIPTC_CHECKER_SERVER=1.

- The server exits after 600 seconds without a build (was 300).
- Its name also digests the compiler executable's identity, so each scriptc
  installation and build connects only to servers it started.
- The server process runs in the root directory, so it does not keep the
  first build's directory in use; its --cwd argument is unchanged.
- When the connection to the server fails after it answered, through a
  failed transfer or an ended stream, the native CLI runs the command again
  in a new process with SCRIPTC_CHECKER_SERVER=0 and exits with its status.
- SCRIPTC_CHECKER_SERVER no longer changes the frontend cache key: the
  server's answers are the build's own checker's.

The CLI docs describe the background process, its socket directory, when it
exits and how to stop it.
2026-10-27 08:21:48 -05:00
Chris Tate d9eb38a54d Split the slowest sanitized suites by case and raise the sanitized bootstrap's stack (#2414) 2026-10-27 08:21:31 -05:00
Malte UblandClaude Opus 5.5 db2c41910c Suppress the root certificates' leak under a stripped libnode
The libnode-packs workflow's packs are stripped, so the certificate
loading threads' Node frames have no names and the node::crypto
suppressions do not match. Each certificate is parsed through libnode's
OpenSSL PEM reader, which the runtime never links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:01:13 -07: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 c554942816 Link AbortController without the fetch unit (#2412) 2026-10-27 07:57:03 -05:00
Chris Tate f553e5790d Check that recorded units leave held functions and IR data unchanged
Function tables and IR data maps hold objects a unit could change in place, which a record does not carry. The serial mode now keeps the encoded content of their entries from the start of each wave and throws an InternalCompilerError naming an entry whose content changed by the wave's end.
2026-10-27 07:54:56 -05:00
Chris Tate adbf74802f Free a worker's group-arena pool when the worker exits (#2413) 2026-10-27 07:54:31 -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 UblandClaude Opus 5.5 597b759e4f Merge the restacked base into ni/i11-cross-heap-gc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 05:41:37 -07:00