Bytecode VM Design (Register-Based)
Pleno will use a register-based bytecode VM from the start. This choice is performance-oriented and aligns with the goal of being faster than Python while remaining dynamic. A register VM yields fewer instructions, fewer memory push/pop operations, and a cleaner path to JIT later.
High-Level Pipeline
Source (.pln)
-> Lexer -> Parser -> AST
-> Compiler -> Bytecode (Chunk)
-> VM -> ExecuteInstruction Set (v0)
Design goals:
- Keep opcodes minimal and orthogonal.
- Use fixed-width instruction encoding for fast decoding.
- Keep register count bounded per function for locality.
Encoding
Each instruction is 4 bytes:
[Opcode: u8][A: u8][B: u8][C: u8]Ais destination register.B,Care source registers or immediate indices (depending on opcode).
Core Opcodes
Register and Constants
Move(R[A] = R[B])LoadConst(R[A] = Constants[B])LoadNull(R[A] = null)LoadTrue(R[A] = true)LoadFalse(R[A] = false)
Arithmetic
AddSubMulDivModNeg
Comparison
EqNeLtLeGtGe
Logical
NotAndOr(compiled via jumps for short-circuit)
Control Flow
Jump offsetJumpIf offset(if R[A] then PC += offset)JumpIfNot offset(if !R[A] then PC += offset)
Variables
GetGlobal(R[A] = Globals[Constants[B]])SetGlobal(Globals[Constants[B]] = R[A])
Calls
Call (R[A] = R[B](R[C]...R[C+N]))Return(return R[A])Closure(R[A] = closure(proto[B]))
Objects and Collections
GetProperty(R[A] = R[B][Constants[C]])SetProperty(R[B][Constants[C]] = R[A])GetIndex(R[A] = R[B][R[C]])SetIndex(R[B][R[C]] = R[A])NewArray(R[A] = [])NewMap(R[A] = {})ArrayAppend(R[A].push(R[B]))
Advanced
Import(R[A] = import(Constants[B]))Throw(throw R[A])TypeOf(R[A] = typeof(R[B]))Is(R[A] = R[B] is Type[C])
Fast Paths (Optional v0.1)
LoadInt(R[A] = smallint(B))AddInt(R[A] = R[B] + immediate)SubInt(R[A] = R[B] - immediate)
Bytecode Structures
Chunk.code: Vec<Instruction>Chunk.constants: Vec<Value>Chunk.prototypes: Vec<Function>Chunk.lines: Vec<u32>Chunk.max_registers: usize
Compiler Guide
Register Allocation
- Per function, allocate registers for locals and temporaries.
- Keep registers small (<= 256) for compact encoding.
- Free temporaries after use to reduce pressure.
Expressions
- Literal:
LoadConstinto a fresh register. - Binary: compile left into R1, right into R2, emit opcode into R3.
- Unary: compile operand into R1, emit opcode into R2.
Variables
let x = expr-> compile expr into Rn, bind local to Rn.x = expr-> compile expr into Rn, emit store to local/global slot.
If/Else
- Compile condition into Rn.
JumpIfNotto else label.- Compile then block.
Jumpto end label.- Patch else label, compile else block.
While / For
- Track loop start and end labels.
JumpIfNotexits loop.Jump/Loop(negative offset) for continue.
Functions + Calls
- Each function produces a
Chunkwith its own register file size. Calluses a base register and contiguous args.
Closures
- Capture locals into upvalues in the compiler.
- Emit
Closurewith upvalue descriptors. - Runtime resolves captures to shared cells.
OOP
classcompiles to class object in constants.obj.field->GetProperty name_idx.obj.field = expr-> compile expr,SetProperty.obj.method(a,b)->Invoke(optional), orGetProperty+Call.
Pattern Matching
Compile match to a cascade of tests and jumps.
- Evaluate match value once into a temp register.
- For each case: emit test,
JumpIfNotto next. - Guard expressions compiled into boolean checks.
- Default case is the final fall-through.
VM Implementation Guide
VM State
registers: Vec<Value>frames: Vec<CallFrame>globals: HashMap<String, Value>
Call Frame
closure: Rc<Closure>ip: usizebase: usize(register base for frame)
Execution Loop
- Fetch
Instructionatipand increment. - Decode
opcode, a, b, c. - Dispatch via
match. - For errors, return
RuntimeError(no panics).
Value Model (Dynamic)
Start with a tagged enum; move to NaN boxing later if needed.
Value::Null | Bool | Int | Float | String | Array | Map | Set | Function | Closure | Object | ClassGarbage Collection Design
v0: Tracing mark-sweep
- Heap stores arrays, maps, strings, functions, objects.
- Roots:
- Register file
- Globals
- Call frames (closures, upvalues)
- Mark: traverse references and mark reachable.
- Sweep: free unmarked objects.
Future: Generational GC
- Young gen for short-lived values.
- Old gen for long-lived values.
- Requires write barriers.
Why Register VM (Best Now)
- Fewer instructions per program.
- Better cache locality vs. stack VM.
- Easier path to JIT and inline caching.
We can start with register VM and still add optimizations incrementally:
- Inline caches for property access
- NaN boxing for compact values
- Specialized opcodes for common fast paths