Every SCR_ELEM_REF array stored its element type's retain, release and trace entry points in its own header (24 bytes), so ScrArr was 80 bytes: an 80-byte block for arrays of acyclic elements and a 96-byte block for cycle-capable ones. The three pointers now live in one constant ScrArrOps descriptor per element type, which the compiler emits once per module (@sc_arrops.*) and the runtime keeps static, and the header holds a pointer to it plus a 4-byte `cyc` flag beside `elem` that the inline RC fast paths test. ScrArr is 64 bytes: arrays take 64- and 80-byte blocks.
scr_arr_new_ref(retain, release, trace, cap) becomes scr_arr_new_ops(ops, cap); scr_set_to_arr_ref takes the result's descriptor from its caller instead of building an array from the set's key adapters. The backend names the %ScrArr fields in SCR_ARR_FIELD. The layout change bumps the runtime ABI marker to scr_runtime_abi_v13.
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.
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.
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.
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.
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.
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).