You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.).
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 nextEQL/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).
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 highn 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 ownbp.
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.