mc64-spec.md 41 KB

MC64 — A 64-Bit Stack Machine

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


1. Naming and document purpose

  • word (slot) = 8 bytes = 64 bits — the unit of the stack, frame cells and global cells.
  • scalar = a typed value held in one slot: 32-bit types (CARDINAL, INTEGER, and 8-bit BYTE/CHAR/BOOLEAN) or 64-bit types (LONGINT, LONGCARD, SET, REAL, pointer).
  • quad = two slots = 16 bytes = 128 bits (the LONGREAL type).
  • Operands that in MCode/16 were 16-bit words are here 64-bit words; byte operands stay bytes unless stated otherwise.

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.


2. Memory model

  • Flat, byte-addressable address space; all addresses are 64-bit little-endian.
  • Basic storage unit is the byte; the machine word is 8 bytes; words are naturally aligned (address % 8 == 0) unless stated otherwise.
  • Little-endian everywhere, including stack entries, code operands and multibyte values.
  • 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.


3. Registers / machine state

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.


4. Data types

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.


5. Stack and frames

One stack holds evaluation data and activation frames.

5.1 Frame layout

              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]
  ...
  • Locals use negative word offsets: FP[-1], FP[-2], … The last reserved word (adjacent to the frame link) is offset -1.
  • Parameters use positive word offsets: parameter k is at FP[k+2].
  • The caller pushes arguments in reverse declaration order (last parameter pushed first), so parameter 1 is the deepest argument and lands at FP[3].

5.2 Call sequence

  ; 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 res
  • LONGREAL_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.


6. Addressing families

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


7. Modules, procedures and linkage

7.1 Module descriptor (in-memory)

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.

7.2 Procedure address resolution

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.

7.3 External calls

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.


8. Loader and on-disk format

A module file ("*.MC4") holds one or several linked modules plus their dependencies.

8.1 File header (64 bytes)

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

8.2 Dependency record (32 bytes each)

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

8.3 Load sequence

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

8.4 Global variable allocation

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.


9. Execution

9.1 Run loop

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.

9.2 Exceptions

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.

9.3 Host interface (0C3H 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.


10. Sub-opcode dispatches

Two opcodes delegate to a secondary byte:

10.1 Sub-opcode 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.)

10.2 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

11. Instruction set reference

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.

11.1 0x00–0x1F — loads, stores, params, indexed

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]

11.2 0x20–0x3F — stack ops, local/global loads & stores, copies

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

11.3 0x40–0x5F — extended dispatch, global 2–15

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)

11.4 0x60–0x7F — indirect loads/stores (offsets 0–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)

11.5 0x80–0x9F — addresses, leaves, strings, immediates

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

11.6 0xA0–0xBF — comparisons, integer arithmetic, conversions

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.

11.7 0xC0–0xDF — checked/long/real arithmetic, switch, short-circuit

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

11.8 0xE0–0xFF — jumps, bit sets, calls

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


12. Complex instructions

12.1 String compare (0C4H)

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

12.2 Switch / case statement (0CDH)

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.

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


13. Initialization sequence

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.


14. Differences from MCode/16 (migration notes)

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


15. Minimal encoding example

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.


16. GNU Modula-2 (ISO) implementation notes

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.

16.1 Build rules

  • Every module is compiled with gm2 -fiso -c <module>.mod.
  • Only a program module is passed to the link step; library modules are given as pre-compiled .o. Passing several .mod files at once makes gm2 emit one main per module and the link fails on duplicate main.
  • Deterministic build: 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>.

16.2 Language constraints observed

  • 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.
  • Ignored function results are errors: a bare Pop () ; (return value discarded) does not compile; always capture into a variable.
  • Variant records: the tag must be an exhaustive choice; 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.
  • No import alias: 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).
  • Set constructors: an inline FlagSet{...} argument can crash gm2 ("internal compiler error: expecting ConstVar symbol"); assign the constructor to a local VAR and pass the variable instead.
  • Definition modules: an IMPORT must precede EXPORT QUALIFIED.
  • HALT(n) works under -fiso and is used for fatal host errors (THROW aborts with SIGABRT).

16.3 Host services

  • No libc: no 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.
  • File I/O uses StreamFile.Open/Close + IOChan.RawRead/RawWrite (ChanConsts FlagSet{readFlag, oldFlag, rawFlag} / {writeFlag, rawFlag}).
  • Screens output writes go to the ISO standard output channel (StdChans.StdOutChan) with IOChan.TextWrite; LC real values go out via RealStr.RealToStr.

16.4 Machine representation

  • Slots are 64-bit Little-Endian, addressed through a 16 MiB static CHAR arena (Memory.ms, MaxMem = 16777216) with byte-assembling readers/writers (ReadSlot/WriteSlot, ReadLong/WriteLong, ReadWord/WriteWord).
  • The image is loaded at 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.