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