Skip to content

Latest commit

 

History

History
449 lines (369 loc) · 30.8 KB

opcode.md

File metadata and controls

449 lines (369 loc) · 30.8 KB

Lilith M-Code opcode reference

Complete, per-opcode reference for the Lilith M-Code instruction set as implemented in this project. Ground truth for every entry here is src/m2/m2c/decode.MOD's Mnem table (mnemonic-to-opcode mapping and operand-shape marker) cross-checked against src/m2/m2c/M3GL.MOD (the real compiler's code generator — gives the actual stack effect of every opcode through its PutOp* call sites) and docs/Lilith.txt's Appendix 1, "The M-code interpreter" — the ETH report's own Modula-2 program defining the instruction set, which is the only per-opcode semantics this project has. There was no pre-existing M-Code emulator to consult (see README.md); the semantics were re-derived from those two sources and implemented independently in src/c/lilith/cpu.c (nothing was copied). Named opcode constants (M2_OP_xxx, M2_OPBASE_xxx) are defined once, for all three tools (lilith, m2c, m2l), in src/c/m2.h.

Machine model: Lilith is a 16-bit stack machine. A CARDINAL word is 16 bits; INTEGER is the same 16 bits interpreted two's-complement; BITSET is the same 16 bits interpreted as a bit vector, numbered from the most significant bit: element 0 is physical bit 15 (see the bit-numbering note in §12). There are two logically separate stacks (see src/c/lilith/cpu.c's file header for the full discussion):

  • the expression stack — a small register-file-like stack every opcode below pushes/pops through; holds intermediate values within one statement/expression.
  • the memory stack — word-addressed memory (cpu->mem[]) holding global data, the heap, and every procedure's own parameter/local frame, addressed relative to bp ("L", current frame base) or gp ("G", current module's global-data base).

Stack-effect notation below: ( before -- after ), top of stack on the right. x, y, ... are single words; xh,xl denotes a 2-word (32-bit/double or REAL) value as (high, low), high pushed first (deeper), matching SYSTEM.LONG's own convention.

Opcode numbers are hexadecimal throughout, as they are in m2.h and in both implementations. The original sources spell them in octal (PutOpZ(353B)), which is why the C tree's comments sometimes still do.

Jump targets are always relative to the address right after the opcode's own operand (i.e. the address of the next instruction), not the opcode byte itself.

Operand-shape legend

Marker Meaning
(none) no operand bytes follow the opcode
byte one operand byte follows
byte,byte two operand bytes follow
word one 2-byte big-endian operand word follows
word,word two 2-byte big-endian operand words follow
byte,word one operand byte, then one operand word
(imm) no operand bytes; a small value is baked into the low bits of the opcode itself (see the family tables below)

1. Literals and constants

Opcode Mnemonic Operand Stack effect Semantics
0x00–0x0F LI0–LI15 (imm) ( -- n ) Push the immediate n (0..15) baked into the opcode. M2_OPBASE_LI + n.
0x10 LIB byte ( -- n ) Push the operand byte, zero-extended.
0x12 LIW word ( -- n ) Push the operand word.
0x13 LID word,word ( -- hi,lo ) Push a 32-bit immediate as two words: first operand word = high half, second = low half.
0xD5 LIN (none) ( -- 0xFFFF ) Push all-ones (0xFFFF) — the CARDINAL/INTEGER "−1" literal.

2. Addressing (push an address, not a value)

Opcode Mnemonic Operand Stack effect Semantics
0x14 LLA byte ( -- addr ) Push bp + offset (address of a local variable).
0x15 LGA byte ( -- addr ) Push gp + offset (address of a global variable in the current module).
0x16 LSA byte ( base -- addr ) Pop a base address, push base + offset (address of a structured-value field/element).
0x17 LEA word ( -- addr ) External (cross-module) variable address. Operand word packs (moduleNo:8, localAddr:8); resolves through that module's own dataBase[] and pushes the absolute address.

3. Local / global / structured word load-store

bp-relative ("local"), gp-relative ("global"), and address-on-stack ("structured") families each come in four shapes: single word (W), double/2-word (D), and — for the local/global forms only — a 16-wide inline-immediate-offset family for offsets that fit in the opcode's own low bits (W2..Wf etc.).

3.1 Local (bp-relative)

