# 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: ```sh 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): ```sh 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). ```sh 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: ```sh ./mkdemo && ls -l boot.MC4 ./mcint boot.MC4 ``` ```text -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]`: ```sh ./mc64 ``` ```text Hello MC64! [vm end] ``` ## 4. `mcint` argument behaviour ```sh ./mcint # no argument -> usage, exit 0 ./mcint nope.MC4 # missing/bad file -> loader fatal, exit 1 ``` ```text 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: ```sh ./mkdtest && ./mcint example.MC4 ``` ```text 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): ```sh ./mkread printf 'First line\nSecond line\n' | ./mcint readtest.MC4 ``` ```text 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: ```sh ./mkdep && ./mcint DEP.MC4 ``` ```text 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 `.MC4` image, which this interpreter then boots: ```sh 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 ``` ```text 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: ```sh ./trans8to64 # usage, exit 1 ./trans8to64 missing.MCD out.MC4 # bad input, exit 1 ``` ```text usage: trans8to64 in.MCD out.MC4 trans8to64: cannot read input ``` For a valid single-module input it prints `trans8to64: in -> out : ok ( 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`: ```sh xxd -s 360 /path/to/m-code-64/MC4/boot.MC4 ``` ```text 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.