Compilation
The compiler walks the AST and emits bytecode into function objects. A module owns a runtime constant pool, and the first constant is the top-level function object that runs the program's top-level statements.
Compiler
Compilation begins by parsing source into the persistent top-level AST. The compiler clears the current top-level code object, compiles any newly parsed top-level statements, and appends hlt. In the interactive prompt, the same module, VM, compiler, and top-level AST are reused so earlier declarations remain available.
The compiler tracks a current function and an estimated stack count while emitting bytecode. When stack depth grows beyond a function's recorded maximum, the function's maxStackCount is updated. The VM uses that maximum before entering a function to catch stack overflow early.
Constant Pool
The module constant pool is a dynamic array of runtime values. Constant 0 is always the top-level function object. Each compiled function definition creates a new function object and stores a pointer to it in the constant pool. Integer literals that do not fit in the compact immediate encodings are also stored in the constant pool and loaded with ldc.
The constant pool is runtime data, not source syntax. Disassembled bytecode may show ldc or call with a numeric index, and that index refers to an entry in the module's constant array for the current compiled module.
The constant pool mixes different Value payloads. Function entries are pointer values that point to function objects. Large integer entries are integer values. The VM knows how to interpret an entry from the instruction that loaded it: call treats the indexed constant as a function pointer, while ldc pushes the indexed value onto the stack.
For example, a source file with one top-level program, one function definition, and a large integer literal may produce a constant pool shaped like this:
| Index | Stored value | How bytecode uses it |
|---|---|---|
0 |
Pointer to the top-level function object | Loaded by the VM before execution starts. |
1 |
Pointer to the user-defined function object | Referenced by call 1. |
2 |
Large integer value | Referenced by ldc 2. |
Function objects contain their own bytecode, parameter count, local count, and maximum stack count. The constant pool owns the function object references for the module, while each function object owns the bytes that the VM executes for that function.
Bytecode
Bytecode is stored as a growable array of bytes on each function object. Instructions are one byte, followed by zero, one, or two operand bytes depending on the opcode. Multi-byte immediates are written big-endian by the compiler and read back the same way by the VM and disassembler.
Small integer constants use compact push instructions. Values 0 through 3 use push_0 through push_3, signed 8-bit immediates use push, signed 16-bit immediates use push, and larger integer literals use ldc to load from the constant pool.
Generated Bytecode
matchbox -d file.mb compiles a file and prints generated bytecode instead of running it. The disassembler prints one mnemonic per line and prints immediates beside instructions such as ldc, ldg, stg, ldl, stl, reqs, branch instructions, and call.
Generated bytecode is useful for understanding what the compiler emitted, but it is not a stable interchange format in 0.2. It reflects current compiler implementation choices such as specialized local load/store forms and compact push forms.
Instructions
| Mnemonic | Purpose |
|---|---|
hlt |
Halt top-level execution. |
reqs imm8 |
Request a built-in service by service opcode. |
ldc imm8 |
Load a value from the module constant pool. |
reg |
Register the top stack value as a new global. |
ldg imm8 |
Load a global by index. |
stg imm8 |
Store a global by index. |
ldl imm8, ldl_0, ldl_1, ldl_2, ldl_3 |
Load a local or frame-relative value. |
stl imm8, stl_0, stl_1, stl_2, stl_3 |
Store a local or frame-relative value. |
push imm8, push imm16, push_0, push_1, push_2, push_3 |
Push an integer literal. |
pop |
Remove the top stack value. |
dup |
Duplicate the top stack value. |
inc, dec |
Increment or decrement the top stack value. |
add, sub, mul, div, rem, pow |
Apply integer arithmetic to the top two stack values. |
band, bor, bxor, bnot |
Apply bitwise operations; bnot is bitwise complement. |
lsl, lsr, asr |
Shift integer values. |
not, neg |
Apply logical NOT or numeric negation to the top stack value. |
beq imm16, blt imm16, ble imm16, jmp imm16 |
Branch relative to the current instruction pointer. |
call imm16 |
Call a function by constant-pool index. |
ret |
Return without an explicit value; the VM pushes 0. |
retv |
Return with the top stack value. |
Some opcodes are present in the VM before the source language emits them. For example, branch instructions exist in bytecode, but 0.2 does not yet implement user-facing control flow.
AST to Bytecode
Integer literals emit push or ldc. Binary expressions compile the left expression, then the right expression, then the arithmetic or bitwise instruction. Prefix expressions compile the operand and emit not, bnot, or neg. Variable reads emit ldg or ldl, while assignments emit the right-hand expression and then stg or stl.
Function definitions compile into separate function objects. Function calls compile arguments left to right and then emit call with the callee's constant-pool position. Built-in functions are represented as service requests and emit reqs.