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
| State | Type | Purpose |
|---|---|---|
PC | int | Program counter — index into the bytecode stream |
stack | std::vector<Variant> | Operand stack — values being computed with |
variables | std::vector<Variant> | Flat variable storage, one slot per variable, sized from the compiled variable count |
pcStack | std::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 construct | Bytecode behavior |
|---|---|
x = expr | Evaluate expr by pushing operands and applying arithmetic opcodes (ADD/SUB/…), then pop the result into variables[x] |
println / print / inputInt / inputStr | EXEC (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 / jump | label is purely a compile-time bookmark; jump compiles to JUMP (0x05), an unconditional PC reassignment |
routine / call / endroutine | call 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 throughexecute()one instruction at a time and inspects the samestack/variables/PCstate 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.