Opcode Mnemonic Operand Stack effect Semantics
0x20 LLW+ byte ( -- v ) Push mem[bp+offset].
0x21 LLD+ byte ( -- hi,lo ) Push mem[bp+offset], mem[bp+offset+1] (2-word load).
0x24–0x2F LLW4–LLWf (imm) ( -- v ) LLW+ with offset = 4..15 baked into the opcode (M2_OPBASE_LLW + n; offsets 0–3 are never emitted this way — see note below).
0x30 SLW+ byte ( v -- ) Store v to mem[bp+offset].
0x31 SLD+ byte ( hi,lo -- ) Store hi to mem[bp+offset], lo to mem[bp+offset+1].
0x34–0x3F SLW4–SLWf (imm) ( v -- ) SLW+ with offset 4..15 baked into the opcode (M2_OPBASE_SLW + n).

Note: offsets 0–3 of the LLW/SLW inline-immediate family are architecturally valid but never observed in m2c-generated code (a procedure's first local slots are reserved by the calling convention); see src/c/lilith/opcodes.c's own header comment for the empirical basis of this.

3.2 Global (gp-relative)

Opcode Mnemonic Operand Stack effect Semantics
0x40 LGW+ byte ( -- v ) Push mem[gp+offset].
0x41 LGD+ byte ( -- hi,lo ) Push mem[gp+offset], mem[gp+offset+1].
0x42–0x4F LGW2–LGWf (imm) ( -- v ) LGW+ with offset 2..15 (M2_OPBASE_LGW + n).
0x50 SGW+ byte ( v -- ) Store v to mem[gp+offset].
0x51 SGD+ byte ( hi,lo -- ) Store hi to mem[gp+offset], lo to mem[gp+offset+1].
0x52–0x5F SGW2–SGWf (imm) ( v -- ) SGW+ with offset 2..15 (M2_OPBASE_SGW + n).

gp always names the currently executing module's own global-data base; it changes on a cross-module call/return (CLX/RTN), not on a plain local call.

3.3 Structured (address popped off the stack)

Opcode Mnemonic Operand Stack effect Semantics
0x60–0x6F LSW0–LSWf (imm) ( addr -- v ) Pop addr, push mem[addr+n], n = 0..15 baked into the opcode (M2_OPBASE_LSW + n).
0x70–0x7F SSW0–SSWf (imm) ( addr,v -- ) Pop v then addr, store v to mem[addr+n] (M2_OPBASE_SSW + n).
0x80 LSW+ byte ( addr -- v ) Pop addr, push mem[addr+offset].
0x81 LSD+ byte ( addr -- hi,lo ) Pop addr, push mem[addr+offset], mem[addr+offset+1].
0x82 LSD0 (none) ( addr -- hi,lo ) Pop addr, push mem[addr], mem[addr+1] (offset 0 case of LSD+).
0x90 SSW+ byte ( addr,v -- ) Pop v, pop addr, store v to mem[addr+offset].
0x91 SSD+ byte ( addr,hi,lo -- ) Pop lo, hi, addr; store hi at addr+offset, lo at addr+offset+1.
0x92 SSD0 (none) ( addr,hi,lo -- ) Offset-0 case of SSD+.

4. Indexed (array-element) load/store

GenIndex leaves a checked base address and a 0-based element index as two separate words on the stack; these opcodes combine them — but only when the compiler has left it that much work to do. Two cases never reach them:

  • A constant index into a fixed array whose word offset comes out below 256, and whose element is not a CHAR, is folded into the base address itself (GenIndex sets x.off and the mode to fldMd): no index word is pushed at all, and the access becomes an offset load/store — the LSW+/LSD+/SSW+/SSD+ row of §3.3. A CHAR element is excluded because its offset is in bytes, not words.
  • An element larger than two words has its index pre-scaled to words by GenIndex — SHL when the element size is a power of two, UMUL otherwise — after the lower-bound adjustment and the range check (CHKZ, and CHKS for a signed index). UADD then combines base and offset, and the load or store is a plain one.

So the scaling these opcodes do is the scaling for the sizes they can do cheaply: LXW and SXW index by element, LXB and SXB halve a CHAR index to reach the word (2 chars/word), LXD and SXD multiply by 2. Anything else has already been converted to a word offset before the opcode is chosen.

Opcode Mnemonic Operand Stack effect Semantics
0x83 LXFW (none) ( base,idx -- hi,lo ) Indexed load of a 2-word REAL element: ea = base + 2*idx. Mechanically identical to LXD; kept for opcode coverage (m2c itself only ever emits LXD for REAL).
0x84 LST+ byte ( -- addr ) LSTA adr: push the absolute address of a string-table literal. adr is AllocString's word offset relative to the string table's own start; the emulator adds the module's gp and the string table's base address (captured from the module's second data block at load time).
0x85 LXB (none) ( base,idx -- v ) Indexed load of a byte-packed CHAR element (2 chars/word, high byte = even index): wa = base + idx/2; v = high byte of mem[wa] if idx even, low byte if odd.
0x86 LXW (none) ( base,idx -- v ) Indexed load of a 1-word element: push mem[base+idx].
0x87 LXD (none) ( base,idx -- hi,lo ) Indexed load of a 2-word element: ea = base + 2*idx; push mem[ea], mem[ea+1]. The index is a raw element count — LXD/SXD scale by 2 themselves.
0x93 SXFW (none) ( base,idx,hi,lo -- ) Indexed store of a 2-word REAL element. Mechanically identical to SXD.
0x95 SXB (none) ( base,idx,v -- ) Indexed store of a byte-packed CHAR element (same packing as LXB); other byte of the target word preserved.
0x96 SXW (none) ( base,idx,v -- ) Indexed store of a 1-word element.
0x97 SXD (none) ( base,idx,hi,lo -- ) Indexed store of a 2-word element: ea = base + 2*idx.

5. External (cross-module) variable access

Opcode Mnemonic Operand Stack effect Semantics
0x22 LEW byte,byte ( -- v ) Load a word from another module's global data: operand bytes (moduleNo, localAddr); resolves moduleNo through dataBase[].
0x23 LED byte,byte ( -- hi,lo ) Same as LEW, 2-word load.
0x32 SEW byte,byte ( v -- ) Store a word into another module's global data.
0x33 SED byte,byte ( hi,lo -- ) Same as SEW, 2-word store.

6. Jumps

All targets are self-relative to the address right after the opcode's own operand. JxC variants pop a word and branch if it is zero (Modula-2 FALSE); the unconditional forms take no stack input.

Opcode Mnemonic Operand Stack effect Semantics
0x18 JPC word ( c -- ) Pop c; if c = 0, jump forward/backward by the signed word offset.
0x19 JP word (none) Unconditional jump by the signed word offset.
0x1A JFC byte ( c -- ) Pop c; if c = 0, jump forward by the byte offset.
0x1B JPF byte (none) Unconditional forward jump by the byte offset.
0x1C JBC byte ( c -- ) Pop c; if c = 0, jump backward by the byte offset.
0x1D JPB byte (none) Unconditional backward jump by the byte offset.
0x1E ORJ byte ( c -- c or ∅ ) Short-circuit OR: peek c; if nonzero, jump forward (leaving c on the stack as the result); otherwise pop it and fall through.
0x1F AJP byte ( c -- c or ∅ ) Short-circuit AND: peek c; if zero, jump forward (leaving c); otherwise pop it and fall through.

7. Unsigned (CARDINAL) arithmetic

Opcode Mnemonic Stack effect Semantics
0xB8 UADD ( x,y -- x+y ) Unsigned add, wraps mod 65536.
0xB9 USUB ( x,y -- x-y ) Unsigned subtract, wraps mod 65536.
0xBA UMUL ( x,y -- x*y ) Unsigned multiply, low 16 bits of the product.
0xBB UDIV ( x,y -- x/y ) Unsigned divide. Traps (runtime error) if y = 0.
0xBC UMOD ( x,y -- x mod y ) Unsigned remainder. Traps if y = 0.

8. Signed (INTEGER) arithmetic

Opcode Mnemonic Stack effect Semantics
0xD8 IADD ( x,y -- x+y ) Signed add.
0xD9 ISUB ( x,y -- x-y ) Signed subtract.
0xDA IMUL ( x,y -- x*y ) Signed multiply.
0xDB IDIV ( x,y -- x/y ) Signed divide. Traps if y = 0.
0xCE ABS ( x -- |x| ) Signed absolute value.
0xCF NEG ( x -- -x ) Signed negate.

9. Double-precision (32-bit) arithmetic

Two-word values are (hi,lo), hi pushed first (deeper) — matching SYSTEM.LONG's own convention (see M2S.MOD's decimal-literal scanner).

Opcode Mnemonic Stack effect Semantics
0x88 DADD ( xh,xl,yh,yl -- rh,rl ) 32-bit signed add.
0x89 DSUB ( xh,xl,yh,yl -- rh,rl ) 32-bit signed subtract.
0x8A DMUL ( x,y -- rh,rl ) Widening multiply: two single words in, one 32-bit product out.
0x8B DDIV ( xh,xl,yh,yl -- rh,rl ) 32-bit signed divide. Traps if the divisor is 0.
0x8E DSHL ( xh,xl -- rh,rl ) Shift the 32-bit value left by exactly 1 bit (no operand). Ground truth: M2S.MOD's Times16 uses four consecutive DSHLs for ×16.
0x8F DSHR ( xh,xl -- rh,rl ) Arithmetic shift right by exactly 1 bit.

10. REAL (IEEE-754 single precision) arithmetic

REAL is 2 words, same (hi,lo) convention as the double-precision group. FCT's byte operand selects direction.

Opcode Mnemonic Operand Stack effect Semantics
0x98 FADD (none) ( xh,xl,yh,yl -- rh,rl ) REAL add.
0x99 FSUB (none) ( xh,xl,yh,yl -- rh,rl ) REAL subtract.
0x9A FMUL (none) ( xh,xl,yh,yl -- rh,rl ) REAL multiply.
0x9B FDIV (none) ( xh,xl,yh,yl -- rh,rl ) REAL divide. Traps if the divisor is 0.0.
0x9C FCMP (none) ( xh,xl,yh,yl -- rel ) Pop two REALs, push a signed relation code (0xFFFF if x<y, 1 if x>y, 0 if equal). The relation code is consumed, not by a normal comparison, but by the very next EQL/NEQ/ILSS/ILEQ/IGTR/IGEQ, which switches to a single-operand "test against zero" mode for that one instruction (see §11).
0x9D FABS (none) ( xh,xl -- rh,xl ) Clear the sign bit of the high word.
0x9E FNEG (none) ( xh,xl -- rh,xl ) Flip the sign bit of the high word.
0x9F FCT byte see below b0=0: FLOAT, ( v -- rh,rl ), CARDINAL → REAL. b0=2: TRUNC, ( xh,xl -- v ), REAL → CARDINAL, truncating toward zero. Any other operand byte is unimplemented (no codegen evidence).

11. Comparisons

Opcode Mnemonic Stack effect Semantics
0xAA ULSS ( x,y -- x<y ) Unsigned <.
0xAB ULEQ ( x,y -- x<=y ) Unsigned <=.
0xAC UGTR ( x,y -- x>y ) Unsigned >.
0xAD UGEQ ( x,y -- x>=y ) Unsigned >=.
0xC8 EQL ( x,y -- x=y ) (normal) / ( r -- r=0 ) (after FCMP) Equality. Generic two-operand form ordinarily; right after FCMP, tests FCMP's one-word relation code against 0 instead.
0xC9 NEQ as EQL Inequality, same dual behavior.
0xCA ILSS as EQL Signed <, same dual behavior.
0xCB ILEQ as EQL Signed <=, same dual behavior.
0xCC IGTR as EQL Signed >, same dual behavior.
0xCD IGEQ as EQL Signed >=, same dual behavior.

All comparison results push 1 (true) or 0 (false) as a CARDINAL word — Modula-2 BOOLEAN representation.

12. Bitset (BITSET) operations

Opcode Mnemonic Operand Stack effect Semantics
0xD0 OR (none) ( x,y -- x|y ) Bitwise OR.
0xD1 XOR (none) ( x,y -- x^y ) Bitwise XOR.
0xD2 AND (none) ( x,y -- x&y ) Bitwise AND.
0xD3 COM (none) ( x -- ~x ) Bitwise complement ({0..15} - x as a Modula-2 set).
0xD4 IN (none) ( x,y -- bit ) Test membership: push element x of set y (Modula-2 x IN y). Element x is physical bit 15-x (see the note below).
0xD6 MSK (none) ( n -- mask ) Push {0..n-1} as a bitset: elements 0..n-1, i.e. the high n bits, 0 if n=0, all-ones if n>=16 (see the note below).
0xD7 NOT (none) ( x -- ¬x ) Boolean NOT: 1 if x=0, else 0 (not a bitwise complement — operates on a 0/1 BOOLEAN word).
0xBD ROR (none) ( s,n -- s ror n ) Rotate s right by n bits (n masked to 0–15). Rotation direction is self-consistent regardless of bit-numbering convention (unlike IN/MSK/BIT), so needs no special note.
0xBE SHL (none) ( x,n -- x<<n ) Logical shift left by n (masked to 0–15).
0xBF SHR (none) ( x,n -- x>>n ) Logical shift right by n (masked to 0–15).
0xDD BIT (none) ( n -- {n} ) Push the singleton bitset {n} (element n, i.e. physical bit 15-n — see the note below).

Bit-numbering note: Lilith numbers BITSET elements with element 0 as the most significant physical bit and element 15 as the least significant one — confirmed against docs/Lilith.txt's reference opcode semantics: NOT is defined there as XOR against the singleton set {15} to flip a 0/1 BOOLEAN word, which only works if element 15 is the bit a 0/1 value actually occupies. This is the opposite of the C-native "bit n = 1<<n" convention, so IN/MSK/BIT all need an explicit 15-n translation between a Modula-2 set element number and the corresponding host bit position (getting this backwards was a real bug in both src/c/lilith/cpu.c and src/c/m2c/m3gl.c's MSK helper, now fixed).

