# L3 option 2 — self-host root cause and fix (2026-10-05) Branch **`ast-stage-c`**. This supersedes the "size/complexity-triggered" conclusion in `docs/handoff-l3-option2.md`. ## Result - **`t_lower3` byte-matches**; suite **184/184** (with `lower_ok t_lower3`). - **Self-host green: `FIXPOINT OK`** (`stage2.ssa == stage3.ssa`, 3,184,888 bytes). - Zero `SymTab` surface; no `FORWARD`; no `SymTab` edits. ## Root cause of the self-host SIGABRT Not size/complexity. It is a **deterministic** codegen bug: > A record's array field is stored as an inline descriptor > (`{ l , data }`), but in **static array-of-record data** the > record elements are emitted as a single zero blob (`z n*size`), so the > embedded **length word is never initialized** (stays 0). Passing such > a field as an **open-array actual** makes the callee read length 0 and > trap (`call $abort`). Bisect evidence: `m2s2` aborts in `Lower.CopyN` ← `NoteProc` ← `ProcHeading` while parsing `Lower.mod`. Minimal repro `docs/wip/arrrec-field-openarray.mod` (`procs[i].name` → `VAR dst: ARRAY OF CHAR`): gm2-built compilers work, V3-built ones abort (rc 134). Shrinking `MaxSym`/`MaxProc` did not help because the bug is not size-related. Option 2's `procs : ARRAY OF ProcEnt` table hit exactly this pattern (`procs[i].name`, `procs[i].pname` passed to `CopyN`/`CopySpan`). ## Fix landed **Flatten Lower's procedure table** into parallel flat arrays — no `ARRAY OF RECORD` at all: - `procName` (stride 64), `procUid`, `procRes`, `procDep`, `procExt`, `procNpar`, and `parTy`/`parVar`/`parName` (strides `MaxPar`, `MaxPar*64`). - `NoteProc`/`NoteParam` write via `CopySpan` into the flat arrays; `FindProc`/`ProcInfo`/`ParamInfo` read them. Flat top-level arrays are descriptor-backed with a correct length word, so no record field is ever passed as an open array. This matches the handoff's "no arrays of records in Lower" rule (which the earlier design did not actually satisfy). ## Deferred: general codegen fix A correct general fix was implemented and validated on the repro: `QbeGen.ArrBodyItems` expands record/class elements per element (reusing a record-field walk, with `ArrData` emitting per-element nested sub-descriptors via a mirrored `RecStatics`). It fixes the repro (rc 0) **but inflates the compiler image past `QbeGen.sessBuf` (4 MiB)** — `AST.arena` is `ARRAY [0..65535] OF NodeRec` and its data line grows from ~40 bytes to ~2 MB. The whole image exceeds 4 MiB and is silently truncated (`call ` at EOF → qbe error). Options for the follow-up: 1. Emit record-array elements in a **compact interleaved form**: merge adjacent zero runs into `z`, emitting only `l ` at each array field (e.g. `NodeRec` → `l 8, z 48` per element, ~590 KB total, which fits). Needs field-size arithmetic; must match `RecItems` layout. 2. Raise `sessBuf` (band-aid; image keeps growing). 3. Keep flattening at the source level (as Lower now does). Also documented: `docs/wip/nested-array-field-bug.mod` (nested-array record field) is a related, still-open case. ## Latent fragility (do not touch without care) Removing the pre-existing `ASTDUMP` in `M2.atg`'s root production makes the V3-built compiler emit **spurious** semantic errors in `Lower.mod` (`AST.NChild: invalid call`, etc.) and breaks the fixpoint. So the debug dump stays for now; it is a symptom of the remaining "parser-size-sensitive" V3 miscompile and should be revisited separately.