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

Bytecode Format

Instruction encoding

Lumen’s bytecode is not byte-packed — it’s a flat std::vector<int>. Each instruction is one or more ints: an opcode, followed by however many operand ints that opcode needs. There’s no length prefix per instruction; the VM knows how many operands to consume from the opcode alone, via getOpCodeOffset().

For the full opcode table, see Opcode Reference.

The .bin container

Compiled programs are written to disk with BinaryProgram::save() (src/programfile.cpp) in a small custom binary format:

FieldTypeDescription
Signature2 bytes0xFE 0xFA — validated on load, load fails if it doesn’t match
Bytecode lengthint32Number of ints in the bytecode stream
Bytecodeint32[length]The instruction stream itself
String pool sizeint32Number of pooled string literals
String pool entriesrepeated {int32 len, char[len]}Each string literal, length-prefixed, not null-terminated
Variable countint32Number of variable slots to allocate at VM startup

This is the entire file — no header versioning, no section table. It’s deliberately minimal.

The string pool

Every string literal in the source is deduplicated into a single pool during compilation (resolveString() in src/helpers.cpp); a PUSH instruction for a string operand carries an index into this pool rather than embedding the text inline in the bytecode stream.

Variables are slots, not names

By the time a program reaches the .bin file, variable names are gone — the compiler maps each name to an integer slot index (resolveVariableIndex()), and the VM allocates a flat std::vector<Variant> of that size at startup. This is why a .bin file alone is unreadable to a human: --disassemble will show you variable index 3, not the original name. To get names back, you need the separate debug symbols file — see Debug Symbols.

Values on the stack: Variant

Every value that moves through the VM — on the stack or in a variable slot — is a tagged union (include/types.h):

typedef enum {
    TAG_INT = 2,
    TAG_FLOAT = 3,
    TAG_STRING = 1
} TypeTag;

typedef struct {
    TypeTag type;
    std::variant<int64_t, double, std::string> data;
} Variant;

In practice, today’s arithmetic and comparison opcodes operate on int64_t exclusively (see Opcode Reference for the exact behavior) — TAG_FLOAT is defined in the type system but not yet exercised by the current instruction set.