A class instance keeps its own dynamic properties in a lazily created bag
(`%dynProperties`). Untyped reads went through the bag accessor that
creates and stores the bag on first use: selecting a method checks the bag
for an own property shadowing it, and the named and computed fallbacks,
`in`/own probes, Reflect.get, for-in and accessor reads all looked their key
up there. So a pure read stored into the object. On a published object the
frozen-object guard rejects that store with "Cannot assign to read only
property '%dynProperties'", where Node reads without error.
Read-only bag accessors now never store. The untyped
%dyn.class.readProperties is rebuilt from the settled bag dispatch without
its lazy stores, and typed read helpers return a prototype-backed bag
instead of storing it. Every read path uses the read-only accessor: key
tests treat an absent bag as having no own keys, and lookups that must see
Object.prototype read from a fresh, unstored empty object. Writes keep the
storing accessor.
Found with tsc-ts at --checkers 8 on vercel/eve: a checker thread read
`host.commonSourceDirectory()` through an untyped interface value whose
object is the published Program.
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.
`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.
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.
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.
`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.