Commit Graph
3704 Commits
Author SHA1 Message Date
Malte Ubl 7027039809 Merge branch 'main' into perf/map-get-inline-retain 2026-10-11 16:31:53 -07:00
Malte Ubl b9d391fc8d Merge branch 'main' into fix/narrowed-property-calls 2026-10-11 16:31:49 -07:00
Malte Ubl f88830c4be Merge branch 'main' into perf/static-immutability 2026-10-11 16:31:46 -07:00
Malte Ubl 73ee5e17e9 Acyclic: a dedicated default interface; shortest cycle in explanations
`implements Acyclic` resolved to Record<never, never>, and the frontend
indexes implemented types as class protocols by symbol, so every Record
value in the program took checked identity (on tsc-ts: Int32 field
contracts failed with SC7003). The default is now AcyclicObject, an empty
interface only @scriptc/types declares, and type arguments in heritage
clauses are reported (they would make a class implement its own type).
Corpus 4642 stores an Acyclic class with an Int32 field in a Record.

explainCycle picks the shortest cycle through the unit and a mutable
edge (BFS distances forward and backward within the component): on
tsc-ts `Node.parent -> Node` instead of a 20-step path through the
checker.
2026-10-11 16:30:25 -07:00
Malte Ubl 4dc638b328 Acyclic: compile explainCycle in the self-hosted compiler; MDX-safe docs
An empty step list literal is typed explicitly so the native compiler
maps it to the step array type, and the positions list avoids braces,
which the docs pipeline would parse as an MDX expression.
2026-10-11 16:30:25 -07:00
Malte Ubl ad2fcfbc27 try/catch discharges NoThrow: handlers take what they catch
The may-throw analysis now models handlers. A throwing operation or call
inside a `try` with a `catch`, or inside a try/catch whose `finally`
never completes normally (it returns, breaks or continues, discarding the
pending exception), adds no seed and no call edge. The catch and finally
bodies are analyzed like any other code, so rethrows and a finally that
re-raises stay exact. Worker programs (a termination request unwinds
through catch) and generator bodies (a `.return()` does too) keep the
conservative answer.

Unreachable statements no longer seed: the frontend's SC9002 trap after a
body that returns from both its try and its catch (or ends in a switch
with a default, or an infinite loop) is dead code. On tsc-ts this drops
14 functions from the may-throw set and 44 pending-exception checks.

A NoThrow function may now contain handled calls; the SC7001 hint names
the remedy that applies (catch it, remove the recursion, or the worker
exception).
2026-10-11 16:30:24 -07:00
Malte Ubl 3ce9370b35 Acyclic contract type: classes and object types that cannot lie on a cycle
`class C implements Acyclic`, `interface I extends Acyclic` and
`type T = Acyclic<{ ... }>` promise that no chain of references leads
from such an object back to itself. The check is the cycle analysis that
decides tracing, so a proven type carries no cycle-collector header. When
it does not hold, SC7007 shows the cycle: the path of fields, and why it
can close (a field assigned after construction, with the first such
write; an array, Map or Set; a function, dynamic or promise field; a
subclass sharing the layout). cycle-analysis.ts gains explainCycle over
the same graph with edge labels; computeTraced is unchanged.

Acyclic elsewhere is reported with SC7006. Docs, @scriptc/types,
corpus 4642, diagnostics snapshots and TypeScript baselines included.
2026-10-11 16:30:24 -07:00
Malte Ubl 5cc3babc73 Add the ProgramLifetime contract: classes whose instances live until exit skip reference counting and cycle collection
`class Node implements ProgramLifetime` (@scriptc/types, import type only, erased for Node) promises that instances of the class and of its subclasses are kept until the program exits. scriptc then creates them immortal: `new` stores rc = SIZE_MAX and allocates no collector header, the class's retain and release entry points are empty (so reference counting on every static type of the hierarchy folds away), and the cycle analysis treats the hierarchy as a leaf, which untraces types that only reached it. Immortal objects are already part of every RC, collector, weak-collection and publication contract (static literals, published graphs).

The promise only changes memory use; SC7007 refuses it where something else could observe it: on a class that is not the root of its hierarchy (including built-in bases), in library builds, and on classes whose instances can be published to other threads. SC7006 reports the marker outside a class's implements clause.

