tutorial.md 11 KB

MC64 — tutorial

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.

1. What's here

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.

2. Building the VM and the tools

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

3. Your first image

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]

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

5. The opcode exercise: example.MC4

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

6. Interactive stdin: readtest.MC4

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

7. Cross-module calls: STAK.MC4 + DEP.MC4

mkdep 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).
  • The loader loads the dependencies first, into low 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.

8. A compiler-produced image, end to end

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.

9. trans8to64: 16-bit images forward

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

10. Anatomy of an image

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.

11. Going further

  • Read 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).
  • The demo generators in 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).
  • The compiler tutorials in 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.