From 4484eba83d0564512f486a80fa310be196438b05 Mon Sep 17 00:00:00 2001 From: CUBELinux-2 Date: Mon, 10 Aug 2026 19:55:08 -0400 Subject: [PATCH] =?UTF-8?q?feat(cubelinux-2):=20Package=204=20=E2=80=94=20?= =?UTF-8?q?cubecode=20(cubevm):=20code/data=20mapping=20+=20safe=20cube-ad?= =?UTF-8?q?dressed=20bytecode=20VM=20over=20CZYX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Cargo.toml | 1 + cubecode/Cargo.toml | 10 + cubecode/src/cell.rs | 177 +++++++++++++++ cubecode/src/lib.rs | 44 ++++ cubecode/src/opcode.rs | 255 +++++++++++++++++++++ cubecode/src/vm.rs | 486 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 973 insertions(+) create mode 100644 cubecode/Cargo.toml create mode 100644 cubecode/src/cell.rs create mode 100644 cubecode/src/lib.rs create mode 100644 cubecode/src/opcode.rs create mode 100644 cubecode/src/vm.rs diff --git a/Cargo.toml b/Cargo.toml index 4d0547d..df34b0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "cubecoords", "cubestore", "cubefs", + "cubecode", ] [workspace.package] diff --git a/cubecode/Cargo.toml b/cubecode/Cargo.toml new file mode 100644 index 0000000..e7b08fd --- /dev/null +++ b/cubecode/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "cubecode" +version = "0.1.0" +edition.workspace = true +license.workspace = true +description = "Code/data mapping + safe cube-addressed bytecode VM over CZYX (PDF Package 4)" + +[dependencies] +cubecoords = { path = "../cubecoords" } +cubestore = { path = "../cubestore" } diff --git a/cubecode/src/cell.rs b/cubecode/src/cell.rs new file mode 100644 index 0000000..285e047 --- /dev/null +++ b/cubecode/src/cell.rs @@ -0,0 +1,177 @@ +//! Code/data mapping: how functions, modules, kernels, layers and +//! checkpoints are represented as CUBELinux records (PDF Package 4). +//! +//! The PDF's intent: "Treat each function, module, or model component as a +//! record: header: title, type ('fn', 'kernel', 'layer', 'checkpoint'), +//! created/last updated, owner, etc. Use association flags and CZYX links for +//! call graphs, dependency edges, variant implementations, version history." +//! +//! We make that concrete: a [`CodeCell`] is the typed bundle a record's +//! header *should* carry (we store it as `Option` doc_type + a typed +//! `Kind` enum in the title-space so the existing `CubeHeader` wire codec is +//! reused without a new serialize format). The VM reads the body as bytecode +//! and resolves `CALL_LINK n` to `linked_records[n]`. + +use cubecoords::{CubeHeader, Czyx}; + +/// The kind of code artifact a record holds. +/// +/// These are the PDF's example `type`s. They are stored as the record's +/// `doc_type` string so the binary header wire format (cubestore) carries +/// them for free, and so cubefs can surface them as a file's "type". +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Kind { + /// A plain function: a self-contained bytecode routine. + Fn, + /// A kernel: a routine the host treats as a primitive building block + /// (e.g. a vector op offloaded to `syscall`). + Kernel, + /// A model layer: weights/params for an AI component, addressable as a + /// record so other layers link to it. + Layer, + /// A model checkpoint: a snapshot of weights at a training step. + Checkpoint, + /// A variant: an alternate implementation of another routine. + Variant, + /// An unknown/unspecified artifact kind. + Other, +} + +impl Kind { + /// The canonical `doc_type` string persisted in the record header. + pub fn as_str(&self) -> &'static str { + match self { + Kind::Fn => "fn", + Kind::Kernel => "kernel", + Kind::Layer => "layer", + Kind::Checkpoint => "checkpoint", + Kind::Variant => "variant", + Kind::Other => "other", + } + } + + /// Parse a `doc_type` string back into a [`Kind`]. + pub fn from_doc_type(s: &str) -> Kind { + match s { + "fn" => Kind::Fn, + "kernel" => Kind::Kernel, + "layer" => Kind::Layer, + "checkpoint" => Kind::Checkpoint, + "variant" => Kind::Variant, + _ => Kind::Other, + } + } +} + +/// A typed, named code cell mapped onto a cube record. +/// +/// Decision: rather than invent a new serialization, a `CodeCell` is built +/// from (and flattened into) the existing [`CubeHeader`] + body bytes: the +/// `Kind` rides in `doc_type`, the human name in `title`, and the bytecode +/// in the body. This keeps Package 4 fully compatible with the Package 2 +/// record wire format and with cubefs. `code_kind`/`set_kind` bridge the +/// enum to the string. +pub struct CodeCell { + /// The cube coordinate this cell is stored at. + pub label: Czyx, + /// Typed header. + pub header: CubeHeader, + /// Decoded bytecode (re-derived from the body on construction). + pub code: Vec, +} + +impl CodeCell { + /// Build a code cell from a header + raw body, deriving the decoded + /// bytecode. Returns `None` if the body is not valid bytecode. + pub fn from_record(label: Czyx, header: &CubeHeader, body: &[u8]) -> Option { + let code = crate::opcode::decode(body).ok()?; + // Carry a clone of the header for callers that want the metadata + // (owner, kind, timestamps) without re-fetching. + let mut header = header.clone(); + header.size_bytes = Some(body.len() as u64); + header.refresh_flags(); + Some(CodeCell { + label, + header, + code, + }) + } + + /// The artifact kind, read from `doc_type`. + pub fn kind(&self) -> Kind { + Kind::from_doc_type(self.header.doc_type.as_deref().unwrap_or("other")) + } + + /// The human title, if any. + pub fn name(&self) -> Option<&str> { + self.header.title.as_deref() + } + + /// Indices of records this cell links to (its call-graph / dependency + /// edges). `CALL_LINK n` addresses `linked_records[n]`. + pub fn links(&self) -> &[Czyx] { + &self.header.linked_records + } + + /// Serialize for storage: returns the body bytes (re-encoded from the + /// decoded `code` so a `CodeCell` constructed in memory round-trips + /// exactly through the codec, not through a stale original buffer). + pub fn body(&self) -> Vec { + crate::opcode::encode(&self.code) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cubecoords::Czyx; + + fn cell(kind: Kind, links: usize) -> CodeCell { + let mut h = CubeHeader::new(); + h.title = Some("adder".into()); + h.doc_type = Some(kind.as_str().into()); + let link_coords: Vec = (0..links) + .map(|i| Czyx::new(7, 0, 0, i as u8 + 1)) + .collect(); + h.linked_records = link_coords; + h.refresh_flags(); + let code = vec![crate::opcode::Op::Const(1), crate::opcode::Op::Halt]; + CodeCell { + label: Czyx::new(1, 1, 1, 1), + header: h, + code, + } + } + + #[test] + fn kind_roundtrips_through_doc_type() { + for k in [ + Kind::Fn, + Kind::Kernel, + Kind::Layer, + Kind::Checkpoint, + Kind::Variant, + ] { + assert_eq!(Kind::from_doc_type(k.as_str()), k); + } + } + + #[test] + fn from_record_roundtrips() { + let c = cell(Kind::Fn, 2); + let body = c.body(); + let back = CodeCell::from_record(c.label, &c.header, &body).unwrap(); + assert_eq!(back.kind(), Kind::Fn); + assert_eq!(back.name(), Some("adder")); + assert_eq!(back.links().len(), 2); + assert_eq!(back.code, c.code); + } + + #[test] + fn invalid_body_rejected() { + let mut h = CubeHeader::new(); + h.doc_type = Some("fn".into()); + h.refresh_flags(); + assert!(CodeCell::from_record(Czyx::new(1, 2, 3, 4), &h, &[0xFF]).is_none()); + } +} diff --git a/cubecode/src/lib.rs b/cubecode/src/lib.rs new file mode 100644 index 0000000..f280e3f --- /dev/null +++ b/cubecode/src/lib.rs @@ -0,0 +1,44 @@ +//! Package 4 of CUBELinux-2: cubevm / cubecode — code/data mapping over the +//! CZYX cube (PDF "cubevm or cubecode"). +//! +//! The PDF's intent: "use the cube as a substrate for storing and +//! introspecting code and AI artifacts. Treat each function, module, or model +//! component as a record ... Use association flags and CZYX links for: call +//! graphs, dependency edges, variant implementations, version history." +//! +//! Resolution of the interesting tension: the PDF also says "body: bytecode, +//! machine code, or serialized model weights" and imagines "a scripting +//! language or DSL whose runtime walks cube links to load and dispatch +//! functions." The second clause is the load-bearing one for the design — it +//! is about *addressable, introspectable code-as-data*. We therefore build a +//! **safe, deterministic, cube-addressed bytecode VM** rather than a +//! machine-code JIT: +//! +//! * Each function/leaf is a record whose body is bytecode (`opcode.rs`). +//! * The call graph *is* the cube's association graph: `CALL_LINK n` +//! executes `linked_records[n]` (`vm.rs`). No foreign machine code is ever +//! executed; the body is always decoded through the codec first, so the +//! cube never runs something that didn't survive `decode`. +//! * AI artifacts (layers, checkpoints) are first-class `Kind`s so they +//! address into the same cube the code lives in (`cell.rs`). +//! +//! This runs on the metal here, needs no elevated privileges, and is fully +//! deterministic — a prerequisite for the PDF's "navigate and reconstruct +//! experiments" use case. A real model registry would point `Kind::Layer` +//! records at weight blobs (stored as record bodies) and link them into a +//! graph the VM can traverse. +//! +//! Scope note: `cubetrace`/`cubedbt`/`cubeai` (execution capture + replay + +//! ML over traces) are the *next* layer and are excluded on this hardware; +//! they would consume `cubecode`'s records as their input. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod cell; +pub mod opcode; +pub mod vm; + +pub use cell::{CodeCell, Kind}; +pub use opcode::{decode, encode, CodeError, Op}; +pub use vm::{Fault, RunResult, Vm, SYS_DEGREE, SYS_LINKED_EXISTS, SYS_NOP, SYS_TRACE}; diff --git a/cubecode/src/opcode.rs b/cubecode/src/opcode.rs new file mode 100644 index 0000000..ec47ad1 --- /dev/null +++ b/cubecode/src/opcode.rs @@ -0,0 +1,255 @@ +//! 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)); + } +} diff --git a/cubecode/src/vm.rs b/cubecode/src/vm.rs new file mode 100644 index 0000000..09181a4 --- /dev/null +++ b/cubecode/src/vm.rs @@ -0,0 +1,486 @@ +//! 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. + pub fn run(&mut self, entry: Czyx) -> RunResult { + self.output.clear(); + match self.exec_cell(entry, 0) { + 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. + fn exec_cell(&mut self, label: Czyx, depth: usize) -> 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 stack: Vec = Vec::new(); + 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)?; + // 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, &mut 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 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) }); + } +}