tsc-ts with nine roots marked (Node, NodeList, FlowNode, Symbol, Type, Signature, IndexInfo, SymbolLinks, NodeLinks): single-threaded wall -25.8% Excalidraw, -22.2% TypeORM, -25.9% self-check, -28.5% Sentry; peak RSS -6.5% to -16.5%; identical diagnostics.
2026-10-11 16:30:23 -07:00
Malte Ubl cdf964ea85 Allocate plain map headers out of line
Map construction inlines into compiled callers, and the inline small-allocator path (with its clearing of recycled blocks) added 37 KB to tsc-ts across its constructors and init functions. A noinline helper keeps those call sites as small as the libc calloc call they had: tsc-ts text is now +5.6 KB over origin/main for this branch.
2026-10-11 16:30:23 -07:00
Malte Ubl 2a7c22ae49 Keep map storage on the runtime allocator
Acyclic map headers, entry arrays, bucket tables and number-key indices came from glibc (calloc/malloc/realloc/free) even when small: a map with one entry paid a 160-byte glibc header plus a 96-byte glibc entry block, and every growth step went through glibc realloc. They now use scr_mem_calloc/alloc/realloc/free like arrays, so blocks up to 512 bytes come from the size-class slices (inline free lists, huge pages after the slice prefix) and larger ones still go to the system through scr_mem_realloc. Nothing outside scr_map.c allocates or frees map storage.
2026-10-11 16:30:22 -07:00
Malte Ubl 6778826d8f Keep the array growth sequence; round only exact construction to its block
Rounding every dense capacity up to its allocator block changed the growth sequence (a 48-byte block holds 5 slots, so storage went 48/96/192/384 B instead of 48/80/144/288/576 B), which moved array storage into other size classes: +3.6% simulated L1 misses on the tsc-ts self-check and +2.6%/+4.3% wall on the self-check and TypeORM. Growth is back to 4, 8, 16, ...; only exactly sized construction fills its block. Adds a runtime test for exact construction followed by appends, index writes and length extension over the spare capacity.
2026-10-11 16:30:22 -07:00
Malte Ubl ead9f4ecba Size array storage to its construction count and its allocator block
Arrays constructed with a known element count (literals, slices, map/filter results) reserved at least four slots, so a one-element literal took a 48-byte block for 9 bytes of content. Construction now allocates exactly the requested slots, and any dense capacity is rounded up to fill its small-object allocator block (1 slot -> 16 B, 2-3 -> 32 B). Appends below four slots grow straight to four and double from there as before. The first storage of an array is a plain allocation with its states cleared, without the realloc, state move and sparse absorption of later growth. New capacity still starts as holes.
2026-10-11 16:30:21 -07:00
Chris Tate c9fe46d8c0 Merge main into perf/offthread-prefault 2026-10-11 18:10:02 -05:00
Chris Tate ddacf49d11 Merge pull request #875 from vercel-labs/ctate/prepare-v0-2-02ac14
Prepare v0.2.9
v0.2.9
2026-10-11 18:02:02 -05:00
Chris Tate 6adb753e89 Correct the runtime ABI version in the 0.2.9 notes 2026-10-11 17:32:45 -05:00
Chris Tate fa11424e5f Prepare v0.2.9 2026-10-11 17:25:14 -05:00
Chris Tate 83dce99e51 Merge pull request #874 from vercel-labs/ctate/give-sanitized-emitter-dec393
Give the sanitized emitter CI job swap and a larger stack
2026-10-11 17:23:51 -05:00
Chris Tate 3bf0d0b9a5 Give the sanitized emitter job swap and a larger stack
The sanitized native emitter peaks near 13 GB while it emits the
compiler and then compiles the 348 MB module with clang under
AddressSanitizer, which together exceed the 16 GB runner. Back the
rebuild-emit job with a 24 GiB swap file and lift its stack limit,
since the instrumented emitter's recursion can exceed 8 MiB.
2026-10-11 16:45:12 -05:00
Malte Ubl 2162b0af65 Identity bitwise forms over proven int32 operands return the operand
`x | 0`, `x ^ 0`, `x & -1`, `x << 0`, `x >> 0` (and `x >>> 0` for a
uint32) over an operand the integer range analysis proves an int32 now
return the operand's number and its integer views instead of converting
to i32 and back: ranges exclude -0, so the result is the operand bit for
bit. The range analysis computes bitwise result ranges from the operand
ranges (exact ToInt32/ToUint32 views, shift-count ranges) instead of the
blanket int32 range, so `(n & 0xff) | 0`, `s.length >> 1` or `x >>> 1`
keep tight bounds for later integer consumers.
2026-10-11 14:24:59 -07:00
Malte Ubl 9899c4014b Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 13:39:52 -07:00
Malte Ubl bb274adc5d Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 13:27:39 -07:00
Malte Ubl d57b7c4b63 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 13:27:32 -07:00
Malte Ubl eae8e02481 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 13:27:25 -07:00
Malte Ubl 5ac6252916 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 13:27:18 -07:00
Malte Ubl 0148f6b741 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
2026-10-11 13:27:11 -07:00
Chris Tate c9452c8c33 Merge pull request #867 from vercel-labs/perf/checker-round4
PR: Read nullable-union array elements inline and borrow them for preserving live ranges
2026-10-11 15:25:09 -05:00
Chris Tate d761cb530d Merge main into perf/static-immutability 2026-10-11 15:19:32 -05:00
Chris Tate 8706ceafcf Merge pull request #866 from vercel-labs/ctate/check-index-guard-9174bc
Check the index guard in the integer byte-loop test
2026-10-11 15:17:22 -05:00
Malte Ubl fa991511e1 Hint LLVM to inline the shared indexed-read helpers
Every safe a[i] read of one element type calls one lifted helper
(lowerSafeIndexRead): a dense bounds test, a state test and a load
behind cold runtime fallbacks, about twenty instructions. With many
callers LLVM kept the hot instances out of line, so each read paid a
call and converted an integer index to a double and back across it
(tsc-ts Checker.nodeLinks: two calls per paged lookup).

