Instruction-Set Specification, derived from the Turbo Modula-2 MCode bytecode
MC64 is a 64-bit generalization of the MCode virtual machine that Borland Turbo Modula-2 for CP/M executed on Z80. Where MCode/16 is a 16-bit word-addressed stack machine with all scalars 16 bits wide, MC64 keeps the stack slot (word) at 64 bits but preserves Modula-2's type ladder: CARDINAL and INTEGER are 32 bits, SET is 64 bits, LONGINT and LONGCARD are 64 bits, REAL is a 64-bit float and LONGREAL is a 128-bit quad.
The 256-entry opcode map is preserved 1:1. The scalar word-op families (0A0H–0BFH and the checked forms) now operate on the 32-bit types; the "long"/"dword" families (0C5H–0CCH) operate on the 64-bit LONGINT type. Unsigned 64-bit (LONGCARD) arithmetic is provided by the extended sub-dispatch (§10.2).
This document is the normative reference for a VM implementation (e.g. as a C port) and for a compiler code generator that emits MC64.
Memory layout convention (shared by the loader, not enforced by the ISA):
high address
+-----------------------------+ ^
| stack region | | grows DOWN toward globals/heap
| (SP moves toward lower adr)|
+-----------------------------+
| heap region | |
+-----------------------------+ | grows UP (ALLOCATE/MARK/RELEASE)
| module images + globals|
+-----------------------------+ v
| system tables |
low address
The evaluation stack and the heap share the middle; the loader places module images at low addresses and lets the stack come down from the top.
| Register | Meaning |
|---|---|
IP |
instruction pointer — byte address of next opcode |
SP |
stack pointer — byte address of the top word (lowest used) |
FP |
frame pointer — base of the current activation frame |
GP |
global pointer — base of the current module's data window |
OFP |
outer frame pointer — static chain / display link |
MTBL |
module table — MTBL[i] = data base of loaded module i |
The stack grows down: Push(w) = SP -= 8; *(u64*)SP = w.
| Modula-2 type | Width | Stack slots | Notes |
|---|---|---|---|
| BYTE, CHAR, BOOLEAN | 8 bits | 1 slot | held in the low bits of a slot |
| CARDINAL | 32 bits | 1 slot | unsigned arithmetic, zero-extended |
| INTEGER | 32 bits | 1 slot | two's complement, sign-extended |
| SET | 64 bits | 1 slot | bit set, elements 0..63 |
| LONGINT | 64 bits | 1 slot | two's complement signed arithmetic |
| LONGCARD | 64 bits | 1 slot | unsigned 64-bit arithmetic (new) |
| REAL | 64 bits | 1 slot | IEEE 754 binary64 (double) |
| LONGREAL | 128 bits | 2 slots | IEEE 754 binary128 (quad) |
| pointer/address | 64 bits | 1 slot | byte address |
Storage convention: stack slots are always 64 bits. 32-bit values live in the low 32 bits of their slot — CARDINAL is kept zero-extended, INTEGER is kept sign-extended — while SET and the 64-bit types occupy the full slot. Conversion between a 32-bit type and a 64-bit type is a value no-op. ALL loads and stores move a full slot; the width is a semantic property of the arithmetic and comparison opcodes, not of memory access.
Stack representation of a quad (little-endian):
memory: [ low word ... high word ] low word on top (at SP)
Push order for a quad constant or a quad load: high word first, low word last, so the low word ends up on top of the stack.
One stack holds evaluation data and activation frames.
byte offset from FP stack slot
FP - N*8 ........ first local word FP[-N]
FP - 8 ........ last (shallowest) local FP[-1]
FP + 0 ........ OPF (outer frame pointer) FP[0]
FP + 8 ........ OLD FP (dynamic link) FP[1]
FP + 16 ........ return IP FP[2]
FP + 24 ........ parameter 1 FP[3]
FP + 32 ........ parameter 2 FP[4]
...
FP[-1], FP[-2], … The last reserved word
(adjacent to the frame link) is offset -1.FP[k+2].FP[3]. ; evaluate actuals, push them (right to left)
PROCCALL n ; push return IP, jump to proc n (OFP set as needed)
ENTER k ; callee prologue (below)
...
LEAVE0 / PROC_LEAVE n ; callee epilogue (below)
ENTER k:
push OLD FP; push OFP ; FP := SP ; NewFrame
push IP ; procedure start address
Reserve( (255-k) * 8 ) ; locals, in bytes (k=0FFH => none)
PROC_LEAVE n (and the leave family):
SP := FP ; discard frame + locals
OFP := pop ; outer frame pointer
FP := pop ; OLD FP (restore caller FP)
IP := pop ; return address
drop n words ; discard parameter area (n is 7-bit; see below)
if (bit 7 of n) and (OFP ≠ NIL): GP := OFP ; re-enter outer module
Function returns:
FCT_LEAVE n: res := pop; PROC_LEAVE n; push resLONGREAL_FCT_LEAVE n: same with a quad (two words) preserved across the leave.The low 7 bits of n in leave opcodes count words to drop from the caller's
parameter area. Bit 7 is a flag: when set the procedure returns into its outer
(statically enclosing) module, so GP is reloaded from OFP.
| Family | Base | Word offset n from |
|---|---|---|
| Local | FP |
immediate (s8) or tiny code |
| Global | GP (current module window) |
immediate (u8) — GP[n] |
| Extern | MTBL[moduleNumber] |
mod and var operands |
| Indirect | a pointer popped from the stack | immediate (u8) — *(ptr + n) |
| Indexed | a pointer popped from stack + index popped from stack | *(ptr + index) |
Word offset n means byte offset n*8. Exceptions: the indexed byte ops (0DH/1DH)
address single bytes (CHAR/BYTE arrays, strings), and the indexed quad ops (0FH/1FH)
address two-slot LONGREAL elements.
The GP window is an array of at least 65536 words; modules loaded so far live in it.
MTBL is a system-wide array of 256 64-bit module data bases. EXTERN mod,var
operations read MTBL[mod][var].
ModuleDesc (offsets are image-relative; u64 fields little-endian):
0 dependencies : ARRAY[0..31] OF ADDRESS ; 256 bytes (module base pointers)
256 link : ADDRESS ; next module in chain
264 name : ARRAY[0..15] OF CHAR ; 16-char, NUL-padded
280 loadAddr : ADDRESS ; base of module data image
288 checksum : u32 (32-bit; see §8.3 for the coverage rule)
292 flags : u8 (bit0 OVERLAY, bit1 Z80/NATIVE, bit2 TOINIT, bit3 RECURSE)
293 varCount : u8 (number of global variable entries)
294 depCount : u8 (number of dependencies)
295 pad : u8 (reserved, zero)
296 procsAddr : ADDRESS ; location of the proc table
304 varSizes : varCount × ADDRESS (u64 global variable sizes in bytes)
The descriptor lives at image offset 0 (i.e. immediately after the 64-byte file
header). In a single-module file the whole image is a bare descriptor + code + data, so
moduleStart (file header, §8.1) is taken as 0.
Each module has a procedure table at procsAddr: N contiguous 64-bit relative
byte offsets, one per procedure.
procedureAddress(k) = procsAddr + k*8 + (i64) table[k]
The offset is relative to the end of the entry cell, so entry 0 can point directly
after its own cell. The table consecutive cells live just below (or beside) the module
data; procsAddr is fixed up by the loader.
EXTERN_CALL mod,proc:
OFP := GP ; remember current module
GP := MTBL[mod] ; switch to callee module
push IP
IP := procedureAddress(mod, proc)
EXTERN_CALL1 (address+proc number on the stack) does the same with
procsNum = pop; modBase = pop; GP = modBase.
A module file ("*.MC4") holds one or several linked modules plus their dependencies.
| Offset | Size | Field | Meaning |
|---|---|---|---|
| 0 | 8 | magic | "MC64\0\0\0\0" (constant 0x_4D_43_36_34) |
| 8 | 8 | fileSize | bytes of the image block (not header) |
| 16 | 8 | moduleStart | offset of the module descriptor within the image |
| 24 | 8 | depsOffset | offset of the dependency table within the image |
| 32 | 8 | nbDependencies | number of dependencies |
| 40 | 24 | reserved | zero |
| Size | Field | Meaning |
|---|---|---|
| 16 | name | module name, NUL-padded |
| 8 | version | import version (0 = any) |
| 8 | location | offset of the fixup that must point at the module data base after load |
LoadWithDependencies(modName, referencer, version):
open file; read header
origin := allocAddr; load image blob at origin
read dependency records (depsOffset + i*32)
walk the module chain starting at origin + moduleStart:
add origin to loadAddr, procsAddr, each non-NIL dependency pointer
set RECURSEFLAG
verify checksum of the requested (last) module
if not already loaded:
relocation pass: for each fixup pair (location,count) in the descriptor,
add origin to the target pointed by the (relocated) location
translate dependency fixups: moduleBase := loadModule(dep); *(location+origin) := moduleBase
init module globals (varCount × sizes), zero-filled
Checksum rule (as implemented): the module checksum is the plain 32-bit sum of the
image bytes (file offsets HeaderSize..fileSize-1) excluding the four checksum
bytes themselves (file 352..355 = image 288..291). For the reference loader the
image is placed at ArenaBase (1 MiB) with the descriptor at image offset 0, and the
module's data window (dataWin) is allocated right after the image, 8-byte aligned, and
zero-filled; dependencies, link and multi-module relocation are currently
unsupported (depCount > 0 is rejected).
After loading, the loader allocates varSize bytes for each entry, fills them with
zeros and records the allocated base in the module descriptor's var-size table slot.
Run:
loop
opcode := NextByte()
dispatch opcode
loop forever
NextByte reads one byte and advances IP. NextWord reads a 64-bit little-endian
word (8 bytes). NextSigned reads a signed 8-bit value.
| Exception | Raised by |
|---|---|
| IllegalInstruction | opcode 00 |
| Unimplemented | opcodes 01, 87, native-stub services |
| Overflow | checked arithmetic (0C0H–0C2H, 0D0H–0D1H) |
| RangeError | 0DAH, 0DBH, 0DCH, 0DDH |
| DivideByZero | DIV/MOD with zero divisor |
| StackOverflow | reserve crossing the stack/heap limit |
| OutOfMemory | ALLOCATE breathing the stack |
| StringTooLong | copy_string overflow |
| LoadError | loader (module not found / version conflict) |
A raising opcode aborts the current Run and unwinds to the nearest handler
(register via RAISE/handler services). If no handler exists, the VM terminates the
current module and reports the exception record (name, kind, IP), then returns to the
caller of Call. Concurrency/process support (TRANSFER, NEWPROCESS) is out of
scope for the baseline machine and raises Unimplemented.
SYSTEM)SYSTEM is the escape hatch to the host environment (successor to the CP/M BDOS):
id := pop ; service number
param := pop ; address (or scalar)
Baseline services (implementation provided by the VM):
| id | Service |
|---|---|
| 0 | EXIT (param = status ignored) |
| 1 | write NUL-terminated string at param |
| 2 | read line into buffer at param |
| 3+ | host-defined extensions |
Service 2 (read line) pulls characters from the host standard input up to
(but not including) the line mark, or until end of input. The bytes are stored at
param, a NUL terminator is appended, and the byte count read (excluding the
terminator) is pushed onto the operand stack. An empty line or immediate end of
input pushes 0. A guest therefore reads a line with:
load_imm_word buf; load_imm_byte 2; system; and may echo it straight back with
service 1 because the buffer is NUL-terminated.
Two opcodes delegate to a secondary byte:
0x12 — LONGREAL (quad) operations| sub | mnemonic | operands | effect |
|---|---|---|---|
| 00 | load_local_q | s8 n | push quad FP[n]..FP[n+1] (local) |
| 01 | load_global_q | u8 n | push quad GP[n]..GP[n+1] (global) |
| 02 | load_i_q | u8 n | pop p; push quad p[n]..p[n+1] (indirect) |
| 03 | load_extern_q | mod, var | push quad MTBL[mod][var..var+1] |
| 04 | store_local_q | s8 n | store quad of stack into local |
| 05 | store_global_q | u8 n | store quad into global |
| 06 | store_i_q | u8 n | pop p; store quad into p[n..] |
| 07 | store_extern_q | mod, var | store quad into external module |
| 08 | load_indexed_q | — | pop i (index), pop p; push quad p[i] |
| 09 | store_indexed_q | — | pop q (quad); pop i; pop p; store quad p[i] |
| 0A | quad_fct_leave | u8 n | q:=pop; proc_leave n; push q (quad fct leave) |
| others | illegal |
(0x40 0x12 is reserve_string, see extended table.)
0x40 — extended operations| sub | mnemonic | effect |
|---|---|---|
| 00 | drop | pop 1 word |
| 01 | enter_monitor | no-op (host hook for preemption) |
| 02 | leave_monitor | no-op |
| 03 | long_negate | pop LONGINT; push -LONGINT (64-bit signed negate) |
| 04 | build_field_mask | pop hi; pop lo; push (1<<hi) - (1<<lo) |
| 05 | ALLOCATE | pop size; pop p; alloc; *p := block |
| 06 | DEALLOCATE | pop size; pop p; free *p; *p := NIL |
| 07 | MARK | pop p; *p := heapMark |
| 08 | RELEASE | pop p (mark addr); release; *p := NIL |
| 09 | FREEMEM | push bytes free |
| 0A | TRANSFER | unimplemented (processes) |
| 0B | IOTRANSFER | unimplemented |
| 0C | NEWPROCESS | unimplemented |
| 0D | BIOS | host call: fct:=pop; param:=pop; BIOS(fct,param) |
| 0E | MOVE | pop size; pop dst; pop src; memcpy(dst,src,size) |
| 0F | FILL | pop value; pop size; pop addr; memset(addr,value,size) |
| 10 | INP | host I/O read port |
| 11 | OUT | host I/O write port |
| 12 | reserve_string | see §13 |
| 13 | assert | assertion: pop 0 → raise RangeError |
| 14 | uc_less | pop b; pop a; push (a < b) (LONGCARD) |
| 15 | uc_less_eq | pop b; pop a; push (a <= b) (LONGCARD) |
| 16 | uc_greater | pop b; pop a; push (a > b) (LONGCARD) |
| 17 | uc_greater_eq | pop b; pop a; push (a >= b) (LONGCARD) |
| 18 | uc_add | pop b; pop a; push (a + b) mod 2^64 (LONGCARD) |
| 19 | uc_sub | pop b; pop a; push (a - b) mod 2^64 (LONGCARD) |
| 1A | uc_mul | pop b; pop a; push (a * b) mod 2^64 (LONGCARD) |
| 1B | uc_div | pop b; pop a; push (a DIV b) (raise DivideByZero) |
| 1C | uc_mod | pop b; pop a; push (a MOD b) (raise DivideByZero) |
| 1D | uc_to_real | pop LONGCARD; push REAL (rounded; optional) |
| 1E | real_to_uc | pop REAL; push LONGCARD (truncated; optional) |
| others | illegal |
Notation: stack effects are written bottom…top → result. pop reads the top word.
u8/i8/u64/i64 are operand encodings. Multi-word quads are noted as q.
| Hex | Mnemonic | Operand | Effect |
|---|---|---|---|
| 00 | reserved | — | raise IllegalInstruction |
| 01 | RAISE | — | unimplemented (see §9.2) |
| 02 | load_proc_addr | u8 n | push procedureAddress(current_mod, n) |
| 03–07 | load_param (1–5) | — | push FP[3..7] (param 1..5) |
| 08 | load_local_dw | i8 n | push slot FP[n] (legacy 64-bit slot / LONGINT) |
| 09 | load_global_dw | u8 n | push slot GP[n] (LONGINT/LONGCARD) |
| 0A | load_stack_dw | u8 n | pop p; push slot p[n] (64-bit) |
| 0B | load_extern_dw | mod,var | push slot MTBL[mod][var] (64-bit) |
| 0C | load_extern_w | nibble | push slot MTBL[m][n] (nibble m,n) |
| 0D | load_indexed_byte | — | pop i; pop p; push (u8)p[i] |
| 0E | load_indexed_w | — | pop i; pop p; push p[i*8] (one slot) |
| 0F | load_indexed_q | — | pop i; pop p; push quad p[i*8],p[i*8+1] (LONGREAL) |
| 10 | load_outer | — | push FP of enclosing frame (display +1) |
| 11 | load_outer_n | u8 n | push frame pointer n display-steps up |
| 12 | LONGREAL op | sub | secondary dispatch (§10.1) |
| 13–17 | store_param (1–5) | — | pop; FP[3..7] := value |
| 18 | store_local_dw | i8 n | pop; store slot into FP[n] |
| 19 | store_global_dw | u8 n | pop; store slot into GP[n] |
| 1A | store_stack_dw | u8 n | pop; pop p; store slot into p[n] |
| 1B | store_extern_dw | mod,var | pop; store slot into MTBL[mod][var] |
| 1C | store_extern_w | nibble | pop; store slot into MTBL[m][n] |
| 1D | store_indexed_byte | — | pop v; pop i; pop p; p[i] := v (byte) |
| 1E | store_indexed_w | — | pop v; pop i; pop p; p[i*8] := v |
| 1F | store_indexed_q | — | pop q; pop i; pop p; store quad at p[i*8] |
| Hex | Mnemonic | Operand | Effect |
|---|---|---|---|
| 20 | dup | — | push top |
| 21 | swap | — | exchange top two words |
| 22–2B | load_local_n | — | push FP[-(op & 0x0F)] (offsets +2..−11) |
| 2C | load_local | i8 n | push FP[n] (n<0 local, n>0 param) |
| 2D | load_global | u8 n | push GP[n] |
| 2E | load_stack | u8 n | pop p; push p[n] |
| 2F | load_extern | mod,var | push MTBL[mod][var] |
| 30 | copy_block | — | pop size; pop src; pop dst; memcpy |
| 31 | copy_string | — | pop srcSize; pop dstSize; pop src; pop dst; copy NUL-terminated |
| 32–3B | store_local_n | — | pop; FP[-(op & 0x0F)] := value (s2..−11) |
| 3C | store_local | i8 n | pop; FP[n] := value |
| 3D | store_global | u8 n | pop; GP[n] := value |
| 3E | store_stack | u8 n | pop; pop p; p[n] := value |
| 3F | store_extern | mod,var | pop; MTBL[mod][var] := value |
| Hex | Mnemonic | Effect |
|---|---|---|
| 40 | extended | secondary dispatch (§10.2) |
| 41 | load_stack_d0 | pop p; push slot p[0] (alias of 60H, retained) |
| 42–4F | load_global_n | push GP[op & 0x0F] (offsets 2–15) |
| 50 | end_program | return control to the loader/kernel |
| 51 | store_stack_d0 | pop v; pop p; store slot p[0] (alias of 70H) |
| 52–5F | store_global_n | pop; GP[op & 0x0F] := value (2–15) |
| Hex | Mnemonic | Effect |
|---|---|---|
| 60–6F | load_i_n | pop p; push p[op & 0x0F] (one slot) |
| 70–7F | store_i_n | pop v; pop p; p[op & 0x0F] := v (one slot) |
| Hex | Mnemonic | Operand | Effect |
|---|---|---|---|
| 80 | load_local_addr | i8 n | push byte addr FP + n*8 |
| 81 | load_global_addr | u8 n | push byte addr GP + n*8 |
| 82 | load_stack_addr | u8 n | pop p; push p + n*8 |
| 83 | load_extern_addr | mod,var | push byte addr MTBL[mod] + var*8 |
| 84 | proc_leave | u8 n | leave: drop n & 0x7F words; re-enter if bit7 |
| 85 | fct_leave | u8 n | res:=pop; proc_leave n; push res |
| 86 | longfct_leave | u8 n | q:=pop; proc_leave n; push q (quad) |
| 87 | asmcode | u8 n | native code block; unimplemented in baseline |
| 88–8B | leave (0..3) | — | leave, drop 0..3 words, bit7 set (outer return) |
| 8C | call_rel | u8 n | push IP; IP += n (pc-relative string ptr) |
| 8D | load_imm_byte | u8 | push zero-extended u8 |
| 8E | load_imm_word | u64 | push 64-bit immediate (8 bytes) |
| 8F | load_imm_quad | 16 bytes | push quad constant (low word on top) |
| 90–9F | load_imm 0–15 | — | push small constant op & 0x0F |
| Hex | Mnemonic | Effect |
|---|---|---|
| A0 | equal | pop b; push (pop = b) (full slot, any width) |
| A1 | not_equal | pop b; push (pop # b) |
| A2 | uless | pop b; push (pop < b) (CARDINAL, 32-bit) |
| A3 | ugreater | pop b; push (pop > b) (CARDINAL) |
| A4 | uless_eq | pop b; push (pop <= b) (CARDINAL) |
| A5 | ugreater_eq | pop b; push (pop >= b) (CARDINAL) |
| A6 | add | pop b; push (pop + b) (CARDINAL, mod 2^32) |
| A7 | sub | pop b; push (pop - b) (CARDINAL, mod 2^32) |
| A8 | umul | pop b; push (pop * b) (CARDINAL) |
| A9 | udiv | pop b; push (pop DIV b) (raise DivideByZero) |
| AA | umod | pop b; push (pop MOD b) (raise DivideByZero) |
| AB | eq0 | push (pop = 0) |
| AC | inc | push (pop + 1) (32-bit) |
| AD | dec | push (pop - 1) (32-bit) |
| AE | add_imm | u8 n; push (pop + n) (CARDINAL) |
| AF | sub_imm | u8 n; push (pop - n) (CARDINAL) |
| B0 | shl_imm | u8 n; push (pop << n) (CARDINAL, logical) |
| B1 | shr_imm | u8 n; push (pop >> n) (logical) |
| B2 | iless | pop b i32; push (pop < b) (INTEGER) |
| B3 | igreater | pop b i32; push (pop > b) (INTEGER) |
| B4 | iless_eq | pop b i32; push (pop <= b) (INTEGER) |
| B5 | igreater_eq | pop b i32; push (pop >= b) (INTEGER) |
| B6 | not | push (¬ bool(pop)) |
| B7 | complement | push (0xFFFFFFFF − pop) (32-bit bitwise NOT) |
| B8 | imul | pop b i32; push (pop * b) (INTEGER) |
| B9 | idiv | pop b i32; push (pop DIV b) (raise DivideByZero) |
| BA | long_to_card | pop LONGINT; push CARDINAL (low 32 bits, zero-extended) |
| BB | long_to_int | pop LONGINT; push INTEGER (low 32 bits, sign-extended) |
| BC | abs | pop i32; push |
| BD | int_to_long | pop INTEGER; push LONGINT (sign-extended) |
| BE | long_to_real | pop LONGINT; push REAL (rounded) |
| BF | real_to_long | pop REAL; push LONGINT (truncated) |
Notes: CARDINAL-LONGCARD and LONGCARD-LONGINT widenings are value no-ops (all are
one slot; see §4). Because 32-bit errors would be silent, CARDINAL/INTEGER arithmetic
uses the checked forms of §11.7 when CHECK-range diagnostics are enabled.
| Hex | Mnemonic | Effect |
|---|---|---|
| C0 | uadd_checked | pop b; push (pop + b) (CARDINAL; raise Overflow on wrap) |
| C1 | usub_checked | pop b; push (pop - b) (CARDINAL; raise on borrow) |
| C2 | umul_checked | pop b; push (pop * b) (CARDINAL; raise on overflow) |
| C3 | system | host call (§9.3) |
| C4 | string_comp | see §12.1 |
| C5 | long_compare | pop b; pop a; push (a>b), push (a<b) (LONGINT, signed 64) |
| C6 | long_add | pop b; pop a; push a + b (LONGINT) |
| C7 | long_sub | pop b; pop a; push a − b |
| C8 | long_mul | pop b; pop a; push a × b |
| C9 | long_div | pop b; pop a; push a DIV b (signed; raise on 0) |
| CA | long_mod | pop b; pop a; push a MOD b (signed; raise on 0) |
| CB | not_zero | push (pop # 0) |
| CC | long_abs | pop a; push |
| CD | switch | §12.2 (case tables) |
| CE | jump_stack | pop; IP := value (computed jump/return) |
| CF | push_code_addr | u64; push (IP - 1 + off) (pc-relative) |
| D0 | iadd_checked | pop b i32; push (pop + b); raise Overflow (INTEGER) |
| D1 | isub_checked | pop b i32; push (pop - b); raise Overflow (INTEGER) |
| D2 | reserve | pop size; check limit; SP -= size; push new SP |
| D3 | reserve_string | see §13 |
| D4 | enter | u8 k — prologue, reserve (255−k)*8 bytes |
| D5 | real_compare | pop r2; pop r1; push r1>r2, push r1<r2 (REAL) |
| D6 | real_add | pop r2; pop r1; push r1+r2 (REAL) |
| D7 | real_sub | pop r2; pop r1; push r1−r2 |
| D8 | real_mul | pop r2; pop r1; push r1×r2 |
| D9 | real_div | pop r2; pop r1; push r1/r2 |
| DA | urange_check | pop low; pop size; Top in [low, low+size]? (CARDINAL) |
| DB | irange_check | pop low i32; pop size; Top in [low, low+size]? (INTEGER) |
| DC | limit_check | u8 n; if Top() > n raise RangeError (CARDINAL) |
| DD | check_positive | if (i32)Top() < 0 raise RangeError (INTEGER) |
| DE | and_jp | u8 n; if not pop then push false; IP += n |
| DF | or_jp | u8 n; if pop then push true; IP += n |
| Hex | Mnemonic | Operand | Effect |
|---|---|---|---|
| E0 | jp | i64 | IP += rel |
| E1 | jpfalse | i64 | if not pop then IP += rel |
| E2 | jp_fwd | i8 | IP += rel |
| E3 | jpfalse_fwd | i8 | if not pop then IP += rel |
| E4 | jp_back | u8 | IP -= rel |
| E5 | jpfalse_back | u8 | if not pop then IP -= rel |
| E6 | bit_or | — | pop b; push bitset(pop) ∪ bitset(b) |
| E7 | bit_in | — | pop b; push (pop ∈ bitset(b)) |
| E8 | bit_and | — | pop b; push bitset(pop) ∩ bitset(b) |
| E9 | bit_xor | — | pop b; push bitset(pop) △ bitset(b) |
| EA | power2 | — | push (1 << pop) |
| EB | extern_proc_call | — | n:=pop; base:=pop; call via MTBL (base=addr) |
| EC | nested_call | u8 n | call proc n with OFP = FP (static nesting) |
| ED | proc_call | u8 n | call proc n, OFP = NIL |
| EE | call_with_frame | u8 n | pop f; call proc n with OFP = f |
| EF | extern_call | mod,proc | call proc in module mod (2 operands) |
| F0 | extern_call_nib | nibble | call proc n in module m (high/low nibbles) |
| F1–FF | call 1..15 | — | call proc op & 0x0F, OFP = NIL |
Jump offsets are relative to the address after the operand(s). All jumps except
E0/E1 are byte offsets; E0/E1 carry a full 64-bit signed offset.
SET operations (0E6H–0EAH) operate on the 64-bit SET type; element indices are
0..63 and power2 computes 1 << pop (mod 2^64).
pop size2; pop size1 ; buffer lengths (bytes)
pop str2; pop str1
compare char-by-char up to a NUL terminator or exhausted size
push (str1 > str2) ; boolean
push (str1 < str2) ; boolean
The caller consumes the two booleans (typically or_jp / and_jp to build lexicographic
ordering tests).
Encoding after the opcode:
lowBound u64
highBound u64
retOffset i64 ; reserved; must be 0
jumpTable (highBound − lowBound + 1) × i64
Dispatch:
N := high − low + 1
first := address of the first table cell
endOfTable := first + N*8
if value < lowBound or value > highBound:
IP := endOfTable ; default code follows the table
else:
cell := first + (value − lowBound)*8
off := (i64)*cell
if off < 0: ; call-switch form
push endOfTable ; return address after the table
IP := cell + 8 + off
Entry offsets are relative to the end of their own cell; a negative offset denotes a
call-switch (each case is a procedure, returning to the code after the table).
retOffset is retained for encoding compatibility with MCode/16 and must be 0.
reserve_string (0D3H and 0x40 0x12)src := pop ; byte address of the string data
nBytes := pop
nWords := (nBytes + 7) DIV 8
copy nWords words from src onto the stack (stack limit checked)
push the address of the copy ; top of the copied region
Used by the compiler to materialize a local owned copy of a string constant (the constant itself lives in the code area at a pc-relative address).
Call(modName):
save current module chain and caller frame
MARK(heap)
LoadWithDependencies(modName)
for each newly loaded module:
allocate + zero global variables
for each module flagged TOINIT, in load order:
GP := module base
FP := fresh frame ; reference frame
IP := procedureAddress(module, 0) ; its initializer
Run()
RELEASE(heap mark)
restore caller module chain
The last module initialized is the requested one; this is how the system boots the
target module and returns to the shell when it end_programs.
| MCode/16 | MC64 |
|---|---|
| word = 16 bits, address space 64 KB | word = 64 bits, address space 2^64 |
| scalar types 16 bits; LONGINT/REAL 32 bits (2 words); LONGREAL 64 (4 words) | CARDINAL/INTEGER 32 bits; SET/LONGINT/LONGCARD 64 bits; REAL 64-bit float; LONGREAL 128 bits (2 slots) |
| byte/nibble operands (bytes, # args, depths) | byte operands unchanged; all word operand fields widened 2→8 bytes |
| local addressing in bytes (F+2n) | local addressing in words (FP ± n*8) |
| global window 64 K words, module table in top entries | window of ≥64 K words + dedicated 256-slot module table MTBL |
| module baseline tables relative to base−2/−18/−14 | explicit procsAddr, loadAddr, dependencies[] fields (module descriptor) |
Enter reserves 255−n bytes |
Enter reserves (255−n)×8 bytes |
| immediates: byte / 2-byte word / 4-byte dword | byte / 8-byte word / 16-byte quad |
| switch tables: 16-bit offsets, +8000H unsigned trick | 64-bit signed offsets, plain unsigned compare, retOffset reserved |
proc table: base-2 cell, +1+offset |
procsAddr + k*8 + offset |
| BDOS 0C3H | SYSTEM host-call ABI (§9.3) |
.MCD (no magic, CP/M 8-char names) |
.MC4 (magic header, 16-char names, 64-bit fields) |
All 256 opcodes keep their MCode/16 position in the map; only operand widths and the scalar→(32/64/128-bit) mapping differ. The scalar word-op families (0A0H–0BFH) are 32-bit; the "long" families (0C5H–0CCH) are 64-bit; LONGCARD arithmetic is provided by the extended sub-dispatch (§10.2).
A procedure P(x: CARDINAL): CARDINAL that computes 2*x + 1 (32-bit CARDINAL ops):
d4 fe enter -2 ; prologue: reserve (255-0xFE)*8 = 8 bytes (1 slot)
8d 02 load_imm_byte 2
3c fe store_local -2 ; tmp := 2
03 load_param1 ; x (FP[3])
2c fe load_local -2 ; tmp
a8 umul ; CARDINAL multiply (32-bit)
8d 01 load_imm_byte 1
a6 add ; CARDINAL add (32-bit)
85 01 fct_leave 1 ; return; drop 1 word (the parameter)
Provenance: transcribed from the reverse-engineering of Borland Turbo Modula-2 (CP/M
Z80), the MCode_specification bytecode interpreter in Reversing-Turbo-Modula2.
The reference interpreter in mc64/ is written in pure ISO Modula-2 and compiled
exclusively with gm2 -fiso. These notes record the GNU M2 idioms and constraints the
source relies on.
gm2 -fiso -c <module>.mod..o. Passing several .mod files at once makes gm2 emit one main per
module and the link fails on duplicate main.Memory Local Global Stack Instruction Extended Interpreter
Loader2 Console FileIO (each .mod), then MC64.mod and mkdemo.mod, then
gm2 -fiso -o mc64 MC64.mod <12 .o> and gm2 -fiso -o mkdemo mkdemo.mod <12 .o>.AND/OR are Boolean-only: gm2's ISO front end has no integer bitwise AND/OR.
Integer bitwise ops are done with a BitOp(a, b, mode) loop (64 iterations of
DIV 2^k/MOD 2); bit masking uses MOD/DIV arithmetic (opc MOD 16,
(fl DIV 4) MOD 2 for the TOINIT bit). A stray integer AND typically surfaces as a
misleading "is not a boolean expression" diagnostic pointed at a VAR declaration
several lines earlier.Pop () ; (return value discarded)
does not compile; always capture into a variable.CASE : BOOLEAN OF |
TRUE: ... | FALSE: ... works, CASE : CARDINAL OF is rejected ("not all variant
record alternatives"). This is the mechanism used to reinterpret a 64-bit slot as
REAL/LONGCARD and a 128-bit LONGREAL as two slots.FROM m IMPORT x AS y is a parse error. Name clashes (e.g.
IOChan.WriteLn vs a local WriteLn) are avoided by not importing the colliding name
(the module writes LF itself).FlagSet{...} argument can crash gm2
("internal compiler error: expecting ConstVar symbol"); assign the constructor to a
local VAR and pass the variable instead.IMPORT must precede EXPORT QUALIFIED.HALT(n) works under -fiso and is used for fatal host errors (THROW aborts
with SIGABRT).DEFINITION MODULE FOR "C", no M2PIM legacy modules (Args,
UnixArgs, …). Vendor m2pim modules fail to link against -fiso objects.System.ProgramArgs.ArgChan is not usable in this gm2 build: TextRead on it
blocks forever and Look returns a '.' filler instead of endOfInput. The VM
therefore boots a fixed boot.mc4 (generated by mkdemo) instead of parsing argv.StreamFile.Open/Close + IOChan.RawRead/RawWrite (ChanConsts
FlagSet{readFlag, oldFlag, rawFlag} / {writeFlag, rawFlag}).StdChans.StdOutChan) with IOChan.TextWrite; LC real values go out via
RealStr.RealToStr.CHAR arena
(Memory.ms, MaxMem = 16777216) with byte-assembling readers/writers
(ReadSlot/WriteSlot, ReadLong/WriteLong, ReadWord/WriteWord).ArenaBase = 1048576 via LoadImage(data, off, dst, n)
(images start at file offset 64, hence the source offset parameter). 32-bit descriptor
fields (e.g. checksum) must be read with ReadLong, never with the 8-byte reader.LONGCARD is used for addresses, stack slots and 64-bit arithmetic; untyped literals,
H-suffixed hex constants and VAL conversions all compile under -fiso.