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:
| Field | Type | Description |
|---|---|---|
| Signature | 2 bytes | 0xFE 0xFA — validated on load, load fails if it doesn’t match |
| Bytecode length | int32 | Number of ints in the bytecode stream |
| Bytecode | int32[length] | The instruction stream itself |
| String pool size | int32 | Number of pooled string literals |
| String pool entries | repeated {int32 len, char[len]} | Each string literal, length-prefixed, not null-terminated |
| Variable count | int32 | Number 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.