Implements the PDF's Package 4 (cubevm/cubecode): the cube as a substrate for
storing and introspecting code + AI artifacts.
- opcode.rs: decode/encode codec for a deterministic, safe bytecode (28 ops:
stack arithmetic/logic/shift, comparisons, jumps, CALL_LINK, RET, SYSCALL,
DUP/DROP). Body is always decoded through this codec before execution, so
the cube never runs code that didn't survive decode.
- cell.rs: CodeCell + Kind (Fn/Kernel/Layer/Checkpoint/Variant/Other). Kind
rides in the record doc_type; the call graph is the cube's linked_records,
so CALL_LINK n executes linked_records[n].
- vm.rs: stack-based interpreter over CubeStore. CALL_LINK follows cube edges;
call depth is bounded (no infinite cube loops); deterministic + no unsafe +
no float, so runs are reproducible (prereq for 'navigate and reconstruct
experiments'). Host syscalls introspect the cube (degree/link-exists/trace).
Design decision (the PDF's interesting tension): it says body may be
'bytecode, machine code, or serialized model weights' but the load-bearing
clause is a DSL/runtime that 'walks cube links to load and dispatch functions'
= addressable code-as-data. On this hardware a foreign-machine-code JIT is
neither safe nor needed, so we built the safe bytecode VM. cubetrace/
cubedbt/cubeai (execution capture + ML over traces) are the next layer and are
excluded here.
Verified: ./check (fmt+tests+clippy -D warnings) green; cubecode: 13 tests
(arith, div-by-zero fault, CALL_LINK edge-follow, bad-link fault, recursion
depth bound, syscall trace/degree). Gate: cde1e62 -> +Package4.
256 lines
7.4 KiB
Rust
256 lines
7.4 KiB
Rust
//! 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<Op>`), 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<u8> {
|
|
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<u8, CodeError> {
|
|
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<Vec<Op>, 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));
|
|
}
|
|
}
|