# 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< 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 |v| | | 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 (ar2, push r1 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_program``s. --- ## 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 .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`.