13. Expression-stack spill/reload

A call can only use the small expression-stack register file for its own argument-passing and result; these opcodes spill whatever the caller had pending there (e.g. accum + F(...)) into a tagged block on the memory stack before the call, and restore it afterward.

Opcode Mnemonic Semantics
0xB2 STOR STORE: move every word currently on the expression stack into a count-prefixed block on the memory stack; remember the block's address (spill nests for a spilled call that itself calls further). Expression stack is empty afterward.
0xB3 STFV STOFV: identical spill to STORE, used ahead of a call through a procedure-typed value (the packed module/proc word is popped separately by CLF).
0xB0 LDFW LODFW: pop the just-completed call's 1-word result, restore the most recently spilled block, push the result back on top.
0xB1 LDFD LODFD: same as LODFW for a 2-word (Double/REAL) result.
0xB4 STOT STORE-adjacent placeholder: pop and discard one word (a target-procedure-value placeholder in the PrepCall/procedure-typed-call sequence).

14. Stack manipulation / range checks / copy

Opcode Mnemonic Operand Stack effect Semantics
0xDE NOP (none) (none) No operation.
0xB5 COPT (none) ( a -- a,a ) Duplicate the top word (used by INC/DEC codegen: an address is pushed once by loadAdr, then duplicated so the value load and the write-back share it).
0xB6 DECS (none) ( a,b -- b ) Drop the second-from-top word.
0xB7 PCP+ byte ( src,n -- ) PCOP a: pop word count n and source address src; copy n words from src into a freshly reserved block on the memory stack, then overwrite local word a (mem[bp+a]) with that block's address. Used for a VALUE dynamic-array parameter.
0xC5 CHK (none) ( v,lo,hi -- v ) Peek v (leaves it on the stack), pop hi then lo; traps if v (signed) is outside [lo,hi].
0xC6 CHKZ (none) ( v,hi -- v ) Peek v, pop hi; traps (unsigned) if v > hi.
0xC7 CHKS (none) ( v -- v ) Peek v; traps if v (signed) is negative.
0xA5 UCHK (none) ( v,lo,hi -- v ) Peek v, pop hi then lo; traps (unsigned) if v is outside [lo,hi].

