Appendix B: The full opcode reference
Every instruction in Muspel's bytecode ISA, one row each, mnemonic matching exactly what musc compile/musc debug print (op_name, muspel/opcodes.odin). Operand width is op_operand_bytes in the same file, the single source of truth both the disassembler and the VM's own dispatch loop (vm_step, muspel/vm.odin) read from, so the two can never quietly disagree about an instruction's width. Introduced-in chapter is where the opcode's own case first appears in vm_step; every one of these was added exactly once and never changed shape afterward.
| Mnemonic | Operand | Stack effect | Semantics | Introduced |
|---|---|---|---|---|
CONSTANT | u8 idx | -> value | push chunk.constants[idx] (also how an enum member's resolved tag is pushed, Ch. 11) | Ch. 10 |
NIL | - | -> nil | push the nil value | Ch. 10 |
TRUE | - | -> true | push the boolean true | Ch. 10 |
FALSE | - | -> false | push the boolean false | Ch. 10 |
POP | - | value -> | discard the top of the stack | Ch. 10 |
DUP | - | value -> value value | duplicate the top of the stack | Ch. 10 |
ADD | - | a b -> (a+b) | numeric add, or string concat | Ch. 11 |
SUB | - | a b -> (a-b) | numeric subtract | Ch. 11 |
MUL | - | a b -> (a*b) | numeric multiply | Ch. 11 |
DIV | - | a b -> (a/b) | numeric divide; .Error on zero (Ch. 18) | Ch. 11 |
MOD | - | a b -> (a%b) | numeric modulo (math.mod, sign of the dividend); .Error on zero | Appx. G |
NEGATE | - | a -> (-a) | numeric negation | Ch. 11 |
NOT | - | a -> (!truthy(a)) | logical not | Ch. 11 |
EQUAL | - | a b -> (a==b) | value equality (struct/array equality is field-wise/element-wise, Ch. 13) | Ch. 11 |
NOT_EQUAL | - | a b -> (a!=b) | value inequality | Ch. 11 |
LESS | - | a b -> (a<b) | numeric less-than | Ch. 11 |
LESS_EQUAL | - | a b -> (a<=b) | numeric less-or-equal | Ch. 11 |
GREATER | - | a b -> (a>b) | numeric greater-than | Ch. 11 |
GREATER_EQUAL | - | a b -> (a>=b) | numeric greater-or-equal | Ch. 11 |
GET_LOCAL | u8 slot | -> value | push the current frame's local slot | Ch. 11 |
SET_LOCAL | u8 slot | value -> value | overwrite a local slot, leave value | Ch. 11 |
GET_GLOBAL | u8 name idx | -> value | push the named global's value; .Error if undefined (Ch. 18); also how alias.function (Ch. 20) resolves | Ch. 11 |
SET_GLOBAL | u8 name idx | value -> value | overwrite a global, leave value | Ch. 11 |
DEFINE_GLOBAL | u8 name idx | value -> | bind the top of stack as a global; also how a module's own functions are re-exported under a qualified name (Ch. 20) | Ch. 11 |
JUMP | i16 offset | - | ip += offset, unconditional | Ch. 11 |
JUMP_IF_FALSE | i16 offset | value -> value | if !truthy(value), ip += offset; peeks, never pops | Ch. 11 |
CALL | u8 argc | f a1..aN -> result | call f with argc arguments; .Error if not callable or over Max_Call_Depth (Ch. 18) | Ch. 12 |
RETURN | - | value -> | pop value, return it to the caller | Ch. 12 |
MAKE_STRUCT | u8 layout idx | v1..vN -> instance | build a struct from a Struct_Layout constant, scattering pushed values to their resolved field indices; .Error on arena exhaustion (Ch. 18) | Ch. 13 |
GET_FIELD | u8 name idx | obj -> value | read a struct field by name, scanning the instance's own field-name table; .Error on a non-struct or unknown field | Ch. 13 |
SET_FIELD | u8 name idx | obj value -> value | write a struct field by name, same lookup as GET_FIELD | Ch. 13 |
MAKE_ARRAY | u8 count | v1..vN -> array | build a fixed-capacity array of count elements; .Error on arena exhaustion (Ch. 18) | Ch. 13 |
GET_INDEX | - | arr idx -> value | bounds-checked array read; .Error on a non-array or out-of-range index | Ch. 13 |
SET_INDEX | - | arr idx value -> value | bounds-checked array write, same checks as GET_INDEX | Ch. 13 |
YIELD | - | - | suspend the VM; resumes at the very next instruction | Ch. 15 |
Every arithmetic and comparison opcode above (ADD through GREATER_EQUAL) reports a graceful .Error, never an Odin panic, if either operand isn't the type it expects (as_number/ as_number_pair, muspel/vm.odin, section 18.6): the table doesn't repeat that note on every row, since it's uniform across all of them. MAKE_STRUCT/GET_FIELD/SET_FIELD/MAKE_ARRAY/ GET_INDEX/SET_INDEX get the same treatment for their own failure modes (section 18.4's arena budget, and a non-struct/non-array/out-of-range operand), never an unchecked type assertion.
An enum member needs no opcode of its own: Phase.Attacking compiles straight to CONSTANT with its resolved integer tag (section 11.4), reusing machinery this table already has rather than adding a new row for it.
Operand encoding
u8: one byte, an index into the current chunk's constant pool (CONSTANT,GET_GLOBAL/SET_GLOBAL/DEFINE_GLOBAL,GET_FIELD/SET_FIELD,MAKE_STRUCT'sStruct_Layout), a local variable's stack slot (GET_LOCAL/SET_LOCAL), an argument count (CALL), or an element count (MAKE_ARRAY). Caps a single chunk at 256 constants, a single call at 255 arguments, and a single array literal at 255 elements, real limits this book names rather than hides (section 10.3).i16: two bytes, big-endian, a signed jump offset (JUMP/JUMP_IF_FALSE), measured from the instruction immediately after the jump's own two operand bytes. A negative offset is a backward jump (a loop,emit_loop);patch_jumpwrites a forward one once the jump target is finally known (section 11.3).GET_INDEX/SET_INDEXtake no operand at all: unlike a struct field's name, an array index is a runtime value, already sitting on the stack as whatever expression computed it, not something the compiler could bake into the instruction ahead of time (section 10.7).
Reading a listing
musc compile/musc debug (Chapters 10 and 14) print every instruction as OFFSET LINE MNEMONIC OPERAND, jump operands resolved to an absolute target rather than the raw signed delta the bytecode actually stores, and GET_FIELD/MAKE_STRUCT's own operands resolved to the name (and, for MAKE_STRUCT, the full field list) they refer to:
0000 0003 CONSTANT 0000 '5'
0002 | DEFINE_GLOBAL 0001
0004 0004 GET_GLOBAL 0002
0006 | CONSTANT 0003 '10'
0008 | LESS
0009 | JUMP_IF_FALSE 000011 -> 0023
0004 | MAKE_STRUCT 0000 'Vec2' fields=["x", "y"]
0018 | GET_FIELD 0009 'pos'
0000/0002/... is the byte offset; 0003/| is the source line (a bare | repeats the previous instruction's own line, section 14.1); CONSTANT 0000 '5' shows both the raw constant- pool index and the value it resolves to; JUMP_IF_FALSE 000011 -> 0023 shows both the raw signed offset the bytecode stores and the absolute byte offset it lands on; MAKE_STRUCT's line names the struct type and its full field list, and GET_FIELD's names the field, not just a bare index into the constant pool either would otherwise leave the reader to look up by hand.