session-summary.md 9.6 KB

Modula-2 VM/Interpreter — session summary

Goal: reproduce, in GNU Modula-2 (gm2 -fiso) running on real hardware, the classic Turbo Modula-2 Z80 interpreter as a 64-bit MC64 machine, driven by mc64-spec.md. Constraints honored so far: no analysis/disassembly of .MCD binaries — work from .mod/.def sources and the spec; module library compiled separately with -c.

Companion effort: m2c/ — a single-pass Modula-2 compiler (Coco/R M2c.atg

  • hand-written SymTab, MGen) targeting MC64 .MC4 images runnable under mcint. Currently at step 11 (separate compilation units), 157/157 tests green (see summary_step1.md … summary_step11.md).

Project layout

  • M2compiler/mc64/ — all VM code: Memory, Local, Stack, Console, FileIO, Global, Instruction, Extended, Interpreter, Loader2 (.def + .mod); programs MC64.mod (REPL harness), mkdemo.mod (boot.MC4), mcint.mod (mcint file.MC4 launcher), mkdtest.mod (example.MC4 generator); built: mkdemo, mc64, mcint, mkdtest, boot.MC4, example.MC4.
  • M2compiler/m2c/ — the compiler: M2c.atg (grammar + attributes), compiler.frm (true source of generated M2c.mod: driver loop + Msg table via -->Errors), SymTab (checking), MGen (MC64 emitter), M2cS/M2cP (CR-generated scanner/parser — never edit), M2c (binary), run_tests.sh, tests/ (.mod + .def + .LST), per-test .MC4 in m2c/ root, Showcase*.mod tours.
  • M2compiler/mc64-spec.md — normative spec (§5.2 frames, §7.1 descriptor layout, §8.3 checksum rule, §9.3 SYSTEM ABI, §10 0x12/0x40 sub-dispatches, §11 opcode table, §12 switch/strings, §16 GNU-M2 ISO implementation notes).
  • M2compiler/m-code-summary-64.md, m-code-summary.md — opcode summaries.
  • M2compiler/summary_stepN.md — per-compiler-step log (commit + tag per step).
  • M2compiler/Resources/Turbo-reloaded/Reversing-Turbo-Modula2-main/MCode_specification/ — reference Turbo M2 sources (used only as Modula-2 sources, never binary analysis).
  • gm2 ISO libs at .../gcc/x86_64-pc-linux-gnu/16.0.1/m2/m2iso/; FIO at m2/m2pim/.

Build recipes

Compiler (m2c/, via ./build.sh, needs CR at ../../CocoGm2/CR): regenerates M2cS/M2cP/M2c.mod from M2c.atg (+ Msg from M2c.err via compiler.frm), recompiles, relinks ./M2c. NEVER edit M2c.mod (directly: it is overwritten), M2cS.*, M2cP.* — edit M2c.atg, compiler.frm, SymTab.*, MGen.* instead. Single-file: ./M2c tests/foo.mod (writes tests/foo.LST, FOO.MC4). Multi-file session (step 11): ./M2c lib.def impl.mod prog.mod (definitions, then implementations, then exactly one program; one <Prog>.MC4, one .LST per input, fail-fast). Test: bash run_tests.sh (needs ../mc64/mcint). Libraries are pre-compiled and linked as .o (only the program .mod is passed to the link step — multiple .mod files would collide on main):

for m in Memory Local Global Stack Instruction Extended Interpreter Loader2 Console FileIO; do
  gm2 -fiso -c $m.mod; done
gm2 -fiso -o mkdemo mkdemo.mod FileIO.o Console.o
gm2 -fiso -o mc64   MC64.mod   Stack.o Memory.o Console.o ...   # library set only
gm2 -fiso -o mcint  mcint.mod Loader2.o Interpreter.o Extended.o Instruction.o \
        Stack.o Global.o Local.o Memory.o Console.o FileIO.o
gm2 -fiso -o mkdtest mkdtest.mod FileIO.o Console.o

Verification status (all green)

  • ./mkdemo -> boot.MC4 ; ./mc64 -> Hello MC64! + [vm end] (0) ; ./mcint boot.MC4 -> Hello MC64! (0) ; ./mcint -> usage (0) ; ./mcint nope.MC4 -> Fatal (1).
  • m2c: 157/157 (bash run_tests.sh): steps 1–10 incl. step-10.1 init/proc-slot fix (regression b_mix -> 110, Showcase3 -> 168); step 11 adds 7 multi-file run tests (d_basic 25, d_from 12, d_multi 82, d_init 510, d_func 55, d_type 1122, d_case 25)
    • 6 single-error rejections (201/231/200). Tags step1 … step11 (+ step2.1, step10.1 fixes) all point at their commits.
  • Showcases (hand-computed tours): 157, 83, 168, 225 — all print their value under mcint.
  • ./mkdtest -> example.MC4 (1350 B, MC64 format) ; ./mcint example.MC4 — 24/24 opcode-checks pass (exit 0): loop g:=2g+1 x10 = 2047; add/sub/mul/div/mod; bit_and/or/xor & power2; extended 0x40: drop, uc_add, uc_mul, uc_div, uc_mod, long_negate, build_field_mask; load_imm_word; real_add 3.5+2.25 -> 5 (real_to_long); dup/swap; copy_block -> "abc"; global/local_dw load/store; load_global_dw; forward jp_fwd; jpfalse_back loops; proc_call into decimal print_num (enter/fct_leave, load_param, reserve, udiv/umod, store_indexed_byte).
    • ./mkread -> readtest.MC4 (453 B) ; ./mcint readtest.MC4 — SYSTEM service 2 reads stdin lines: printf 'first line\nsecond line\n' | ./mcint readtest.MC4 echoes you said: first line then again: second line (0). Edge cases verified: EOF without trailing newline still echoes; empty line -> count 0; immediate EOF -> 0; two consecutive reads prove the line mark is consumed (Skip) so the next read starts fresh.
  • ./mcint boot.MC4 -> Hello MC64! (0) ; ./mcint example.MC4 — 24/24 (0).
  • Public-dollar checks: :: VM accepts THE-SPEC–conformant images only.