The helpers now carry inlinehint. tsc-ts self-check (callgrind,
single-threaded): -0.73 % instructions; binary +0.41 %.
2026-10-11 13:10:15 -07:00
Malte Ubl 994d9ea7f6 Dispatch literal integer switches without separate range tests
A literal integer switch over a number tested min <= x <= max with two
ordered compares before converting and round-tripping the discriminant.
The round trip alone already rejects every non-matching value once the
conversion is frozen: NaN and infinities compare unordered, a frozen
out-of-range conversion converts back to a different value, fractions
fail, and -0 selects case 0 like strict equality. Cases in [2^31, 2^32)
dispatch on i64 instead of fptoui to i32.

A discriminant already proven to be an exact integer (int32 slots,
x | 0, x >>> 0, counted-loop counters) now selects directly, narrowing
to i32 only when its proven range fits.

tsc-ts (callgrind, single-threaded): self-check about -0.8 % instructions,
mostly switch (node.kind) and token dispatch.
2026-10-11 13:10:11 -07:00
Malte Ubl 757c48c2f8 Anchor the canonical byte-loop check on the element-check blocks
f3cfa80a asserted that the canonical byte loop contains no `fptosi double`
at all, meaning no exact-index round trip. The fixture also coerces its
running sum with ToUint32, whose inline fast path legitimately uses
`fptosi double ... to i64`, so the test fails on main (aaccbb39) as well;
#865 merged before the shards that run it. Assert instead that no
`bytes.index` check block is emitted, which keeps the original intent
(no unproven-index path for a proven in-bounds induction index).
2026-10-11 13:10:06 -07:00
Chris Tate f43ef506db Update byte-loop emission test for exact index checks
(cherry picked from commit f3cfa80a8e)
2026-10-11 13:08:09 -07:00
Chris Tate 8b5cf78e19 Update constant-table emission test for exact index checks
(cherry picked from commit 9b80f3d4b4)
2026-10-11 13:08:08 -07:00
Malte Ubl 0f05bf368d Pre-fault only for large slices and heaps
The first version committed memory ahead for every program: the heap's
16 MiB top pad was faulted in as soon as the helper started, and every
slice past its first 256 KiB faulted the next chunk ahead. A small server
(runtime suite http-api) peaked at 20.7 MiB instead of 4.4 MiB.

- The helper now only watches the break until the heap has grown by
  32 MiB; only then does it set the top pad and fault growth in ahead.
  Smaller heaps keep glibc's defaults, including its dynamic mmap
  threshold, which any top-pad mallopt fixes.
- A slice faults ahead only once it has carved 1 MiB, so lightly used
  classes commit nothing extra (and a program that never fills one does
  not start the helper at all).
2026-10-11 13:07:04 -07:00
Malte Ubl 2039d88e91 Record the preflight/order baseline of the charCodeAt loop fixture 2026-10-11 13:05:27 -07:00
Chris Tate 187ca8b998 Check the index guard in the integer byte-loop test 2026-10-11 15:05:18 -05:00
Malte Ubl 86c4edab54 Read charCodeAt at proven integer indices without a double round trip
String lengths and found indices are now int32 facts for the integer
range analysis: runtime strings hold at most SCR_STR_MAX_CAP UTF-8 bytes,
and a UTF-16 length never exceeds the UTF-8 byte length. Loops bounded
by a string length therefore get integer counters, and charCodeAt at the
counter counts as an integer consumer for counted-loop versioning.

