# 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 — root-caused (2026-10-19) The old note here said removing `ASTDUMP` from `M2.atg` broke the fixpoint and was a symptom of a "parser-size-sensitive" miscompile. That was a **misdiagnosis**. `ASTDUMP` dumped the whole AST to stdout; when the compiler was built from its own sources that dump *contained* the string literal `"Parsed correctly"` (the driver's success message), and the fixpoint's M2make gate was `grep -q Parsed` — so the gate passed even when the compiler session was reporting `Incorrect source`. Removing the dump removed the masking line, and the *pre-existing* session errors surfaced — not a size effect. The errors were real: `Lower.mod` called its mutually recursive walkers before their declarations (`invalid call`, codegen dropping the calls), plus the `FormalOk` open-array comparison bug. All fixed in `a3ef897`: `ASTDUMP` removed, the gate tightened to `grep -q 'Parsed correctly'`, Lower's walkers declared in `Lower.def`, `FormalOk` corrected. The gate now fails on any session error (verified: a data-order violation makes `M2` print `Incorrect source` and the gate fails), and the fixpoint is byte-identical across the four size-changing commits of 2026-10-19. (The separate record-array-field / open-array codegen issue is described above; the deferred general `ArrBodyItems` fix and `docs/wip/nested-array-field-bug.mod` remain open — that is a *distinct* bug from the `ASTDUMP` one.)