Branch ast-stage-c. This supersedes the "size/complexity-triggered"
conclusion in docs/handoff-l3-option2.md.
t_lower3 byte-matches; suite 184/184 (with lower_ok t_lower3).FIXPOINT OK (stage2.ssa == stage3.ssa,
3,184,888 bytes).SymTab surface; no FORWARD; no SymTab edits.Not size/complexity. It is a deterministic codegen bug:
A record's array field is stored as an inline descriptor (
{ l <len>, 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).
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).
The general fix (option 1 below) was landed: QbeGen.ArrBodyItems
expands record/class array elements per element when the element
(recursively) has a nested-array field, with ArrData emitting the
matching per-element sub-descriptors, so every inline array field's
length word is in the static data.
To stay inside QbeGen.sessBuf (4 MiB), it is a hybrid: only
small record arrays (data ≤ 131072 bytes) are expanded per element;
large ones (e.g. AST.arena, 65536 NodeRec) stay a compact zero
blob, and their elements' inline lengths are set at run time by a
generated init loop (QbeGen.DeclRecTemplate + QbeGen.InitRecArray,
driven from Lower). Plain record arrays (no nested-array field) stay
a compact zero blob, so AST.arena does not balloon.
Fixed and covered by compiler/tests/t_arrrecfld.mod (small),
t_arrrecbig.mod (large), t_nestarr.mod (nested) and t_recarr.mod
— all run green, and docs/wip/arrrec-field-openarray.mod /
docs/wip/nested-array-field-bug.mod now compile and exit 0 / 42. No
open codegen case remains here.
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 and is fixed — that was a distinct bug from the ASTDUMP
one, resolved by the hybrid ArrBodyItems/InitRecArray fix.)