15. Procedure call / return / frame management

Opcode Mnemonic Operand Semantics
0xEB ENT+ byte ENTR n: reserve the current procedure's parameter+local region with one unconditional stack-pointer bump (4 fixed link words + n); sets bp to the base of the reserved region. Actual call linkage (return PC, caller's bp, static link) is tracked by the emulator natively, off the guest memory stack, but ENTR's own operand numbering still matches the real object-file encoding.
0xEC RTN (none) Return: restore pc, bp, sp, and the current module from the call-linkage record; the callee's expression-stack result (0, 1, or 2 words) is left exactly where the caller expects it.
0xF0 CLL+ byte CLL n: call procedure n (by index into the current module's entry table). Emitted when the callee is reachable without walking the static (lexical) chain — a same-level sibling, or a procedure calling one nested directly inside itself. The callee's static link is set to the caller's own bp.
0xF1–0xFF CLL1–CLLf (imm) Same as CLL+, with the procedure number 1..15 baked into the opcode (M2_OPBASE_CLL + n).
0xEE CLI+ byte CLI n: call an outer-lexical-level procedure. The byte operand is the procedure number; the static link is the word already on the expression stack, pushed by an immediately preceding GB/GB1.
0xED CLX- byte,byte Cross-module call. First byte = target module number (rewritten by m2l from a module-local import index to the final global module number at link time); second byte = procedure number within that module's own entry table. Static link is always 0 (no Lilith lexical scope crosses a module boundary).
0xEF CLF (none) Call through a first-class procedure value. Pops a packed word (moduleNo:8, procNo:8) — the same packing LIW produces for a procedure-valued constant/variable. Propagates the caller's own static link (a procedure value carries no separate static-link component of its own).
0xE8 GB + byte GB n: walk n hops outward along the static (lexical-enclosing) chain and push the resulting frame-base address. Traps if n = 0.
0xE9 GB1 (none) Push the currently executing frame's own static link (one hop — the common case, emitted separately from GB n for that reason).
0xA8 ENP+ byte Set interrupt priority. No-op (no interrupts are modeled on the host).
0xA9 EXP (none) Exit/restore interrupt priority. No-op.

16. Block move / compare

Opcode Mnemonic Stack effect Semantics
0xE0 MOV ( dst,src,n -- ) Copy n words from src to dst, one word at a time, low address first. Operand order on the stack (bottom→top) is dst, src, n.
0xE1 CMP ( dst,src,n -- dv,sv ) Compare n words starting at dst and src; stop at the first mismatch (or the last word if all equal) and push the mismatching dst word then the mismatching src word (0,0 if n=0). The caller derives the relation itself from dv/sv.
0xDF MOVF — Unimplemented: no confirmed operand format from any codegen call site.

17. Traps and checks

Opcode Mnemonic Stack effect Semantics
0xC4 TRAP ( n -- ) Pop n; abort execution with a runtime trap reporting TRAP n.

18. Console and disk I/O

Opcode Mnemonic Semantics
0xA1 WRT ( c -- ): pop a word, write its low byte to stdout.
0xA0 READ ( -- c ): read one character from stdin and push it; pushes 0x1B (ESC) at end-of-file (Wirth's original interactive M3PL.MOD loop treated ESC as its exit signal).
0x94 TS ( addr -- old ): "test and set" — pop an address, push the word currently stored there, then unconditionally store 1 ("true") at that address. Used by a module's init-guard prologue (LGA 1; TS; JFC ...) to run module initialization exactly once.
0xA2, 0xA3, 0xA4 DSKR, DSKW, STRK Real Lilith disk-track hardware I/O. Not implemented — no meaningful host equivalent in a portable emulator; traps as unimplemented if reached.

19. SYS n — host file-I/O extension

SYS (opcode 0xA7, byte operand = subcall number) is this project's own extension, backing lib/FileSystem.MOD's host file I/O. It has no counterpart on real Lilith hardware except subcall 4 (heap-size query), which has concrete evidence from real m2orig codegen (M3DL.MOD's Size()). Every subcall not listed below traps as unimplemented.

Subcall Name Stack effect Semantics
4 heap size query ( -- n ) Push LIL_MEM_WORDS - 1 as a stand-in for "how much heap memory is available".
10 FileOpen ( nameAddr,nameHigh,newFlag -- handle ) Open a file. Name is packed 2 chars/word starting at nameAddr, nameHigh chars long (same packing as LXB/SXB); newFlag selects write ("wb") vs read ("rb") mode. Pushes a 1-based handle, or 0 on failure.
11 FileClose ( handle -- ) Close the file associated with handle.
12 FileReadChar ( handle -- packed ) Read one character. Result is packed as (res<<8)|ch: res=1 and the character in the low byte on success, 0 at EOF/error.
13 FileWriteChar ( handle,ch -- ) Write one character to the file.
14 FileGetPosHi ( handle -- hi ) High word of the file's current 32-bit position (paired with subcall 19).
15 FileSetPos ( handle,p0,p1 -- ) Seek to the 32-bit position packed as (p0<<16)|p1.
16 FileReadWord ( handle -- w ) Read one big-endian 16-bit word (two fgetcs). Pushes 0 if either byte hits EOF — a caller distinguishing a genuine 0 word from EOF must check subcall 20 first, as FileSystem.MOD's ReadWord wrapper does.
17 FileWriteWord ( handle,w -- ) Write one big-endian 16-bit word (two fputcs).
18 FileDelete ( handle -- res ) Close handle if open, then delete the file (recalled by name from a table remembered at FileOpen time — the operand is a handle, not a name). Pushes 1 if deleted, 0 otherwise.
19 FileGetPosLo ( handle -- lo ) Low word of the file's current 32-bit position.
20 FileAtEOF ( handle -- atEof ) Pushes 1 if the next read would hit end-of-file, 0 otherwise. Implemented by peeking one byte (fgetc+ungetc) rather than feof(), which in C only becomes true after a read has already failed.

20. CASE-statement dispatch

Opcode Mnemonic Operand Semantics
0xC2 ENC word ENTC: pop the selector value. The operand word is a self-relative offset to a jump table immediately following: low (word), high (word), elseTarget (self-relative word), then one self-relative word per selector value in [low,high]. Jumps to the matching arm, or to elseTarget if the selector is out of range. Records the address right after the whole table so the matching EXC knows where to jump when the arm finishes.
0xC3 EXC (none) Unconditional jump to the address recorded by the innermost active ENTC (end of a CASE arm).

21. Heap allocation

Opcode Mnemonic Stack effect Semantics
0xEA ALOC ( n -- addr ) Pop a word count n; push the current memory-stack pointer as the newly allocated block's address, then bump the stack pointer up by n — a simple bump-allocator, growing the same memory-stack region ENTR grows. No M3GL.MOD call site emits this (heap allocation happens in ordinary Modula-2 source, not compiler-generated code), but it is reachable once M3PL.MOD's REPL starts processing input.

22. FOR-loop dispatch

The FR1/FR2 mnemonics share their row prefix with the (unrelated, unimplemented) FADD-family float-I/O placeholder names in decode.MOD's table, but are genuinely the FOR-statement codegen family — not float I/O.

Opcode Mnemonic Operand Semantics
0xC0 FR1? byte,word FOR1: stack (bottom→top) is addr(loopVar), initial, bound. Pops all three; stores initial at addr. Byte operand is loop direction (0 = ascending TO, nonzero = descending TO via BY with a negative step). Word operand is the self-relative exit-jump target. If the loop shouldn't execute even once (comparing initial against bound per dir, signed), jumps directly to the exit target; otherwise remembers addr/bound/dir for the matching FOR2 and falls through into the loop body.
0xC1 FR2? byte,word FOR2: reached once per iteration, at the bottom of the loop body (zero stack input). Byte operand is the signed step (already in two's-complement form, e.g. BY -1 is byte 255, not magnitude 1). Word operand is the self-relative back-jump target. Loads the loop variable, adds the step, stores it back; if the (signed) continuation test against bound still holds, jumps back to the loop body, otherwise falls through and the loop is finished.

23. Not implemented (no codegen evidence, or genuinely dead in this toolchain)

These opcodes exist in the real Lilith instruction set (per decode.MOD's Mnem table) but have no confirmed call site anywhere in M3GL.MOD's codegen and are not reachable by any program this toolchain can produce. cpu.c traps them explicitly (LIL_TRAP_UNIMPL) rather than silently misbehaving if one is ever encountered:

Opcode Mnemonic
0xE2 DDT (debugger call — no debugger exists)
0xE3 REPL
0xE4 BBLT
0xE5 DCH
0xE6 UNPK
0xE7 PACK
0xAE TRA+
0xAF RDS+
0xDF MOVF (operand format unconfirmed)

Unused opcode slots

Five byte values in decode.MOD's Mnem table are marked --- (never assigned a mnemonic) and are not valid opcodes at all: 0x11, 0x8C, 0x8D, 0xA6, 0xDC. Executing one of these traps as unimplemented, identically to the "not implemented" opcodes in §23.


Cross-reference

  • Named constants for every opcode above: src/c/m2.h (M2_OP_xxx, M2_OPBASE_xxx).
  • Opcode table construction (mnemonic strings + operand-shape decoding): src/c/lilith/opcodes.c.
  • CPU/dispatch implementation: src/c/lilith/cpu.c.
  • Code generator (the only place opcodes are actually emitted): src/m2/m2c/M3GL.MOD (original) / src/c/m2c/m3gl.c (C port).
  • On-disk object-file format these opcodes are serialized into: src/c/m2.h's M2_TAG_* constants and M2NameKey/ M2DataBlockHeader structs; see also docs/m2l.md.