# Array-of-record inline length words — fix and run-time init (2026-10-08) Branch **`ast-stage-c`**. Tags **`v3-fix-arrrecfield`** (code), **`v3-arrrecfield-complete`** (this summary). ## The bug A record's array field is stored **inline** as a descriptor (`{ l , data }`), so its length word must be initialised wherever the record lives. For a **standalone** record `RecItems` does this, and for a **fixed** top-level array `ArrData` does. But in **static array-of-record** data the elements were emitted as one compact zero blob (`z n*size`), so each element's embedded length word stayed 0. Passing such a field as an **open-array actual** made the callee read length 0 and trap (`docs/wip/arrrec-field-openarray.mod`, rc 134; `docs/summary_l3_selfhost_fix.md`). V3-built compilers miscompiled this; Lower's own tables avoided it by using flat arrays. ## The fix — two paths **1. Small record arrays: inline expansion** (`2ebf662`). `QbeGen.ArrBodyItems` / `ArrData` expand record/class elements per element so each inline array field's length word is in the static data. The predicate was generalised from `HasNestedArray` (only `ARRAY OF ARRAY`) to **`HasArrayField`** (any array field, recursively). **2. Large record arrays: run-time init** (`0ee6029`). Expanding a huge table would blow the 4 MiB image — `AST.arena` is `ARRAY [0..262143] OF NodeRec` (262144 records; even an interleaved `l , z ` form needs ~2.6 MB of text). So arrays whose element data exceeds a budget stay compact and their length words are set by a generated init loop: - `QbeGen.RecArrayExpanded(n, t)` centralises the budget (`n * TypeSize(elem) <= 131072`). - `QbeGen.RecArrayNeedsInit(n, t)` — array of record/class that (recursively) has array fields and is too big to expand. - `QbeGen.DeclRecTemplate(varName, rec)` emits `data $__tmpl = { }` (plus any nested sub-descriptors). - `QbeGen.InitRecArray(varName, arrT)` emits the loop that copies the template over each element: element `i` is at `base + 8 + i*stride`. - `Lower.EmitVarNames` records module-level globals that need it (declaring the template alongside their data); `Lower.EmitCountInits` runs the loops in the unit's init — **emitting an init even for a unit with no BEGIN body**, since `main` calls every registered init. Together the two paths cover all sizes; the latent 0-count bug is gone. ## Gotchas - **QBE temps are single-assignment**, so the loop counter must live in a **stack slot** (`Slot4`/`LoadW`/`StoreW`); using a temp made the condition reload the initial `0` and loop forever. - `Op3` takes a 5th `isReal` argument. - The loop must run **before** the unit's body, and an init must exist even when the unit has none. ## Verification - `docs/wip/arrrec-field-openarray.mod` → exit 0 (was rc 134). - `t_arrrecfld.mod` (small, inline path) → 42; `t_arrrecbig.mod` (5000 records, run-time path) → 42. - **suite 183/183**; **FIXPOINT OK (3,388,344 bytes)** in ~7.6 s — the compiler's own `AST.arena` is now initialised by the generated loop at each startup (cheap: one `memcpy`-per-element loop). ## Commits ``` 0ee6029 fix: run-time init of large array-of-record inline length words 2ebf662 fix: init inline array-field length in array-of-record static data ```