|
|
@@ -0,0 +1,324 @@
|
|
|
+# 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
|
|
|
+`<Module>.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
|
|
|
+(<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`:
|
|
|
+
|
|
|
+```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.
|