summary_m2comp_step4.md 5.5 KB

m2comp step 4 — MC64 tree-walking backend (tag: m2comp-step4)

The compiler is end-to-end: Modula-2 source → Coco/R frontend → SymTab checks → AST → MGen MC64 .MC4 images runnable under mcint, with execution tests. Follows the Blaise front/back split (frontend builds the tree; backend walks it) rather than m2c's single-pass emit, reusing m2c's proven lowering recipes and the mc64-spec.md contract.

What was built

  • src/MGen.def/.mod (~1300 lines, single module): EmitModule walks the checked AST and writes <Module>.MC4. Globals from slot 4 (0..3 are the print-helper temps), ENTER frames with ED/EC/EE calls, E0/E1 jumps with fixups, ExitCode print convention (copied EmitPrint), full image assembly (magic/descriptor/proc table/checksum).
    • Scalars: 32-bit ops in slots, signed comparisons for the whole int family (m2c parity, incl. the past-MAXINT edge), eager AND/OR/NOT, DIV/MOD (truncating synthesis), int→real widening, REAL (binary64) arithmetic/comparisons.
    • Control flow: IF/ELSIF (deepest-else nesting), WHILE, REPEAT, LOOP/EXIT, FOR with runtime BY incl. negative direction.
    • Procedures: value/VAR params (addresses in slots), functions, recursion, nested procedures with display access, module procedures, init bodies called once at startup (m2c parity).
    • Composites: slot-per-element arrays (multi-dim), records (declaration-order offsets), whole assign via copy_block, char arrays (literals materialized slot-wise), sets (masks/IN/=), pointers (^, NIL), open-array formals (unchecked, lo=0).
    • CONSTs inline (folded at walk: chained, hex, bool, char, real, string-text); no slots, no startup inits.
  • src/SymTab layout enrichment: NewSubB/NewArrayB/WrapArrayB with folded bounds, IndexBounds (CHAR 0..255, BOOLEAN 0..1, subranges; INTEGER unbounded → 230), ArrayLo/Hi/Len, TypeSlots/FieldOffset (cycle-guarded), const-value table with chained ConstFold (IMPORT AST, acyclic).
  • Grammar updates: bound folding (non-literal → 230, cascade-safe), aux (symbol kind) + tag (module qualifier) stamps on name/field nodes (scopes pop after parsing, so the backend resolves through its own scope stack), num stamp on nkProc, grammar-time 230s for value composite params and composite function returns (m2c parity).
  • src/compiler.frm driver hook: after a successful parse, MGen.EmitModule(AST.GetRoot()) runs before the listing, so backend 230s (via M2compP.SemError) still reach the .LST.
  • run_tests.sh: expect_run (ExitCode compare) and expect_run_none (silent, terminating), both with timeout guards and VM-rc checks so diverging programs fail fast instead of hanging the suite.

Tests — 41/41

4 accept + 15 reject + 6 AST dumps (steps 1–3 intact), 2 silent runs, 14 execution runs: arith 8, flow 69, bool 11, real 1111, proc 162 (fact), varpar 48, nested-display 213, array 39, record 15, set 12, string 111, ptr 111, module-init 83, const-chains 1102.

Bugs found (all fixed, all covered)

  • gm2 is multipass: PROCEDURE ... FORWARD; is rejected with a cryptic "too many errors in pass 3" (CR's -m flag comments forwards out for this reason; m2c has zero forwards). Deleted.
  • Op-code overlap ×2, fatal only to tree-walk dispatch: OpAdd = OpTimes made 2*3 emit ADD; then OpEq = OpAdd made = emit ADD (1=1 passed by accident). Mul codes → 10–14, relation codes → 20–27 (all uses symbolic).
  • 51H/70H store orders differ (addr-on-top vs value-on-top); m2c never emits 51H. Ours uses 70H everywhere.
  • Nested proc bodies emitted inline need jump-over (Jmp(endL)/DefLabel, m2c parity) — else fall-through executes them with the wrong frame.
  • VAR-param stores need the address under the value.
  • Module-qualified addresses must resolve via the export tag, not the module name as a variable.
  • CHAR consts never folded (Q → 230); multi-index chains emitted only the first index (wrong code, now looped).
  • Harness: | head masked VM hangs (rc was head's); silent divergences now fail via timeout + rc checks.

Non-bugs established by bisection

  • showcase.mod and the old ok_proc.mod DIVERGE by construction (Work restarts its own FOR 1..10; P(i,i) mutates its loop control 1→2→-1→…). Any correct implementation hangs; both stay check-only. ok_proc's body was fixed to v := v + 1 so it terminates and runs silent (constructs unchanged).
  • Negative-vs-zero D5 real comparisons misbehave identically under byte-identical m2c output — VM-domain quirk, avoided in tests, not a backend bug. [Correction (step 5.2): misattributed — it WAS a backend (codegen) bug in the recipe shared by both compilers: real < > <= >= emitted stack juggling with no D5, so a leftover operand's bits became the boolean. D5 itself was always correct. Fixed; see summary_m2comp_step5.2.md.]

Deviations from m2c (documented)

Slot-per-element composites (no byte packing, no 0D/1D ops); BY may be a runtime expression; open-array bounds unchecked; module inits run once at startup even when nested (m2c parity); NEW/DISPOSE unreachable (no grammar productions).

Next

Step 5 candidates: DEFINITION/IMPLEMENTATION separate compilation (multi-.MC4 + depCount loader path), opaque types, procedure types, WITH/CASE, LONGINT/LONGREAL quads, debugger listings. SymTab/AST/MGen APIs are ready (ByNum queries, export tables, pnum/typ on nodes).