Odin Speaks Odin ยท Appendix B

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.

MnemonicOperandStack effectSemanticsIntroduced
CONSTANTu8 idx-> valuepush chunk.constants[idx] (also how an enum member's resolved tag is pushed, Ch. 11)Ch. 10
NIL--> nilpush the nil valueCh. 10
TRUE--> truepush the boolean trueCh. 10
FALSE--> falsepush the boolean falseCh. 10
POP-value ->discard the top of the stackCh. 10
DUP-value -> value valueduplicate the top of the stackCh. 10
ADD-a b -> (a+b)numeric add, or string concatCh. 11
SUB-a b -> (a-b)numeric subtractCh. 11
MUL-a b -> (a*b)numeric multiplyCh. 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 zeroAppx. G
NEGATE-a -> (-a)numeric negationCh. 11
NOT-a -> (!truthy(a))logical notCh. 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 inequalityCh. 11
LESS-a b -> (a<b)numeric less-thanCh. 11
LESS_EQUAL-a b -> (a<=b)numeric less-or-equalCh. 11
GREATER-a b -> (a>b)numeric greater-thanCh. 11
GREATER_EQUAL-a b -> (a>=b)numeric greater-or-equalCh. 11
GET_LOCALu8 slot-> valuepush the current frame's local slotCh. 11
SET_LOCALu8 slotvalue -> valueoverwrite a local slot, leave valueCh. 11
GET_GLOBALu8 name idx-> valuepush the named global's value; .Error if undefined (Ch. 18); also how alias.function (Ch. 20) resolvesCh. 11
SET_GLOBALu8 name idxvalue -> valueoverwrite a global, leave valueCh. 11
DEFINE_GLOBALu8 name idxvalue ->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
JUMPi16 offset-ip += offset, unconditionalCh. 11
JUMP_IF_FALSEi16 offsetvalue -> valueif !truthy(value), ip += offset; peeks, never popsCh. 11
CALLu8 argcf a1..aN -> resultcall 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 callerCh. 12
MAKE_STRUCTu8 layout idxv1..vN -> instancebuild a struct from a Struct_Layout constant, scattering pushed values to their resolved field indices; .Error on arena exhaustion (Ch. 18)Ch. 13
GET_FIELDu8 name idxobj -> valueread a struct field by name, scanning the instance's own field-name table; .Error on a non-struct or unknown fieldCh. 13
SET_FIELDu8 name idxobj value -> valuewrite a struct field by name, same lookup as GET_FIELDCh. 13
MAKE_ARRAYu8 countv1..vN -> arraybuild a fixed-capacity array of count elements; .Error on arena exhaustion (Ch. 18)Ch. 13
GET_INDEX-arr idx -> valuebounds-checked array read; .Error on a non-array or out-of-range indexCh. 13
SET_INDEX-arr idx value -> valuebounds-checked array write, same checks as GET_INDEXCh. 13
YIELD--suspend the VM; resumes at the very next instructionCh. 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's Struct_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_jump writes a forward one once the jump target is finally known (section 11.3).
  • GET_INDEX/SET_INDEX take 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.