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.
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.MCD launcher), mkdtest.mod (example.MCD generator);
built: mkdemo, mc64, mcint, mkdtest, boot.mc4, example.MCD.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)..../gcc/x86_64-pc-linux-gnu/16.0.1/m2/m2iso/; FIO at m2/m2pim/.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
./mkdemo -> boot.mc4 ; ./mc64 -> Hello MC64! + [vm end] (0) ;
./mcint boot.mc4 -> Hello MC64! (0) ; ./mcint -> usage (0) ;
./mcint nope.MCD -> Fatal (1)../mkdtest -> example.MCD (1350 B, MC64 format) ; ./mcint example.MCD — 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.MCD (453 B) ; ./mcint readtest.MCD — SYSTEM service 2
reads stdin lines: printf 'first line\nsecond line\n' | ./mcint readtest.MCD
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.MCD — 24/24 (0).:: VM accepts THE-SPEC–conformant images only.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.AND in a program printed a misleading “is not a boolean expression”
diagnostic pointing at a VAR declaration.Pop(); (ignored return value) is a compile error.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..mod goes to the link step; libraries are .o files."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.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.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.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.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.MCD exercises it.mkdtest.mod -> example.MCD, 24 opcode-checks pass under ./mcint.Interpreter.mod
(ReadLine), mc64-spec.md §9.3 updated, mkread.mod -> readtest.MCD proves
line reads + line-mark consumption under piped input..MCD binaries can run on mcint
(widening MCode operands 2→8 bytes and descriptor fields); currently unimplemented.0x08 output with
cursor positioning, more .mod demos (blocks/frames/0x40-calls), cross-check vs the
Turbo-reloaded reference interpreter.