`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.
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 %.
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.
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).
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).
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.
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.
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).
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.
- NoThrow diagnostics name compiler-resolved Node errors (class and message)
and explain parameters the frontend compiles as T | undefined because a
caller passes a possibly missing value (for example xs[i]!), naming that
call.
- Synthetic dynamic-class helpers (%dyn.*) carry the location of the code
that first needed them; they are no longer bound to annotations there.
- Int32 hints only parenthesize where needed; the NoThrow hint no longer
suggests try/catch, which does not discharge the contract.
- Diagnostics snapshots and TypeScript preflight/order baselines for the new
fixtures.
- Docs: /docs/contracts and a limitations entry.
Optional array reads also match arrays whose element is itself the read T | undefined nullable-pointer union; owned copies retain and release through its NULL-tolerant entry points. A local array read borrows when its array operand is rooted at a stable parameter and its own live range preserves heap edges, and a call-argument read borrows when the call preserves them. An optional chain on such a read borrows the inline element instead of taking a second reference.