//! The cube VM: a safe, deterministic, stack-based interpreter whose code //! lives as CUBELinux records and whose call graph is the cube's association //! graph (PDF Package 4). //! //! Why a bytecode VM and not a JIT/translator: the PDF's use cases are a //! "scripting language or DSL whose runtime walks cube links to load and //! dispatch functions" and an "AI experimentation stack that stores every //! model version, metric, and run as linked CZYX records." Both are about //! *addressable, introspectable code as data* — exactly what a record-addressed //! VM gives you. Executing raw foreign machine code in user space on this //! hardware is neither safe nor necessary to demonstrate the design; the //! body is always decoded through [`crate::opcode`] first, so the cube never //! executes something that didn't survive the codec. //! //! The interpreter is deterministic and free of `unsafe` and floating point, //! so two runs of the same program against the same store always yield the //! same result — a prerequisite for the PDF's "navigate and reconstruct //! experiments" goal. use crate::opcode::{decode, Op}; use cubecoords::{CubeHeader, Czyx}; use cubestore::{CubeBackend, CubeStore}; /// Number of local variable slots per VM frame. A free const (not tied to /// the generic `B`) so the per-frame array can be sized in a const context. /// Kept equal to [`Vm::MAX_LOCALS`]. const LOCALS: usize = 16; /// /// Decision: a small, explicit set. `0` is reserved as a no-op probe so /// host linkage is testable without side effects; the rest exercise the /// store (load/store adjacent records) and the data stack, which is what a /// real kernel layer would do. Kept `pub` so a host embedding the VM can /// match on the same ids. pub const SYS_NOP: u8 = 0; /// Push the number of `linked_records` (call-graph degree) of the current /// cell onto the data stack. Lets a program introspect its own graph. pub const SYS_DEGREE: u8 = 1; /// Push `1` if record `i` in `linked_records` exists in the store, else `0`. pub const SYS_LINKED_EXISTS: u8 = 2; /// Print a newline-terminated diagnostic built from the top `n` stack bytes /// (popped) to a side channel. Maps to a kernel-style `write` without /// touching the host process's stdio directly (collected in `Vm::output`). pub const SYS_TRACE: u8 = 3; /// Result of running the VM to completion (or a fault). #[derive(Debug, PartialEq, Eq)] pub enum RunResult { /// Program halted normally. Halted { /// The value left on the data stack (the returned result), or `None` /// if the stack is empty. top: Option, }, /// A fault occurred. Fault { /// The cell where the fault happened. cell: Czyx, /// The instruction index within that cell's decoded code. pc: usize, /// Why execution stopped. reason: Fault, }, } /// Why execution stopped. #[derive(Debug, PartialEq, Eq)] pub enum Fault { /// `CALL_LINK n` referenced a link index past `linked_records.len()`. BadLink(u8), /// The linked record does not exist in the store. MissingCallee(Czyx), /// The callee's body was not valid bytecode. BadCalleeBytecode(Czyx), /// The call stack exceeded [`Vm::MAX_CALL_DEPTH`]. CallStackOverflow, /// Division or modulo by zero. DivByZero, /// `pc` ran past the end of the instruction stream without `RET`/`HALT`. PcOutOfRange, /// A `LOAD`/`STORE` used a local slot >= [`Vm::MAX_LOCALS`]. BadLocal(u8), /// `SYSCALL id` had an unknown id. UnknownSyscall(u8), } /// The cube VM. pub struct Vm { store: CubeStore, /// Diagnostic output accumulated from `SYSCALL 3`. output: Vec, /// Maximum nesting of `CALL_LINK` frames. max_call_depth: usize, } impl Vm { /// Hard ceiling on call nesting (defends against infinite cube loops). pub const MAX_CALL_DEPTH: usize = 64; /// Number of local slots per frame. pub const MAX_LOCALS: usize = 16; /// Wrap a store. pub fn new(store: CubeStore) -> Self { Vm { store, output: Vec::new(), max_call_depth: Self::MAX_CALL_DEPTH, } } /// Diagnostic bytes emitted by `SYSCALL 3` during the last run. pub fn output(&self) -> &[u8] { &self.output } /// Set the call-depth ceiling (default [`Vm::MAX_CALL_DEPTH`]). pub fn set_max_call_depth(&mut self, d: usize) { self.max_call_depth = d; } /// Run the entry cell to completion, returning its result or a fault. /// /// `entry` must exist in the store and hold valid bytecode. Calls follow /// `linked_records` edges; recursion depth is bounded. All frames share a /// single data stack, so a caller passes arguments by leaving them on the /// stack and reads a callee's result (which the callee leaves there) after /// `RET` returns. pub fn run(&mut self, entry: Czyx) -> RunResult { self.output.clear(); let mut stack: Vec = Vec::new(); match self.exec_cell(entry, 0, &mut stack) { Ok(v) => RunResult::Halted { top: v }, Err((cell, pc, fault)) => RunResult::Fault { cell, pc, reason: fault, }, } } /// Execute one cell. `depth` is the current call nesting. `stack` is the /// single shared data stack (callees run on it), so arguments and return /// values flow across cube edges via the stack. fn exec_cell( &mut self, label: Czyx, depth: usize, stack: &mut Vec, ) -> Result, (Czyx, usize, Fault)> { // Fetch and decode the callee record. let (header, body) = self.store .get_record(&label) .ok_or((label, 0, Fault::MissingCallee(label)))?; let code = decode(&body).map_err(|_| (label, 0, Fault::BadCalleeBytecode(label)))?; let mut locals: [u8; LOCALS] = [0; LOCALS]; let mut pc: usize = 0; while pc < code.len() { let op = code[pc]; match op { Op::Nop => {} Op::Halt => { return Ok(stack.pop()); } Op::Const(v) => stack.push(v), Op::Load(i) => { if (i as usize) >= Self::MAX_LOCALS { return Err((label, pc, Fault::BadLocal(i))); } stack.push(locals[i as usize]); } Op::Store(i) => { if (i as usize) >= Self::MAX_LOCALS { return Err((label, pc, Fault::BadLocal(i))); } locals[i as usize] = stack.pop().ok_or((label, pc, Fault::PcOutOfRange))?; } Op::Add | Op::Sub | Op::Mul | Op::Div | Op::Mod | Op::And | Op::Or | Op::Xor => { let (b, a) = (stack.pop(), stack.pop()); let (b, a) = ( b.ok_or((label, pc, Fault::PcOutOfRange))?, b2(a, label, pc)?, ); stack.push(bin_op(op, a, b).map_err(|f| (label, pc, f))?); } Op::Shl | Op::Shr => { let (b, a) = (stack.pop(), stack.pop()); let (b, a) = ( b.ok_or((label, pc, Fault::PcOutOfRange))?, b2(a, label, pc)?, ); stack.push(shift_op(op, a, b)); } Op::Eq | Op::Ne | Op::Lt | Op::Gt | Op::Le | Op::Ge => { let (b, a) = (stack.pop(), stack.pop()); let (b, a) = ( b.ok_or((label, pc, Fault::PcOutOfRange))?, b2(a, label, pc)?, ); stack.push(cmp_op(op, a, b)); } Op::Jmp(t) => { pc = t as usize; continue; } Op::Jz(t) => { let v = stack.pop().ok_or((label, pc, Fault::PcOutOfRange))?; if v == 0 { pc = t as usize; continue; } } Op::Jnz(t) => { let v = stack.pop().ok_or((label, pc, Fault::PcOutOfRange))?; if v != 0 { pc = t as usize; continue; } } Op::CallLink(n) => { if (n as usize) >= header.linked_records.len() { return Err((label, pc, Fault::BadLink(n))); } if depth + 1 > self.max_call_depth { return Err((label, pc, Fault::CallStackOverflow)); } let callee = header.linked_records[n as usize]; let r = self.exec_cell(callee, depth + 1, stack)?; // Callee leaves its result on the shared stack; propagate // it up so the caller can read it after RET returns. if let Some(v) = r { stack.push(v); } } Op::Ret => { return Ok(stack.pop()); } Op::Syscall(id) => { let v = self.syscall(id, &header, stack); if let Err(f) = v { return Err((label, pc, f)); } } Op::Dup => { let v = *stack.last().ok_or((label, pc, Fault::PcOutOfRange))?; stack.push(v); } Op::Drop => { stack.pop().ok_or((label, pc, Fault::PcOutOfRange))?; } } pc += 1; } // Ran off the end without HALT/RET: treat as a clean return of the // top value if any (lenient), but flag runaway programs via the // caller's depth limit instead. Ok(stack.pop()) } /// Host syscall dispatch. `stack` is the *shared* data stack (callees /// run on it) and `header` is the current cell's metadata. fn syscall(&mut self, id: u8, header: &CubeHeader, stack: &mut Vec) -> Result<(), Fault> { match id { SYS_NOP => {} SYS_DEGREE => { stack.push(header.linked_records.len() as u8); } SYS_LINKED_EXISTS => { let i = stack.pop().ok_or(Fault::PcOutOfRange)?; let exists = header .linked_records .get(i as usize) .map(|c| self.store.get_record(c).is_some()) .unwrap_or(false); stack.push(if exists { 1 } else { 0 }); } SYS_TRACE => { // Pop every byte currently on the stack and emit it as a // newline-terminated diagnostic line (low byte of each value). let mut line: Vec = std::mem::take(stack); line.push(b'\n'); self.output.extend_from_slice(&line); } other => return Err(Fault::UnknownSyscall(other)), } Ok(()) } } fn b2(v: Option, cell: Czyx, pc: usize) -> Result { v.ok_or((cell, pc, Fault::PcOutOfRange)) } fn bin_op(op: Op, a: u8, b: u8) -> Result { Ok(match op { Op::Add => a.wrapping_add(b), Op::Sub => a.wrapping_sub(b), Op::Mul => a.wrapping_mul(b), Op::Div => { if b == 0 { return Err(Fault::DivByZero); } a / b } Op::Mod => { if b == 0 { return Err(Fault::DivByZero); } a % b } Op::And => a & b, Op::Or => a | b, Op::Xor => a ^ b, _ => unreachable!("bin_op called on non-binary op"), }) } fn shift_op(op: Op, a: u8, b: u8) -> u8 { let s = (b & 7) as u32; match op { Op::Shl => a.wrapping_shl(s), _ => a.wrapping_shr(s), } } fn cmp_op(op: Op, a: u8, b: u8) -> u8 { let r = match op { Op::Eq => a == b, Op::Ne => a != b, Op::Lt => a < b, Op::Gt => a > b, Op::Le => a <= b, _ => a >= b, }; r as u8 } #[cfg(test)] mod tests { use super::*; use cubecoords::Czyx; use cubestore::HashBackend; fn cell_code( _label: Czyx, links: &[Czyx], ops: &[Op], doc_type: &str, ) -> (CubeHeader, Vec) { let mut h = CubeHeader::new(); h.title = Some("t".into()); h.doc_type = Some(doc_type.into()); h.linked_records = links.to_vec(); h.refresh_flags(); (h, crate::opcode::encode(ops)) } #[test] fn arithmetic_chain() { let mut store = CubeStore::new(HashBackend::new()); let entry = Czyx::new(1, 1, 1, 1); let (h, b) = cell_code( entry, &[], &[ Op::Const(3), Op::Const(4), Op::Add, // 7 Op::Const(2), Op::Mul, // 14 Op::Halt, ], "fn", ); store.put_record(entry, &h, &b); let mut vm = Vm::new(store); assert_eq!(vm.run(entry), RunResult::Halted { top: Some(14) }); } #[test] fn div_by_zero_faults() { let mut store = CubeStore::new(HashBackend::new()); let entry = Czyx::new(1, 1, 1, 1); let (h, b) = cell_code( entry, &[], &[Op::Const(5), Op::Const(0), Op::Div, Op::Halt], "fn", ); store.put_record(entry, &h, &b); let mut vm = Vm::new(store); assert_eq!( vm.run(entry), RunResult::Fault { cell: entry, pc: 2, reason: Fault::DivByZero } ); } #[test] fn call_link_follows_cube_edge() { let mut store = CubeStore::new(HashBackend::new()); let entry = Czyx::new(1, 1, 1, 1); let leaf = Czyx::new(2, 0, 0, 1); // leaf: push 9, return it let (lh, lb) = cell_code(leaf, &[], &[Op::Const(9), Op::Ret], "fn"); store.put_record(leaf, &lh, &lb); // entry: call link 0 (leaf), then add 1, halt -> 10 let (h, b) = cell_code( entry, &[leaf], &[Op::CallLink(0), Op::Const(1), Op::Add, Op::Halt], "fn", ); store.put_record(entry, &h, &b); let mut vm = Vm::new(store); assert_eq!(vm.run(entry), RunResult::Halted { top: Some(10) }); } #[test] fn shared_stack_passes_args_across_cube_edges() { // Locks in the calling convention: the caller leaves the argument on // the SHARED data stack; the callee pops it via Store into a local, // computes, and returns a value the caller then uses. Without a shared // stack this would fault (callee sees an empty frame stack). let mut store = CubeStore::new(HashBackend::new()); let entry = Czyx::new(1, 1, 1, 1); let double = Czyx::new(2, 0, 0, 1); // double: Store 0 (pop arg), Load 0, Const 2, Mul, Ret -> arg*2 let (dh, db) = cell_code( double, &[], &[Op::Store(0), Op::Load(0), Op::Const(2), Op::Mul, Op::Ret], "fn", ); store.put_record(double, &dh, &db); // entry: Const 21, CallLink 0 (double), Halt -> 42 let (h, b) = cell_code( entry, &[double], &[Op::Const(21), Op::CallLink(0), Op::Halt], "fn", ); store.put_record(entry, &h, &b); let mut vm = Vm::new(store); assert_eq!(vm.run(entry), RunResult::Halted { top: Some(42) }); } #[test] fn bad_link_index_faults() { let mut store = CubeStore::new(HashBackend::new()); let entry = Czyx::new(1, 1, 1, 1); let (h, b) = cell_code(entry, &[], &[Op::CallLink(3), Op::Halt], "fn"); store.put_record(entry, &h, &b); let mut vm = Vm::new(store); assert_eq!( vm.run(entry), RunResult::Fault { cell: entry, pc: 0, reason: Fault::BadLink(3) } ); } #[test] fn recursion_depth_bounded() { let mut store = CubeStore::new(HashBackend::new()); let a = Czyx::new(1, 0, 0, 1); let b = Czyx::new(2, 0, 0, 1); // a -> b -> a (mutual recursion via link 0 on each side) let (ha, ba) = cell_code(a, &[b], &[Op::CallLink(0), Op::Halt], "fn"); let (hb, bb) = cell_code(b, &[a], &[Op::CallLink(0), Op::Halt], "fn"); store.put_record(a, &ha, &ba); store.put_record(b, &hb, &bb); let mut vm = Vm::new(store); vm.set_max_call_depth(8); match vm.run(a) { RunResult::Fault { reason: Fault::CallStackOverflow, .. } => {} other => panic!("expected overflow, got {other:?}"), } } #[test] fn syscall_trace_emits_output() { let mut store = CubeStore::new(HashBackend::new()); let entry = Czyx::new(1, 1, 1, 1); let (h, b) = cell_code( entry, &[], &[ Op::Const(b'H'), Op::Const(b'i'), Op::Syscall(SYS_TRACE), Op::Halt, ], "fn", ); store.put_record(entry, &h, &b); let mut vm = Vm::new(store); assert_eq!(vm.run(entry), RunResult::Halted { top: None }); assert_eq!(vm.output(), b"Hi\n"); } #[test] fn syscall_degree_introspects_links() { let mut store = CubeStore::new(HashBackend::new()); let entry = Czyx::new(1, 1, 1, 1); let links = [ Czyx::new(9, 0, 0, 1), Czyx::new(9, 0, 0, 2), Czyx::new(9, 0, 0, 3), ]; let (h, b) = cell_code(entry, &links, &[Op::Syscall(SYS_DEGREE), Op::Halt], "fn"); store.put_record(entry, &h, &b); let mut vm = Vm::new(store); assert_eq!(vm.run(entry), RunResult::Halted { top: Some(3) }); } }