Key technical facts learned

  • AND/OR are BOOLEAN-only under -fiso; integer bitwise ops run through BitOp(a,b,mode) (64 iterations of DIV/MOD) in Interpreter.mod. Masks use MOD/DIV (e.g. opc MOD 16, (fl DIV 4) MOD 2), never integer AND.
  • A stray integer AND in a program printed a misleading “is not a boolean expression” diagnostic pointing at a VAR declaration.
  • Standalone Pop(); (ignored return value) is a compile error.
  • Variant records need BOOLEAN tags (CASE : BOOLEAN OF | TRUE: | FALSE:); FROM m IMPORT x AS y aliases not supported; set constructors inline can ICE gm2 (assign to a VAR first); HALT works.
  • argv works via TextIO.ReadString(ArgChan(), s) after NextArg (one arg per call, program name excluded; a trailing empty argument means “no argument”).
  • FIO (m2pim) links fine under -fiso: OpenToRead, ReadNBytes(f,n,ADR(buf)), IsNoError, Close. Loader2.mod reads images with it.
  • Linking: only the program .mod goes to the link step; libraries are .o files.
  • On-disk image: file bytes 0..63 header, magic "MC64"; image starts at file 64; descriptor image offsets: DescDeps=0, DescLink=256, DescName=264, DescLoadAddr=280, DescChecksum=288 (u32), DescFlags=292, DescVarCount=293, DescDepCount=294, DescPad=295, DescProcs=296, DescVarSizes=304.
  • Checksum = 32-bit sum of file bytes 64..(size-1) excluding file 352..355 (the checksum field itself).
  • LoadImage(VAR data; off; dst; n) gained an offset (off=64) since images start at file offset 64; 32-bit fields read with ReadLong, not 8-byte ReadWord/ReadSlot.
  • Loader: image copied to ArenaBase=1048576; dataWin = desc+imgLen (8-aligned); GP = dataWin; procAddr(k) = procsAddr + k*8 + cell[k] (cell relative to its slot; negative when the target precedes the table); TOINIT = flags bit 2 ⇒ SetIP(ProcedureAddress(0,0)); Run(); Run returns on opcode 0x50.
  • Stack machine stack grows downward; Push = SetSP(SP-8) then write slot; params land at callee FP[3..7] (param 1 = the value on top at the call).
  • store_indexed_byte (0x1D) pops (value, index, ptr) — push (addr, 0, val); copy_block (0x30) pops (size, src, dst) — push dst, src, size; build_field_mask pops high/low — push low first, then high.
  • Proc table must not overlap code (put it after the code).
  • SYSTEM service 2 (read line) implemented via ISO StdChans.StdInChan + IOChan.Look/Skip char loop (avoids TextRead's "current line" blocking); stores chars up to (not incl.) line mark or EOF, NUL-terminates, pushes byte count. Documented in spec §9.3; readtest.MC4 exercises it.
  • m2c gotchas: M2c.mod/M2cS/M2cP are REGENERATED by CR every build — edit M2c.atg, compiler.frm, SymTab, MGen only. Coco/R numbers tokens by first occurrence: new keywords go in end-of-file productions and M2c.mod's Msg re-mirrors M2c.err automatically (verify names after grammar edits). gm2 HALT aborts (SIGABRT) — use loop EXIT for clean failures. Proc/init table slots share one pool (SymTab.AllocInitNum; step-10.1 fixed init stealing a later proc's slot). Never compare against a negative literal inside an & chain in .atg actions (gm2 16.0.1 ICE in generated M2cP.mod; nest IFs).

Current task state

  • m2c step 11 — separate compilation units: DONE (d3856a1, tag step11) — DEFINITION/IMPLEMENTATION/IMPORT across files into one .MC4 (157/157 tests, Showcase4 -> 225).
  • Writing an MC4 example file to exercise the interpreter: DONE — mkdtest.mod -> example.MC4, 24 opcode-checks pass under ./mcint.
  • stdin support for SYSTEM service 2: DONE — implemented in Interpreter.mod (ReadLine), mc64-spec.md §9.3 updated, mkread.mod -> readtest.MC4 proves line reads + line-mark consumption under piped input.

Next steps / open ideas

  • m2c hardening (deferred): M.x as VAR actual (still 233), WITH-field/CHAR tails as VAR actuals, open-to-open, multi-.MC4 images with depCount loading, circular imports, opaque types, LONGINT/LONGREAL quads, procedure types, cones.
  • 16→64 translation pass so classic 16-bit Z80 .MCD binaries can run on mcint (widening MCode operands 2→8 bytes and descriptor fields); currently unimplemented.
  • Hardware traps 0x45 (button/joystick/audio — currently no-ops), 0x08 output with cursor positioning, more .mod demos (blocks/frames/0x40-calls), cross-check vs the Turbo-reloaded reference interpreter.