summary_l3_selfhost_fix.md 3.4 KB

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 <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).

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 <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.
  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.