Akron VM & Bytecode
Register-based general-purpose bytecode VM: opcodes, value encoding, and binary format.
Akron is Achronyme’s general-purpose bytecode VM — a register-based interpreter that runs .achb files produced by the bytecode compiler. It executes the Achronyme language at runtime (ach run) and evaluates prove {} blocks in VM mode.
Akron is paired with Artik, the dedicated witness-computation VM. The split is deliberate: Akron handles scripting and mutable heap state with a GC, while Artik handles side-effect-free witness math with no heap or GC.
Architecture
Source (.ach)
|
v
Bytecode Compiler -> Function prototypes + bytecode
|
v
Serializer -> .achb binary file
|
v
Loader -> Akron heap + stack + frames
|
v
Interpreter -> Register-based execution
VM Structure
Akron {
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>, // built-in functions
open_upvalues: linked list, // captured stack variables
stress_mode: bool, // force GC every cycle
}
Call Frames
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].
Value Representation
Values are tagged 64-bit integers — no boxing for common types:
Bits 63..60 = 4-bit tag
Bits 59..0 = 60-bit payload
Tags
| Tag | Name | Payload |
|---|---|---|
| 0 | INT | i60 signed integer (inline) |
| 1 | NIL | - |
| 2 | FALSE | - |
| 3 | TRUE | - |
| 4 | STRING | u32 handle -> strings arena |
| 5 | LIST | u32 handle -> lists arena |
| 6 | MAP | u32 handle -> maps arena |
| 7 | FUNCTION | u32 handle -> functions arena |
| 8 | FIELD | u32 handle -> fields arena |
| 9 | PROOF | u32 handle -> proofs arena |
| 10 | NATIVE | u32 handle -> natives table |
| 11 | CLOSURE | u32 handle -> closures arena |
| 12 | ITER | u32 handle -> iterators arena |
| 14 | BYTES | u32 handle -> bytes arena (ProveIR constants) |
| 15 | CIRCOM_HANDLE | u32 handle -> circom templates |
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.
Instruction Encoding
Each instruction is a u32 in one of two formats:
ABC Format
[opcode:8][A:8][B:8][C:8]
Used for 3-operand instructions like Add R[A] = R[B] + R[C].
ABx Format
[opcode:8][A:8][Bx:16]
Used for instructions with a 16-bit operand, like LoadConst R[A] = K[Bx] or Jump IP = Bx.
Opcodes
Constants & Moves
| Opcode | Code | Format | Description |
|---|---|---|---|
LoadConst | 0 | ABx | R[A] = K[Bx] — load from constant pool |
LoadTrue | 1 | A | R[A] = true |
LoadFalse | 2 | A | R[A] = false |
LoadNil | 3 | A | R[A] = nil |
Move | 5 | AB | R[A] = R[B] |
Arithmetic
| Opcode | Code | Format | Description |
|---|---|---|---|
Add | 10 | ABC | R[A] = R[B] + R[C] |
Sub | 11 | ABC | R[A] = R[B] - R[C] |
Mul | 12 | ABC | R[A] = R[B] * R[C] |
Div | 13 | ABC | R[A] = R[B] / R[C] |
Mod | 14 | ABC | R[A] = R[B] % R[C] |
Pow | 15 | ABC | R[A] = R[B] ^ R[C] |
Neg | 16 | AB | R[A] = -R[B] |
Comparison & Logic
| Opcode | Code | Format | Description |
|---|---|---|---|
Eq | 20 | ABC | R[A] = R[B] == R[C] |
Lt | 21 | ABC | R[A] = R[B] < R[C] |
Gt | 22 | ABC | R[A] = R[B] > R[C] |
NotEq | 23 | ABC | R[A] = R[B] != R[C] |
Le | 24 | ABC | R[A] = R[B] <= R[C] |
Ge | 25 | ABC | R[A] = R[B] >= R[C] |
LogNot | 26 | AB | R[A] = !R[B] |
Closures & Upvalues
| Opcode | Code | Format | Description |
|---|---|---|---|
GetUpvalue | 34 | AB | R[A] = Upvalue[B] |
SetUpvalue | 35 | AB | Upvalue[B] = R[A] |
CloseUpvalue | 36 | A | Close upvalue at stack slot A |
Functions
| Opcode | Code | Format | Description |
|---|---|---|---|
Return | 54 | A | Return R[A] from current frame |
Call | 55 | ABC | R[A] = Call(R[B], R[B+1]..R[B+C-1]) |
Closure | 56 | ABx | R[A] = Closure(K[Bx]) |
Control Flow
| Opcode | Code | Format | Description |
|---|---|---|---|
Jump | 60 | Bx | IP = Bx |
JumpIfFalse | 61 | ABx | If !R[A] then IP = Bx |
GetIter | 65 | AB | R[A] = Iterator(R[B]) |
ForIter | 66 | ABx | Next item or jump to Bx |
Globals
| Opcode | Code | Format | Description |
|---|---|---|---|
DefGlobalVar | 98 | ABx | Define mutable global |
DefGlobalLet | 99 | ABx | Define immutable global |
GetGlobal | 100 | ABx | R[A] = Global[K[Bx]] |
SetGlobal | 101 | ABx | Global[K[Bx]] = R[A] |
Print | 102 | A | Print R[A] |
Data Structures
| Opcode | Code | Format | Description |
|---|---|---|---|
BuildList | 150 | ABC | R[A] = [R[B]..R[B+C-1]] |
BuildMap | 151 | ABC | R[A] = {R[B]:R[B+1], ...} |
GetIndex | 152 | ABC | R[A] = R[B][R[C]] |
SetIndex | 153 | ABC | R[A][R[B]] = R[C] |
ZK & Dispatch
| Opcode | Code | Format | Description |
|---|---|---|---|
Prove | 160 | - | Compile + verify ZK circuit |
MethodCall | 161 | ABC | Method dispatch R[A] = R[B].method(args) |
CallCircomTemplate | 162 | ABC | Invoke a registered Circom template handle |
Special
| Opcode | Code | Description |
|---|---|---|
Nop | 255 | No operation |
Function Objects
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.
Binary Format (.achb)
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 x u64 LE (Montgomery limbs)
BYTES (14): u32 LE length + raw bytes (ProveIR blobs)
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 Akron’s loader module, which deserializes into heap objects.
Global Variable Layout
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).
Relationship to Artik
Akron and Artik are sibling VMs with disjoint roles:
| Akron | Artik |
|---|---|
General-purpose scripting + prove {} | Witness computation for circuits |
| Tagged values, 13+ type tags | Field elements + fixed-width ints only |
| Mark-sweep GC, typed arenas | No heap, no GC |
| ~40 opcodes, 3-operand format | ~25 opcodes, typed registers |
Invoked by ach run, VM mode | Dispatched from R1CS witness-gen (WitnessOp::ArtikCall) |
See Artik Witness VM for details on the witness path.
Source Files
| Component | File |
|---|---|
| Opcodes | akron/src/opcode.rs |
| VM interpreter | akron/src/machine/vm.rs |
| Call frames | akron/src/machine/frame.rs |
| Interpreter loop | akron/src/machine/interpreter.rs |
| Native dispatch | akron/src/specs.rs |
| Prototype methods | akron/src/machine/methods/ |
| Value encoding | memory/src/value.rs |
| Function struct | memory/src/heap.rs |
| Bytecode compiler | akronc/src/codegen.rs |
| Function compiler | akronc/src/function_compiler.rs |
| Binary serializer | cli/src/commands/compile.rs |
| Binary loader | akron/src/loader.rs |