Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Virtual Machine

The Lumen VM (src/vm.cpp) is a stack machine: instructions operate on an operand stack rather than registers. It’s a straightforward fetch-decode-execute loop with three pieces of state.

Execution state

StateTypePurpose
PCintProgram counter — index into the bytecode stream
stackstd::vector<Variant>Operand stack — values being computed with
variablesstd::vector<Variant>Flat variable storage, one slot per variable, sized from the compiled variable count
pcStackstd::vector<int>Return-address stack, used for routine calls

The main loop

while(true) {
    auto opcode = bytecode[PC];
    int result = execute(bytecode, stringPool, variables, stack, pcStack, PC, halt);
    if(halt) break;
    PC = result;
}

execute() decodes a single instruction, performs its effect, and returns the next PC value. Most instructions simply return PC + offset (fall through to the next instruction); jumps, conditional jumps, calls, and returns instead return an arbitrary target address, which is how control flow works — there’s no separate branch-prediction or block structure at runtime, just PC reassignment.

How each construct compiles down

Language constructBytecode behavior
x = exprEvaluate expr by pushing operands and applying arithmetic opcodes (ADD/SUB/…), then pop the result into variables[x]
println / print / inputInt / inputStrEXEC (0x04) dispatches through a native function table — see Built-in Functions
if <a> <cmp> <b>Push a and b, then a comparison opcode (JEQ/JGR/…) that pops both and jumps past the block if the comparison is false
label / jumplabel is purely a compile-time bookmark; jump compiles to JUMP (0x05), an unconditional PC reassignment
routine / call / endroutinecall compiles to CALL (0x01), which pushes a return address onto pcStack and jumps to the routine’s offset; endroutine compiles to RET (0xFE), which pops pcStack and jumps back
.. (string concat)JOIN (0xAA) pops a count and that many strings off the stack, concatenates them, and pushes the result

Native functions

print, println, inputInt, and inputStr aren’t opcodes of their own — they’re entries in funcMap (src/vmfuncmap.cpp), a lookup table from a small integer index to a C++ lambda. The EXEC opcode (0x04) just looks up the index and calls the corresponding lambda with the current stack and variable storage. This is a deliberately extensible design: adding a new built-in function means adding one entry to funcMap plus a matching entry to the compiler’s funcList, without touching the opcode set at all.

Halting

Execution stops when the VM reaches HLT (0xFF), which the compiler appends to the end of the main bytecode stream after every line of source has been compiled.

Debug and disassembly variants

src/vm.cpp implements the plain execution loop described above. Two related tools reuse the same execute() function but wrap it differently:

  • run_debug() (src/debugvm.cpp) — the interactive debugger, which steps through execute() one instruction at a time and inspects the same stack/variables/PC state between steps.
  • disassemble() (src/disassembler.cpp) — doesn’t execute anything; it statically walks the bytecode stream opcode by opcode and prints a human-readable listing. See Disassembler.