summary_step3.4.md 3.1 KB

V3 step 3.4 — records + WITH (done 2026-09-20)

Suite 56/56 (23 run — incl. 3 trap-aborts — + 33 reject). LL(1)-clean (only the known-benign TypeBlock warning).

Layout: pointer fields (locked amendment)

The plan said array headers sit inline in records, but 3.1 shipped nested separate objects — one type would then have two layouts (silent corruption on cross-assigns). Locked instead: records are flat blobs with scalars/sets inline, array fields as 8-byte pointers to static descriptors, nested records inline, static declaration-order offsets. Whole-record assignment is deep (per-field copy, arrays via CopyArray), so no aliasing.

Grammar (M2.atg)

  • RecordType (+ RecField/RecIdents reusing the field machinery, so classes share offsets for free); class fields widened to full Type for the same reason.
  • . suffixes mixing freely with [...] (r.tags[i], m[i].x); 215 non-record, 216 unknown field.
  • KindField designators resolve through the WITH stacks (base + static offset, array fields auto-dereferenced like nested levels); field use outside WITH/methods is still 230.
  • WITH v [, ...] DO (215 on non-records): pushes fields (inner wins) + bases, pops both exactly; nested and field-designator (WITH r.org) forms work.
  • Assign accepts field targets and copies records deep (CopyRecord); whole-class assign is explicit 230; record/set = is 213 (added to EqCheck); partial composites flow as addresses in Fact (never scalar-loaded).

SymTab (additive)

TypeSize (inline footprint: arrays/pointers 8), lazy declaration-order FieldOffset (+ rank for declaration-order enumeration, invalidated on new fields), FieldOwner via a new scope→record link, FieldCount/FieldName; nominal-cycle depth guard (223 proper still deferred).

Backend (QbeGen)

DeclRec (statics pre-pass + flat items; reverse-chain emission walks declaration order via ranks), FieldAddr, WITH base stack, CopyRecord (scalars/sets inline, arrays deep, records recurse).

Bugs caught

  1. Reversed emission: the field chain builds prepend (reverse declaration) but offsets are declaration-order — first cut emitted backwards while addressing forwards (SIGSEGV via a NULL slot read as pointer). Fixed with declaration ranks.
  2. Inline-vs-pointer size split: TypeSize counted full array sizes inside records while layout stores pointers — offsets went wrong whenever an array field preceded others. TypeSize is now the inline footprint (arrays 8) everywhere.
  3. Silent partial-composite loads/stores (record/set/class through scalar paths) closed by construction; whole-class assign plugged with an explicit 230.

Tests

t_record → 160 (nesting, array fields, deep-copy independence via post-copy mutation, field-designator WITH), t_with → 114 (multi-designator shadowing, nested WITH); t_bad_field (216), t_bad_with (215), t_bad_recassign (210). Total 56.

Deferred: variant records (Wirth has none — correctly absent), WITH-field VAR actuals (needs calls, step 4), class lowering (vtable/THIS), HIGH/LEN, 223 cycle errors.