session-summary.md 6.7 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.

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/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/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

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).
  • ./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.

Current task state

  • 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

  • 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.