m-code-summary.md 3.8 KB

Turbo Modula-2 MCode Machine — Summary

Overview

MCode is the bytecode for Borland's Turbo Modula-2 (CP/M). It's a 16-bit word-addressed stack machine with a single stack shared by evaluation values and call frames. The spec here is an interpreter written in Modula-2 (Interpreter.mod's Run loop) that reads .MCD bytecode files, loads them, and executes them. In the original system, this dispatch loop was hand-written Z80 for speed.

Timeline of a program

Call(modName)              # Loader2.mod:313
  LoadWithDependencies     # load .MCD, read module desc, relocate addresses
  TranslateAddresses       # fix up internal/external references
  allocate module globals
  for each TOINIT module:  ProcCall(0,1); Interpreter.Run   # run its init

A .MCD file: header (fileSize, moduleStart, codeSize, nbDependencies, reserved) + code blob + dependency records (8-char name, version, location). One file can bundle several interlinked modules (chain via link).

Machine state

  • instructionPointer — current bytecode address
  • sp — word stack pointer (grows down; push = DEC(sp,2))
  • Local.framePointer — current frame base (0-relative word indexing)
  • Global.globalPointer — current module's global data base
  • old frame pointer / outer frame pointer — link for display/static chain

Data types on the stack

Size Stack representation
1 word CARDINAL, INTEGER, BOOLEAN, CHAR, address
2 words LONGINT (DPush), REAL (FPush)
4 words LONGREAL (QPush)

Multi-word values are little-endian: high word pushed first.

Calling convention

  • Enter n (0D4H): push new frame (old FP, outer FP), return-IP, reserve 255-n bytes of locals (Instruction.mod:89). Stack order from low to high: locals → return IP → outer FP → old FP → [caller data], so the frame pointer indexes params positively and locals negatively.
  • Calls: ProcCall pushes return address and jumps to the proc's code (resolved through a proc-address table stored just before the module base).
  • ProcLeave n (0E0x…): pops IP, FP, outer FP; discards n words; n ≥ 80H means the proc re-enters its outer module (sets globalPointer).

Addressing families

Mnemonic Meaning Base
Local FP-relative; params +n, locals −n Local.framePointer[n]
Global current module's data globalPointer[n]
Extern other module's data (mod#, var#), resolved via module table Global.Module(modNum)
Indirect (Stack) load via address popped from stack mem[ptr+n]
Array base address (stack) + index mem[base+idx]

Full opcode map is the CASE in Interpreter.mod:84; 00H–FFH covers: 0D0H-series real arith, 0C0H-series checked arith + long arith, 0E0H-series jumps/short-circuit (AndThen/OrElse), 0CDH switch tables, 0EBH-0FFH procedure calls, 40H extended opcodes (ALLOCATE/MOVE/FILL/BIOS/Transfer…), and 12H LONGREAL sub-dispatch.

Highlights

  • Switch tables (0CDH): low/high bounds + jump table; uses a +8000H offset for unsigned comparison; tables can double as call-tables (Push(returnAddr)).
  • Short-circuit evaluated AND THEN / OR ELSE via 0DEH/0DFH with jump offsets.
  • Stack doubles as the heap arena; reserve/reserve_string allocate locals/strings on it.
  • Not implemented in the spec interpreter: processes/coroutines (TRANSFER), ASM, IOTRANSFER, exception raise (0): these raise catchable exceptions instead.

Tooling in the repo

  • unassemble.c — disassembles any .MCD (or the original M2.COM); prints the mnemonic names + a jump/switch-aware listing.
  • MCode_disassembly/*.txt — full disassembly of the original system (KERNEL, COMPILER, EDITOR, SHELL, system libs).

This gives you a complete, executable description of Turbo Modula-2's bytecode — a clean reference if you want your compiler to emit (or a VM to run) it.