This tutorial walks you through the MC64 64-bit Modula-2 virtual machine: the toolchain, the assembled sample images, the image format and the two most satisfying facts about the machine — that a 5 KB text source hands you a running program, and that every demo image in this repository is hand-encoded byte by byte.
All commands below were verified against a freshly built toolchain. The
repository already ships built executables (mcint, mkdemo, …) and sample
images in MC4/, so you can either use those directly or rebuild from source
as shown in step 2.
| File/dir | Role |
|---|---|
mcint |
the interpreter launcher: mcint module.MC4 |
mkdemo |
writes boot.MC4, a one-procedure image that prints a string |
mkdtest |
writes example.MC4, a 24-instruction opcode exercise |
mkread |
writes readtest.MC4, an interactive stdin demo |
mkdep |
writes STAK.MC4 + DEP.MC4, a cross-module dependency demo |
trans8to64 |
translates classic 16-bit .MCD binaries to 64-bit .MC4 |
docs/mc64-spec.md |
the normative specification (§ references below) |
Prerequisites: GNU Modula-2 (gm2 -fiso), a POSIX shell, and the usual
cp/printf/xxd utilities.
Use a scratch build directory so the repository stays untouched:
mkdir -p /tmp/mc64-tut && cd /tmp/mc64-tut
cp -r /path/to/m-code-64/src/. .
Compile the ten library modules (deterministic order, spec §16.1):
for m in Memory Local Global Stack Instruction Extended Interpreter Loader2 \
Console FileIO; do
gm2 -fiso -c "$m.mod"
done
Link each tool. Only the program module goes to the link step — the
libraries are the .o files. Passing several .mod files at once makes gm2
emit one main per module and the link fails on duplicate main (§16.1).
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 mc64 MC64.mod \
Stack.o Memory.o Console.o Global.o Loader2.o Interpreter.o Extended.o \
Instruction.o Local.o FileIO.o
gm2 -fiso -o mkdemo mkdemo.mod FileIO.o Console.o
gm2 -fiso -o mkdtest mkdtest.mod FileIO.o Console.o
gm2 -fiso -o mkread mkread.mod FileIO.o Console.o
gm2 -fiso -o mkdep mkdep.mod FileIO.o Console.o
gm2 -fiso -o trans8to64 trans8to64.mod FileIO.o Console.o
All eight commands should link without diagnostics. (If you'd rather not build, skip to the next step and run the shipped binaries from the repository root — they are equivalent.)
mkdemo writes boot.MC4, a single-module image whose one procedure prints a
string through the write string host service:
./mkdemo && ls -l boot.MC4
./mcint boot.MC4
-rw-rw-r-- 1 ... 396 ... boot.MC4
Hello MC64!
Exit status is 0: the guest reached end_program (SYSTEM service 0). The
string is written by SYSTEM service 1 (write NUL-terminated string at
param, §9.3); the image pushes the buffer address, pushes service 1, calls
system (opcode 0C3H), then executes end_program (50H). In Modula-2,
13C and 12C in mkdemo.mod are octal literals — 0B (vertical tab) and
0A (line feed) — so the demo string really is "Hello MC64!" + VT + LF.
mc64 is a tiny wrapper that boots boot.MC4 and then prints [vm end]:
./mc64
Hello MC64!
[vm end]
mcint argument behaviour./mcint # no argument -> usage, exit 0
./mcint nope.MC4 # missing/bad file -> loader fatal, exit 1
usage: mcint module.MC4
loader: cannot open module file
mcint takes the image path as its first program argument (NextArg +
ReadString (ArgChan (), fname), then Loader2.Call). A missing or malformed
image aborts through the loader, exit 1.
example.MC4mkdtest writes a 1350-byte image that performs 24 instruction checks —
arithmetic, bit flips, the extended 0x40 sub-dispatch, quads and a few
addressing forms — and prints each one:
./mkdtest && ./mcint example.MC4
mc64 test vm :: opcode exercise
g after 10*2+1 = 2047
add 10+6 = 16
sub 20-7 = 13
mul 6*7 = 42
div 100/8 = 12
mod 100%8 = 4
bitand FF&0F = 15
bitor F0|0F = 255
bitxor FF^0F = 240
power2 2^6 = 64
uc_add (2^32-1)+2 = 1
uc_mul 1234567890*2 = 2469135780
uc_div 2^36/2^16 = 1048576
uc_mod (2^32+6) rem 16 = 6
long_negate F0F0F0F0 low = 252645136
field_mask (1<<8)-(1<<2) = 252
real_add 3.5+2.25 = 5
dup+add check = 5
swap check = 3
load_global_dw g = 2047
local_dw FP[-2] = 99
drop then 12 = 12
jp_fwd skip -> 7 = 7
copy_block -> abc
Highlights:
uc_* lines exercise the extended sub-dispatch 0x40 (§10.2) that
provides unsigned 64-bit (LONGCARD) arithmetic — uc_add, uc_mul,
uc_div, uc_mod — plus long_negate and build_field_mask.real_add pushes two REALs (0x8E/0x8F-family constants) and a real_add
opcode: 64-bit floats are first-class scalars.jp_fwd/jpfalse_back prove the 64-bit relative jumps; copy_block
(0x30) pops (size, src, dst) and memcpy's a whole string; the
store_indexed_byte line shows the byte-addressed CHAR path.readtest.MC4mkread writes an image that reads two lines from the host standard input
via SYSTEM service 2 and echoes each with a prefix (plus CRLF):
./mkread
printf 'First line\nSecond line\n' | ./mcint readtest.MC4
you said: First line
again: Second line
Service 2 (read line, §9.3) pulls characters up to (but not including) the
line mark, NUL-terminates the buffer at param, and pushes the byte count
(excluding the terminator) — empty line and immediate EOF push 0. Because the
buffer is NUL-terminated, the guest can bounce it straight back through
service 1. The two consecutive reads also prove the line mark gets consumed:
the second read starts fresh.
STAK.MC4 + DEP.MC4mkdep synthesises the two-module dependency-chain test:
./mkdep && ./mcint DEP.MC4
cross-module ok
(notice: no trailing newline this time — the toast comes straight from the
dependency's own write_string code). Exit status is 0.
What happens:
STAK.MC4 is a dependency-free sidecar with two procedures: an empty TOINIT
and a "write a host string" procedure.DEP.MC4 is the main image with depCount = 1 and the dependency name
STAK appended (8 NUL-padded bytes) at the image tail. Its TOINIT flag
(descriptor flags bit 2) is set, so the loader boots it through
procedureAddress(0,0).MTBL slots (extern
module 0 = first import), then the main module, then runs TOINIT of the
dependencies and of the main image. DEP calls EXTERN_CALL 0, proc1, which
switches GP := MTBL[0] (the callee's data window) and jumps through
STAK's procedure table.Run ./mcint DEP.MC4 from a directory that contains STAK.MC4 — the loader
searches for dependency images next to the requested one.
The whole point of MC64 is to run code emitted by a Modula-2 compiler. The
sibling m2compiler-V2 project's M2comp accepts a source file and writes a
<Module>.MC4 image, which this interpreter then boots:
cd /path/to/m2compiler-V2
./build.sh # the first time; reports "No errors detected."
cp tests/r_flow.mod /tmp/mc64-tut/ && cd /tmp/mc64-tut
/path/to/m2compiler-V2/M2comp r_flow.mod
/path/to/m-code-64/mcint RFlow.MC4
Parsing r_flow.mod
--- Symbol table ---
...
Parsed correctly (M2comp's driver, abridged: it also dumps the AST
and the -- Code RFlow -- backend listing)
69 (mcint: the program's ExitCode global)
M2comp compiles MODULE RFlow (a control-flow tour: IF/WHILE/REPEAT/LOOP/
FOR) into a 918-byte RFlow.MC4 image; mcint boots it and the epilogue that
the compiler embeds for its ExitCode : INTEGER global prints 69. Run the
compiler's own suite (./run_tests.sh inside V2) to see the same pipeline at
scale; its default interpreter path is ../m-code-64/mcint.
trans8to64: 16-bit images forwardClassic Turbo Modula-2 images were 16-bit .MCD files. trans8to64 converts
one into a 64-bit .MC4 image (usage below); it widens word operands 2→8
bytes, rebuilds the procedure table, and re-emits the "MC64" header with a
correct checksum:
./trans8to64 # usage, exit 1
./trans8to64 missing.MCD out.MC4 # bad input, exit 1
usage: trans8to64 in.MCD out.MC4
trans8to64: cannot read input
For a valid single-module input it prints trans8to64: in -> out : ok
(<N> bytes). The repository deliberately ships no classic .MCD sample, and
the tool documents its own limits in the trans8to64.mod header: it is
runnable only for dependency-free modules (extern calls and module-table
address tricks are not mapped), 0x8F REAL constants decode as LONGINT, and
16-bit limit_check is emitted as DC 00 (dead code). SESSION.MD5.
trans8to64 records a baseline hash of the translator so accidental rewrites
are caught.
Every .MC4 is a 64-byte header plus an image (descriptor + code + proc
table). Read the tail of boot.MC4:
xxd -s 360 /path/to/m-code-64/MC4/boot.MC4
00000168: 3001 0000 0000 0000 0800 0000 0000 0000 ................
00000178: 8c0e 4865 6c6c 6f20 4d43 3634 210b 0a00 ..Hello MC64!...
00000188: 8d01 c350 ...P
| bytes | file | image off | meaning |
|---|---|---|---|
30 01 … |
360 | 296 | procsAddr = 0x130 = 304 — procedure table at image offset 304 |
08 00 … |
368 | 304 | proc cell 0 = 8; procedureAddress(0) = 304 + 8 = 312 |
8c 0e |
376 | 312 | call_rel 14: jump 14 bytes ahead, into the string |
… "Hello MC64!" 0b 0a 00 |
378 | 314 | the string, VT (octal 13C), LF (octal 12C), NUL |
8d 01 c3 50 |
392 | 328 | load_imm_byte 1; system; end_program |
The first 64 bytes are the file header; bytes 0–3 are the magic "MC64". The
descriptor then occupies image offset 0 (file 64): name at +264, flags
(TOINIT = 4) at +292, varCount/depCount/pad at +293..295, procsAddr at +296.
Finally, the checksum is the plain 32-bit sum of the image bytes (file
64..end) excluding the four checksum bytes themselves at file 352..355
(image 288..291) — written by the generators after laying out the image (§8.3).
Boot an image and re-decode it in your head: it's 20 bytes of code driving the whole demo.
docs/mc64-spec.md — §8 (loader/image), §9.3 (the host interface),
§10 (extended/quad sub-dispatches), §11 (the full opcode table), §12
(switch/string instructions), §14 (MCode/16 migration) and §15 (a minimal
encoded procedure, step by step).src/mkd*.mod are small enough to read whole and are
the known-good .MC4 encoders — treat them as ground truth when emitting
images from a compiler back end.docs/session-summary.md documents every loader/interpreter milestone and
the tools-and-gotchas behind them (stack polarity, link rules, checksum,
dependency loading).m2compiler-V1/tutorial.md and
m2compiler-V2/tutorial.md cover producing images from Modula-2 source;
docs/porting-plan-m2sp.md sketches the largest target yet — Wirth's M2SP
compiler retargeted to emit MC4.