Skip to content

VM & Bytecode

The Achronyme VM is a register-based bytecode interpreter. It executes compiled .achb files using a fixed-size stack of 65,536 Value slots.

Source (.ach)
Bytecode Compiler → Function prototypes + bytecode
Serializer → .achb binary file
Loader → VM heap + stack + frames
Interpreter → Register-based execution
VM {
heap: Heap, // typed arenas + GC
stack: [Value; 65536], // fixed-size register stack
frames: Vec<CallFrame>, // call stack
globals: Vec<GlobalEntry>, // global variables
natives: Vec<NativeObj>, // 43 built-in functions
open_upvalues: linked list, // captured stack variables
stress_mode: bool, // force GC every cycle
}

Each function call pushes a CallFrame:

CallFrame {
closure: u32, // handle to Closure on heap
ip: usize, // instruction pointer
base: usize, // base offset in stack
dest_reg: usize, // where to store return value
}

Register R[i] in the current frame maps to stack[frame.base + i].

Values are tagged 64-bit integers — no boxing for common types:

Bits 63..60 = 4-bit tag
Bits 59..0 = 60-bit payload
TagNamePayload
0INTi60 signed integer (inline)
1NIL
2FALSE
3TRUE
4STRINGu32 handle → strings arena
5LISTu32 handle → lists arena
6MAPu32 handle → maps arena
7FUNCTIONu32 handle → functions arena
8FIELDu32 handle → fields arena (BN254)
9PROOFu32 handle → proofs arena
10NATIVEu32 handle → natives table
11CLOSUREu32 handle → closures arena
12ITERu32 handle → iterators arena

Integers (tag 0) are the most common value type — using tag 0 means no masking is needed for the common case.

Integer range: -2^59 to 2^59 - 1 (576,460,752,303,423,487). Overflow raises IntegerOverflow.

Each instruction is a u32 in one of two formats:

[opcode:8][A:8][B:8][C:8]

Used for 3-operand instructions like Add R[A] = R[B] + R[C].

[opcode:8][A:8][Bx:16]

Used for instructions with a 16-bit operand, like LoadConst R[A] = K[Bx] or Jump IP = Bx.

OpcodeCodeFormatDescription
LoadConst0ABxR[A] = K[Bx] — load from constant pool
LoadTrue1AR[A] = true
LoadFalse2AR[A] = false
LoadNil3AR[A] = nil
Move5ABR[A] = R[B]
OpcodeCodeFormatDescription
Add10ABCR[A] = R[B] + R[C]
Sub11ABCR[A] = R[B] - R[C]
Mul12ABCR[A] = R[B] * R[C]
Div13ABCR[A] = R[B] / R[C]
Mod14ABCR[A] = R[B] % R[C]
Pow15ABCR[A] = R[B] ^ R[C]
Neg16ABR[A] = -R[B]
OpcodeCodeFormatDescription
Eq20ABCR[A] = R[B] == R[C]
Lt21ABCR[A] = R[B] < R[C]
Gt22ABCR[A] = R[B] > R[C]
NotEq23ABCR[A] = R[B] != R[C]
Le24ABCR[A] = R[B] <= R[C]
Ge25ABCR[A] = R[B] >= R[C]
LogNot26ABR[A] = !R[B]
OpcodeCodeFormatDescription
GetUpvalue34ABR[A] = Upvalue[B]
SetUpvalue35ABUpvalue[B] = R[A]
CloseUpvalue36AClose upvalue at stack slot A
OpcodeCodeFormatDescription
Return54AReturn R[A] from current frame
Call55ABCR[A] = Call(R[B], R[B+1]..R[B+C-1])
Closure56ABxR[A] = Closure(K[Bx])
OpcodeCodeFormatDescription
Jump60BxIP = Bx
JumpIfFalse61ABxIf !R[A] then IP = Bx
GetIter65ABR[A] = Iterator(R[B])
ForIter66ABxNext item or jump to Bx
OpcodeCodeFormatDescription
DefGlobalVar98ABxDefine mutable global
DefGlobalLet99ABxDefine immutable global
GetGlobal100ABxR[A] = Global[K[Bx]]
SetGlobal101ABxGlobal[K[Bx]] = R[A]
Print102APrint R[A]
OpcodeCodeFormatDescription
BuildList150ABCR[A] = [R[B]..R[B+C-1]]
BuildMap151ABCR[A] = {R[B]:R[B+1], ...}
GetIndex152ABCR[A] = R[B][R[C]]
SetIndex153ABCR[A][R[B]] = R[C]
OpcodeCodeFormatDescription
Prove160Compile + verify ZK circuit
OpcodeCodeDescription
Nop255No operation

Each compiled function produces a Function struct on the heap:

Function {
name: String, // for debugging
arity: u8, // parameter count
max_slots: u16, // peak register usage
chunk: Vec<u32>, // bytecode instructions
constants: Vec<Value>, // constant pool
upvalue_info: Vec<u8>, // [is_local, index] pairs
line_info: Vec<u32>, // line number per instruction
}

The line_info vector is parallel to chunk — each instruction has a source line number for error reporting.

The .achb file format (version 9):

Magic: b"ACH\x09" (4 bytes)
Metadata: max_slots (u16 LE)
Global Strings:
count (u32 LE)
for each: length (u32 LE) + UTF-8 bytes
Global Constants:
count (u32 LE)
for each: tag (u8) + payload
INT (0): i64 LE
STRING (1): u32 LE handle
FIELD (8): 4 × u64 LE (Montgomery limbs)
NIL (255): (no payload)
Prototypes:
count (u32 LE)
for each:
name_len (u32 LE) + name bytes
arity (u8)
max_slots (u16 LE)
const_count (u32 LE) + constants
upvalue_count (u32 LE) + upvalue info
bytecode_len (u32 LE) + instructions (u32 LE each)
Main Bytecode:
instruction_count (u32 LE)
instructions (u32 LE each)

The format is compatible with the VM’s loader module, which deserializes into heap objects.

Index: 0..22 23..
Natives User globals

The first 23 slots (0–22) are reserved for native functions. User-defined globals start at index 23 (USER_GLOBAL_START).

ComponentFile
Opcodesvm/src/opcode.rs
VM interpretervm/src/machine/vm.rs
Call framesvm/src/machine/frame.rs
Native dispatchvm/src/specs.rs
Value encodingmemory/src/value.rs
Function structmemory/src/heap.rs
Bytecode compilercompiler/src/codegen.rs
Function compilercompiler/src/function_compiler.rs
Binary serializercli/src/commands/compile.rs
Binary loadervm/src/loader.rs