feat(cubelinux-2): Package 4 — cubecode (cubevm): code/data mapping + safe cube-addressed bytecode VM over CZYX
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.
This commit is contained in:
@@ -4,6 +4,7 @@ members = [
|
|||||||
"cubecoords",
|
"cubecoords",
|
||||||
"cubestore",
|
"cubestore",
|
||||||
"cubefs",
|
"cubefs",
|
||||||
|
"cubecode",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -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" }
|
||||||
@@ -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<String>` 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<crate::opcode::Op>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<CodeCell> {
|
||||||
|
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<u8> {
|
||||||
|
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<Czyx> = (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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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};
|
||||||
@@ -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<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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<u8>,
|
||||||
|
},
|
||||||
|
/// 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<B: CubeBackend> {
|
||||||
|
store: CubeStore<B>,
|
||||||
|
/// Diagnostic output accumulated from `SYSCALL 3`.
|
||||||
|
output: Vec<u8>,
|
||||||
|
/// Maximum nesting of `CALL_LINK` frames.
|
||||||
|
max_call_depth: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: CubeBackend> Vm<B> {
|
||||||
|
/// 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<B>) -> 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<Option<u8>, (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<u8> = 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<u8>) -> 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<u8> = 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<u8>, cell: Czyx, pc: usize) -> Result<u8, (Czyx, usize, Fault)> {
|
||||||
|
v.ok_or((cell, pc, Fault::PcOutOfRange))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bin_op(op: Op, a: u8, b: u8) -> Result<u8, Fault> {
|
||||||
|
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<u8>) {
|
||||||
|
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) });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user