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).
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:
z, emitting only l <len> 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.sessBuf (band-aid; image keeps growing).Also documented: docs/wip/nested-array-field-bug.mod (nested-array
record field) is a related, still-open case.
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.)