A charCodeAt whose index the backend holds as an exact integer calls
@sc_str_char_code_at_int: on a proven-ASCII string one unsigned compare
against the length rejects negative and out-of-range indices, and every
other case defers to scr_str_char_code_at with the exact double.
2026-10-11 13:00:46 -07:00
Chris Tate aaccbb3908 Merge pull request #856 from vercel-labs/perf/array-layout-main
Move rarely used array storage out of line
2026-10-11 14:59:03 -05:00
Malte Ubl 767c33ebdb Give glibc's large blocks transparent huge pages
Blocks above the small-object classes (array storage, hash tables, link pages, large strings) come from glibc, whose brk heap and per-block mappings stay on 4 KiB pages where THP is in madvise mode; glibc advises its own memory only under the glibc.malloc.hugetlb tunable, which a program cannot set for itself. Once the brk heap passes 32 MiB, every large allocation now advises the heap grown since the previous one (and the aligned interior of big blocks mapped outside it), and glibc's top pad is raised to 32 MiB so heap growth no longer leaves each 2 MiB range straddling the heap end when it is first touched; the mmap and trim thresholds are pinned to their adaptive maxima, which setting the pad would otherwise freeze at their defaults. Linux and glibc only.
2026-10-11 12:58:50 -07:00
Chris Tate c2ee2b8cf1 Merge pull request #861 from vercel-labs/perf/gc-trace-null-skip-main
Skip null slots in emitted trace functions
2026-10-11 14:57:10 -05:00
Chris Tate 1f0e66566d Merge main into perf/array-layout-main 2026-10-11 14:55:17 -05:00
Chris Tate 6eef5b7e5a Merge main into perf/gc-trace-null-skip-main 2026-10-11 14:54:07 -05:00
Chris Tate 976342876c Merge pull request #865 from vercel-labs/ctate/update-constant-table-dd172e
Update constant-table emission test for exact index checks
2026-10-11 14:52:54 -05:00
Malte Ubl 61f3bac6d4 Add the @scriptc/types README 2026-10-11 12:52:33 -07:00
Malte Ubl 7cdbfc26a9 Format local-array-reads and record the preflight baseline for corpus 4671 2026-10-11 12:49:26 -07:00
Chris Tate f3cfa80a8e Update byte-loop emission test for exact index checks 2026-10-11 14:48:03 -05:00
Chris Tate 9b80f3d4b4 Update constant-table emission test for exact index checks 2026-10-11 14:45:30 -05:00
Chris Tate 988e78c323 Merge pull request #855 from vercel-labs/perf/heap-layout-main
Shrink cycle-capable objects: typed rest spreads, a 16-byte collector header, leaf capture boxes
2026-10-11 14:42:35 -05:00
Malte Ubl 2278456bbd Pre-fault allocator memory on a helper thread (Linux)
First touch of fresh memory costs the allocating thread a page fault and
a page clear. Linux executables now hand that to a helper thread:

- Size-class slices raise their bump limits in steps (256 KiB on the
  small-page prefix, half a huge page after the switch), and each step
  asks the helper to fault in the next step ahead with
  MADV_POPULATE_WRITE, which commits zeroed pages without storing, so it
  cannot race with carving. A slice holds at most about one step of
  untouched committed memory; a slice's first chunk is not faulted ahead.
- glibc's heap gets a 16 MiB top pad and the helper faults in each brk
  extension as soon as it sees the break move (checked after every
  request, then polled every 1 ms while it keeps growing; an idle program
  does not wake it).

The fast paths are unchanged; pages the helper has not reached are still
faulted by the owner. The helper masks all signals. SCR_SA_PREFAULT=0
turns it off, SCR_SA_PREFAULT_HEAP=0 keeps glibc's heap defaults; kernels
without MADV_POPULATE_WRITE (< 5.14) fall back to the old policy, and
library archives keep it.

The slice-growth allocator check now carves past several huge-page steps
and checks every fresh block reads as zero (UBSan and TSan builds run
with the helper).
2026-10-11 12:38:24 -07:00
Malte Ubl 0825756407 Contract checks compile in the self-hosted compiler
Narrow IR nodes by kind instead of structural casts, and pass concrete arrays
instead of Iterable, so the self-hosting build of the compiler accepts the
contract checker and the may-throw explanations.
2026-10-11 12:38:23 -07:00