summary_arrrec-field-fix.md 3.3 KB

Array-of-record inline length words — fix and run-time init (2026-10-08)

Branch ast-stage-c. Tags v3-fix-arrrecfield (code), v3-arrrecfield-complete (this summary).

The bug

A record's array field is stored inline as a descriptor ({ l <len>, data }), so its length word must be initialised wherever the record lives. For a standalone record RecItems does this, and for a fixed top-level array ArrData does. But in static array-of-record data the elements were emitted as one compact zero blob (z n*size), so each element's embedded length word stayed 0. Passing such a field as an open-array actual made the callee read length 0 and trap (docs/wip/arrrec-field-openarray.mod, rc 134; docs/summary_l3_selfhost_fix.md). V3-built compilers miscompiled this; Lower's own tables avoided it by using flat arrays.

The fix — two paths

1. Small record arrays: inline expansion (2ebf662). QbeGen.ArrBodyItems / ArrData expand record/class elements per element so each inline array field's length word is in the static data. The predicate was generalised from HasNestedArray (only ARRAY OF ARRAY) to HasArrayField (any array field, recursively).

2. Large record arrays: run-time init (0ee6029). Expanding a huge table would blow the 4 MiB image — AST.arena is ARRAY [0..262143] OF NodeRec (262144 records; even an interleaved l <len>, z <n> form needs ~2.6 MB of text). So arrays whose element data exceeds a budget stay compact and their length words are set by a generated init loop:

  • QbeGen.RecArrayExpanded(n, t) centralises the budget (n * TypeSize(elem) <= 131072).
  • QbeGen.RecArrayNeedsInit(n, t) — array of record/class that (recursively) has array fields and is too big to expand.
  • QbeGen.DeclRecTemplate(varName, rec) emits data $<mod>_<var>_tmpl = { <one record with counts set> } (plus any nested sub-descriptors).
  • QbeGen.InitRecArray(varName, arrT) emits the loop that copies the template over each element: element i is at base + 8 + i*stride.
  • Lower.EmitVarNames records module-level globals that need it (declaring the template alongside their data); Lower.EmitCountInits runs the loops in the unit's init — emitting an init even for a unit with no BEGIN body, since main calls every registered init.

Together the two paths cover all sizes; the latent 0-count bug is gone.

Gotchas

  • QBE temps are single-assignment, so the loop counter must live in a stack slot (Slot4/LoadW/StoreW); using a temp made the condition reload the initial 0 and loop forever.
  • Op3 takes a 5th isReal argument.
  • The loop must run before the unit's body, and an init must exist even when the unit has none.

Verification

  • docs/wip/arrrec-field-openarray.mod → exit 0 (was rc 134).
  • t_arrrecfld.mod (small, inline path) → 42; t_arrrecbig.mod (5000 records, run-time path) → 42.
  • suite 183/183; FIXPOINT OK (3,388,344 bytes) in ~7.6 s — the compiler's own AST.arena is now initialised by the generated loop at each startup (cheap: one memcpy-per-element loop).

Commits

0ee6029 fix: run-time init of large array-of-record inline length words
2ebf662 fix: init inline array-field length in array-of-record static data