//! Bytecode opcode set and wire codec for the cube VM (PDF Package 4). //! //! The VM is a safe, deterministic, stack-based interpreter. Code lives as //! records in the CZYX cube (each record's body is a `Vec`), and the //! call graph *is* the cube's association graph: `CALL_LINK` executes a //! record that the current record links to. No foreign machine code is ever //! executed — the body is always decoded through this codec first. //! //! Every op is one byte; ops that carry an operand append it as a second //! byte. The encoding is self-delimiting so a truncated program fails //! cleanly instead of mis-decoding. /// A single VM instruction. /// /// Arithmetic and logic are on `u8` with wrapping semantics (the cube's /// cells are bytes). Jumps address the *decoded* instruction stream by /// index, not raw byte offsets, so a program can be re-encoded without /// invalidating jump targets. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Op { /// Do nothing. Nop, /// Stop execution (pops the call stack; ends the program at the root). Halt, /// Push an immediate byte onto the data stack. Const(u8), /// Push local slot `i` (0..16) onto the data stack. Load(u8), /// Pop the data stack into local slot `i` (0..16). Store(u8), /// `a + b` (wrapping). Add, /// `a - b` (wrapping). Sub, /// `a * b` (wrapping). Mul, /// `a / b`; faults on divide-by-zero. Div, /// `a % b`; faults on modulo-by-zero. Mod, /// `a & b`. And, /// `a | b`. Or, /// `a ^ b`. Xor, /// `a << (b & 7)`. Shl, /// `a >> (b & 7)`. Shr, /// Push `1` if `a == b` else `0`. Eq, /// Push `1` if `a != b` else `0`. Ne, /// Push `1` if `a < b` (unsigned) else `0`. Lt, /// Push `1` if `a > b` (unsigned) else `0`. Gt, /// Push `1` if `a <= b` (unsigned) else `0`. Le, /// Push `1` if `a >= b` (unsigned) else `0`. Ge, /// Unconditional jump to instruction index `t`. Jmp(u8), /// Pop `v`; jump to `t` if `v == 0`, else fall through. Jz(u8), /// Pop `v`; jump to `t` if `v != 0`, else fall through. Jnz(u8), /// Call the record at `linked_records[link]` (the cube edge). Pushes a /// return frame; the callee runs on the *shared* data stack, so callers /// pass arguments by leaving them on the stack and read results after /// `RET` returns. CallLink(u8), /// Return to the caller (pop the call frame). At the root, ends the run. Ret, /// Invoke host syscall `id` (see [`crate::vm::Vm::syscall`]). Syscall(u8), /// Duplicate the top of the data stack. Dup, /// Discard the top of the data stack. Drop, } /// Error decoding a bytecode stream. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum CodeError { /// An operand byte was missing at the end of the stream. Truncated, /// An unknown opcode byte was encountered. UnknownOp(u8), } const OP_CONST: u8 = 2; const OP_LOAD: u8 = 3; const OP_STORE: u8 = 4; const OP_JMP: u8 = 21; const OP_JZ: u8 = 22; const OP_JNZ: u8 = 23; const OP_CALLLINK: u8 = 24; const OP_SYSCALL: u8 = 26; /// Encode a sequence of ops into the wire format. /// /// Single-byte ops map to their byte; operand ops append the operand. The /// result is deterministic and reversible by [`decode`]. pub fn encode(ops: &[Op]) -> Vec { let mut out = Vec::with_capacity(ops.len() * 2); for op in ops { match op { Op::Nop => out.push(0), Op::Halt => out.push(1), Op::Const(v) => { out.push(OP_CONST); out.push(*v); } Op::Load(i) => { out.push(OP_LOAD); out.push(*i); } Op::Store(i) => { out.push(OP_STORE); out.push(*i); } Op::Add => out.push(5), Op::Sub => out.push(6), Op::Mul => out.push(7), Op::Div => out.push(8), Op::Mod => out.push(9), Op::And => out.push(10), Op::Or => out.push(11), Op::Xor => out.push(12), Op::Shl => out.push(13), Op::Shr => out.push(14), Op::Eq => out.push(15), Op::Ne => out.push(16), Op::Lt => out.push(17), Op::Gt => out.push(18), Op::Le => out.push(19), Op::Ge => out.push(20), Op::Jmp(t) => { out.push(OP_JMP); out.push(*t); } Op::Jz(t) => { out.push(OP_JZ); out.push(*t); } Op::Jnz(t) => { out.push(OP_JNZ); out.push(*t); } Op::CallLink(l) => { out.push(OP_CALLLINK); out.push(*l); } Op::Ret => out.push(25), Op::Syscall(id) => { out.push(OP_SYSCALL); out.push(*id); } Op::Dup => out.push(27), Op::Drop => out.push(28), } } out } fn next(b: &[u8], i: &mut usize) -> Result { if *i >= b.len() { return Err(CodeError::Truncated); } let v = b[*i]; *i += 1; Ok(v) } /// Decode a wire-format bytecode stream back into ops. /// /// Returns [`CodeError::Truncated`] if an operand is missing and /// [`CodeError::UnknownOp`] for unrecognized opcode bytes. pub fn decode(b: &[u8]) -> Result, CodeError> { let mut ops = Vec::new(); let mut i = 0; while i < b.len() { let byte = b[i]; i += 1; let op = match byte { 0 => Op::Nop, 1 => Op::Halt, OP_CONST => Op::Const(next(b, &mut i)?), OP_LOAD => Op::Load(next(b, &mut i)?), OP_STORE => Op::Store(next(b, &mut i)?), 5 => Op::Add, 6 => Op::Sub, 7 => Op::Mul, 8 => Op::Div, 9 => Op::Mod, 10 => Op::And, 11 => Op::Or, 12 => Op::Xor, 13 => Op::Shl, 14 => Op::Shr, 15 => Op::Eq, 16 => Op::Ne, 17 => Op::Lt, 18 => Op::Gt, 19 => Op::Le, 20 => Op::Ge, OP_JMP => Op::Jmp(next(b, &mut i)?), OP_JZ => Op::Jz(next(b, &mut i)?), OP_JNZ => Op::Jnz(next(b, &mut i)?), OP_CALLLINK => Op::CallLink(next(b, &mut i)?), 25 => Op::Ret, OP_SYSCALL => Op::Syscall(next(b, &mut i)?), 27 => Op::Dup, 28 => Op::Drop, other => return Err(CodeError::UnknownOp(other)), }; ops.push(op); } Ok(ops) } #[cfg(test)] mod tests { use super::*; #[test] fn encode_decode_roundtrip() { let prog = vec![ Op::Const(42), Op::Store(3), Op::Load(3), Op::Const(2), Op::Mul, Op::Jz(0), Op::CallLink(1), Op::Syscall(0), Op::Halt, ]; let bytes = encode(&prog); assert_eq!(decode(&bytes).unwrap(), prog); } #[test] fn truncated_operand_is_error() { // Op::Const (2) with no following byte. assert_eq!(decode(&[2]).unwrap_err(), CodeError::Truncated); } #[test] fn unknown_op_is_error() { assert_eq!(decode(&[0xFF]).unwrap_err(), CodeError::UnknownOp(0xFF)); } }