Integer induction counters live in unsigned 64-bit slots, so every read
as a number was a uitofp i64 (a 6-7 instruction sequence on x86-64) and
every nested start an fptoui. The runtime's push path also round-tripped
the array length through a double for its bound check.
The values are at most 2^32 - 1: on 64-bit targets convert counters with
sitofp and loop starts with fptosi, convert array lengths and validated
indices through int64_t in scr_array.c, and test the push bound on the
integer length. 32-bit targets keep the unsigned conversions.
tsc-ts Excalidraw: uitofp i64 sequences 32.5 M -> 4.0 M, -0.66%
instructions on top of the throw-path change.
With runtime bitcode import, scr_throw_obj, scr_error_new and the other
throw entry points were inlined into the error paths of small compiled
functions. The inlined bodies needed callee-saved registers that every
call then saved and restored, and they made the helpers too large to
inline into their callers (1047 inlined throw bodies in tsc-ts).
Mark the 25 functions that only run while raising a JS exception
cold and noinline. The LLVM helper never imports noinline bodies, so
the throw paths stay calls. tsc-ts Excalidraw: -0.44% instructions.
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.
Program-lifetime instances were created with rc == SIZE_MAX, which every runtime contract reads as "published": the frozen-object guards throw on stores, postMessage and workerData share the object by pointer instead of cloning it, and the publish walker stops there. So programs that publish could not use ProgramLifetime (SC7007 refused it), and an unpublished instance posted to a worker would have been shared instead of cloned.
They are now created with the midpoint count 2^63 (2^31 on 32-bit targets). Only the class's empty retain and release helpers count references on these types, so nothing ever changes or frees that count; the midpoint keeps even an unpaired generic retain or release away from 0 and SIZE_MAX. Until the program publishes an instance it is an ordinary mutable, thread-local object to every runtime contract: stores succeed, postMessage clones it. Publication marks it SIZE_MAX like any object it reaches (the visited mark, logged and restored when a refusal undoes the publish), after which the frozen guards throw Node's TypeErrors and other threads read it by pointer. No header, extra word, runtime change or new instruction: the count itself is the published mark. The cycle analysis keeps the hierarchies untraced, so the collector never visits an instance, as for every untraced class.
SC7007 no longer refuses program-lifetime classes reachable from a publish() call; it still requires a root class in an executable. Without the marker nothing changes: tsc-ts best-v7 built with and without this change is byte-identical.
Tests: corpus 4634-contract-program-lifetime-publish (Node vs native): program-lifetime hierarchies with parent cycles, maps and sets of them, maps keyed by them and mortal scopes around them, published on the main thread and a worker and read on the other; writes after publication throw on both threads; unpublished instances stay writable; a refused publish leaves visited instances writable; a later publish reaches published and fresh instances; an unpublished instance in workerData is cloned. With the old SIZE_MAX count the program dies at its first constructor store. classes.test.ts covers the count on both pointer widths, the walker and the frozen guard. The contract-program-lifetime-publish diagnostics fixture is removed (the program now compiles). Docs: contracts and limitations pages, @scriptc/types.
Program-lifetime instances are never freed, and neither is anything they hold, so the RC audit of --sanitize builds reported every one of them, and every string, array and map they reference, as live at exit (4633-contract-program-lifetime failed the sanitized differential lane: 306 strings, 15 arrays, 338 objects). Like a program that abandons fibers, a program that keeps program-lifetime objects now skips the leak report with "scriptc RC audit skipped: N program-lifetime object(s) never freed", which the harnesses already filter.
Sanitized constructors call scr_obj_lifetime_note(obj), which counts the instance and registers it with LeakSanitizer as an intentional root, as publication does for published objects: once the audit no longer exits early, LeakSanitizer would otherwise report the instances the program drops. Plain builds emit nothing.
`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.
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.
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).
`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.
`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.
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.