CUBELinux-2: WordFlags/tri-channel 16-bit flag field, tag-14 + scan_by_word_flag, cubetrace capture→replay→golden→lineage, CUBE VP-tree balling index (cubecode::ballindex), cubefs-visible WordFlags, and cube-mvw (sharded multi-writer MVCC store with per-shard WALs + group-commit + compaction, impl CubeBackend => CubeStore<MvwBackend> DB). Adds show-flags/scan-word-flags/trace-*/golden-capture CLI; ~ tests green.

This commit is contained in:
CUBELinux-2
2026-09-06 21:20:04 -04:00
parent 3f007a1f38
commit cc896fc336
22 changed files with 1695 additions and 7 deletions
+215
View File
@@ -0,0 +1,215 @@
//! Feature-vector ball / k-NN index: a metric VP-tree stored as CUBE records.
//! Each node is a feature-vector record (`doc_type = "vp-node"`) whose body holds
//! the pivot `(x,y,z)` + radial radius `tau`, and whose `linked_records` are its
//! two children. Ball recall walks the tree, pruning branches that cannot
//! intersect the query ball — exact, bounded node visits (whitepaper §4.1).
use cubecoords::{CubeHeader, Czyx};
use cubestore::{CubeBackend, CubeStore};
/// A 3-D feature vector (the `(fx,fy,fz)` MEM model).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Vec3 {
pub x: u64,
pub y: u64,
pub z: u64,
}
impl Vec3 {
pub fn new(x: u64, y: u64, z: u64) -> Self {
Vec3 { x, y, z }
}
pub fn d2(&self, o: &Vec3) -> u64 {
let dx = (self.x as i64 - o.x as i64).abs() as u64;
let dy = (self.y as i64 - o.y as i64).abs() as u64;
let dz = (self.z as i64 - o.z as i64).abs() as u64;
dx * dx + dy * dy + dz * dz
}
}
const VP_NODE: &str = "vp-node";
/// Namespace for index nodes (avoid collisions with user data spaces).
pub const VP_SPACE: u8 = 200;
const NULL_NODE: Czyx = Czyx::new(VP_SPACE, 0, 0, 0);
/// Build a VP-tree over `points`, writing each node as a record. Returns the root coord.
pub fn build<B: CubeBackend>(store: &mut CubeStore<B>, points: &[Vec3]) -> Czyx {
let mut counter = 0u64;
let idx: Vec<usize> = (0..points.len()).collect();
build_rec(store, points, &idx, &mut counter)
}
fn build_rec<B: CubeBackend>(
store: &mut CubeStore<B>,
points: &[Vec3],
box_idxs: &[usize],
counter: &mut u64,
) -> Czyx {
if box_idxs.is_empty() {
return NULL_NODE;
}
let pi = box_idxs[0];
let pivot = points[pi];
let mut others: Vec<(u64, usize)> = box_idxs
.iter()
.copied()
.filter(|&i| i != pi)
.map(|i| (pivot.d2(&points[i]), i))
.collect();
others.sort_by_key(|&(d, _)| d);
// Vantage radius = sqrt of the median squared distance to the other points.
let tau = if others.is_empty() {
0
} else {
(others[others.len() / 2].0 as f64).sqrt() as u64
};
let mid = others.len() / 2;
let left_idx: Vec<usize> = others.iter().take(mid).map(|&(_, i)| i).collect();
let right_idx: Vec<usize> = others.iter().skip(mid).map(|&(_, i)| i).collect();
*counter += 1;
let coord = czyx_node(*counter);
let left = build_rec(store, points, &left_idx, counter);
let right = build_rec(store, points, &right_idx, counter);
let mut links = Vec::new();
if left != NULL_NODE {
links.push(left);
}
if right != NULL_NODE {
links.push(right);
}
let mut h = CubeHeader::new();
h.doc_type = Some(VP_NODE.into());
h.title = Some("vp".into());
h.linked_records = links;
h.refresh_flags();
let mut body = Vec::with_capacity(32);
for v in [pivot.x, pivot.y, pivot.z, tau] {
body.extend_from_slice(&v.to_le_bytes());
}
store.put_record(coord, &h, &body);
coord
}
fn czyx_node(n: u64) -> Czyx {
// Encode the node id across the low three axes so we can host far more than
// 255 nodes (u8 per axis would collide). c = index namespace.
Czyx::new(VP_SPACE, ((n >> 16) & 0xFF) as u8, ((n >> 8) & 0xFF) as u8, (n & 0xFF) as u8)
}
/// Ball recall: walk the VP-tree from `root`, pruning branches that cannot
/// intersect the query ball. Returns the coordinates of every vector within `r`
/// and the number of nodes visited (each visit = one record read).
pub fn ball_stats<B: CubeBackend>(
store: &CubeStore<B>,
root: Czyx,
q: Vec3,
r: u64,
) -> (Vec<Czyx>, usize) {
let r2 = r * r;
let mut out = Vec::new();
let mut visits = 0usize;
ball_rec(store, root, &q, r2, r, &mut out, &mut visits);
(out, visits)
}
fn ball_rec<B: CubeBackend>(
store: &CubeStore<B>,
node: Czyx,
q: &Vec3,
r2: u64,
r: u64,
out: &mut Vec<Czyx>,
visits: &mut usize,
) {
if node == NULL_NODE {
return;
}
*visits += 1;
let (h, body) = match store.get_record(&node) {
Some(x) => x,
None => return,
};
if body.len() < 32 {
return;
}
let pivot = Vec3::new(
u64::from_le_bytes(body[0..8].try_into().unwrap()),
u64::from_le_bytes(body[8..16].try_into().unwrap()),
u64::from_le_bytes(body[16..24].try_into().unwrap()),
);
let tau = u64::from_le_bytes(body[24..32].try_into().unwrap());
let d2v = pivot.d2(q);
if d2v <= r2 {
out.push(node);
}
let links = h.linked_records;
let d = (d2v as f64).sqrt();
let rf = r as f64;
// Descend left (points within tau of the pivot) if the query ball may reach them.
if links.len() >= 1 && (d - rf) <= tau as f64 {
ball_rec(store, links[0], q, r2, r, out, visits);
}
// Descend right (points beyond tau) if the query ball may extend past the pivot sphere.
if links.len() >= 2 && (d + rf) > tau as f64 {
ball_rec(store, links[1], q, r2, r, out, visits);
}
}
#[cfg(test)]
mod tests {
use super::*;
use cubestore::HashBackend;
#[test]
fn vp_tree_exact_ball_recall() {
let n = 2000u64;
let mut points = Vec::with_capacity(n as usize);
for c in 0..16u64 {
let (bx, by, bz) = (c * 40, (c * 7) % 16 * 7, (c * 13) % 16 * 5);
for j in 0..(n / 16) {
let o = j as u64 % 12;
points.push(Vec3::new(bx + o, by + (o * 3) % 12, bz + (o * 7) % 12));
}
}
let mut store = CubeStore::new(HashBackend::new());
let root = build(&mut store, &points);
let r = 30u64;
let r2 = r * r;
let mut total_rec = 0usize;
let mut total_true = 0usize;
let mut visits_sum = 0usize;
for anchor in 0..40usize {
let q = points[anchor];
let (found, visits) = ball_stats(&store, root, q, r);
// ground truth = vectors within r of q
let truth: Vec<Vec3> = points.iter().copied().filter(|p| q.d2(p) <= r2).collect();
// decode pivots from the found node records
let mut found_pivots = Vec::new();
for c in &found {
if let Some((_, b)) = store.get_record(c) {
if b.len() >= 24 {
found_pivots.push(Vec3::new(
u64::from_le_bytes(b[0..8].try_into().unwrap()),
u64::from_le_bytes(b[8..16].try_into().unwrap()),
u64::from_le_bytes(b[16..24].try_into().unwrap()),
));
}
}
}
let rec = truth.iter().filter(|v| found_pivots.contains(v)).count();
total_rec += rec;
total_true += truth.len();
visits_sum += visits;
}
eprintln!(
"exact recall {:.1}% ({} of {}), avg {} node-visits per query (N={})",
100.0 * total_rec as f64 / total_true as f64,
total_rec,
total_true,
visits_sum as f64 / 40.0,
n
);
assert!(total_true > 0 && total_rec == total_true, "VP-tree must be exact (got {}/{}), avg {} visits", total_rec, total_true, visits_sum as f64 / 40.0);
assert!(visits_sum as f64 / 40.0 < (n as f64) / 3.0, "VP-tree must prune (avg {} visits)", visits_sum as f64 / 40.0);
}
}
+5 -1
View File
@@ -37,11 +37,15 @@
pub mod cb;
pub mod cell;
pub mod ballindex;
pub mod opcode;
pub mod trace;
pub mod vm;
pub use cb::{Behavior, C_OS_EFFECT, C_OS_KERNEL, HEADER_FLAG_BEHAVIOR};
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};
pub use ballindex::{ball_stats, build, Vec3, VP_SPACE};
pub use trace::{capture_golden, lineage, replay_trace, store_trace, verify_golden, LineageHop};
pub use vm::{replay, Fault, RunResult, Vm, SYS_DEGREE, SYS_LINKED_EXISTS, SYS_NOP, SYS_TRACE};
+263
View File
@@ -0,0 +1,263 @@
//! Execution-trace artifacts: store a captured machine-code trace as a
//! first-class AI-artifact record (`Kind::Layer`) linked back to its source,
//! and replay it to reproduce the run — so an agent can persist & reproduce
//! interesting executions as ordinary CZYX records.
//!
//! A trace is the exact `Vec<Op>` a program actually executed ([`Vm::run_captured`]).
//! We persist it as a record body (via [`crate::opcode::encode`]) with
//! `doc_type = "layer"` (the AI-artifact kind) and a `linked_records` edge to
//! the source cell. Loading it back (via [`CodeCell::from_record`]) decodes the
//! ops and [`replay`] reproduces the observable behaviour with no access to the
//! source program.
use crate::{opcode, replay, Kind, Op, RunResult, Vm};
use cubecoords::{CubeHeader, Czyx};
use cubestore::{CubeBackend, CubeStore};
/// Store a captured execution trace as an AI-artifact record at `coord`,
/// linked to its `source` cell. `name` becomes the record title; the body is
/// the encoded trace; `doc_type` is `Kind::Layer`.
pub fn store_trace<B: CubeBackend>(
store: &mut CubeStore<B>,
coord: Czyx,
source: Czyx,
trace: &[Op],
name: &str,
) {
let mut h = CubeHeader::new();
h.title = Some(name.to_string());
h.doc_type = Some(Kind::Layer.as_str().to_string());
h.linked_records = vec![source];
h.refresh_flags();
store.put_record(coord, &h, &opcode::encode(trace));
}
/// Load a stored trace record at `coord` and replay it, reproducing the run's
/// observable behaviour (data-stack result + `SYS_TRACE` output) from the
/// stored record — no access to the original program. Returns `None` if the
/// record is missing or its body is not a valid trace.
pub fn replay_trace<B: CubeBackend>(
store: &CubeStore<B>,
coord: Czyx,
) -> Option<(RunResult, Vec<u8>)> {
let (_h, body) = store.get_record(&coord)?;
let ops = opcode::decode(&body).ok()?;
Some(replay(&ops))
}
/// Capture a run and persist it as a "golden": the executed trace stored as a
/// `Kind::Layer` record (linked to `source`) and the observed output stored as a
/// companion `golden` record (linked to the trace). Returns the observed output.
pub fn capture_golden<B: CubeBackend + Clone>(
store: &mut CubeStore<B>,
source: Czyx,
trace_coord: Czyx,
golden_coord: Czyx,
name: &str,
) -> Vec<u8> {
let mut vm = Vm::new((*store).clone());
let (_r, trace) = vm.run_captured(source);
let out = vm.output().to_vec();
store_trace(store, trace_coord, source, &trace, name);
let mut h = CubeHeader::new();
h.title = Some(format!("golden:{name}"));
h.doc_type = Some("golden".into());
h.linked_records = vec![trace_coord];
h.refresh_flags();
store.put_record(golden_coord, &h, &out);
out
}
/// Golden-trace regression check: re-run `source` fresh AND replay the stored
/// trace, and assert BOTH reproduce the recorded golden output.
/// * replay vs golden -> the stored trace is a faithful reproduction;
/// * fresh run vs golden -> the program has not drifted (or it would change
/// the observed output).
/// Returns `Ok(())` if the behavior is unchanged, else `Err(reason)`.
pub fn verify_golden<B: CubeBackend + Clone>(
store: &CubeStore<B>,
source: Czyx,
trace_coord: Czyx,
golden_coord: Czyx,
) -> Result<(), String> {
let golden = store
.get_record(&golden_coord)
.map(|(_, b)| b)
.ok_or("golden record missing")?;
let (_, replay_out) = replay_trace(store, trace_coord).ok_or("trace record missing/invalid")?;
if replay_out != golden {
return Err(format!(
"replay output differs from golden ({} vs {} bytes)",
replay_out.len(),
golden.len()
));
}
let mut vm = Vm::new((*store).clone());
let _ = vm.run(source);
let fresh = vm.output().to_vec();
if fresh != golden {
return Err(format!(
"fresh run differs from golden (program changed: {} vs {} bytes)",
fresh.len(),
golden.len()
));
}
Ok(())
}
/// A hop in the experiment-lineage graph: a record and the records it links to.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LineageHop {
/// The record's coordinate.
pub coord: Czyx,
/// Its `doc_type` (kind: fn/kernel/layer/checkpoint/variant/golden/...).
pub doc_type: String,
/// Its human title, if any.
pub title: String,
/// The CZYX records this record links to (its edges).
pub links: Vec<Czyx>,
}
/// Walk the linked-record graph starting at `start`, following
/// `linked_records` edges (breadth-first) up to `depth` levels. Returns the
/// ordered list of reachable records — the experiment lineage chain
/// (source -> data -> weights -> trace -> golden, etc.). Each record is the
/// kind the CUBE associates; provenance is a graph walk, not a lookup.
pub fn lineage<B: CubeBackend>(
store: &CubeStore<B>,
start: Czyx,
depth: usize,
) -> Vec<LineageHop> {
let mut out = Vec::new();
let mut frontier = vec![start];
let mut seen: std::collections::HashSet<Czyx> = std::collections::HashSet::new();
for _ in 0..=depth {
if frontier.is_empty() {
break;
}
let mut next = Vec::new();
for c in frontier {
if !seen.insert(c) {
continue;
}
if let Some((h, _)) = store.get_record(&c) {
let links = h.linked_records.clone();
out.push(LineageHop {
coord: c,
doc_type: h.doc_type.clone().unwrap_or_default(),
title: h.title.clone().unwrap_or_default(),
links: links.clone(),
});
next.extend(links);
}
}
frontier = next;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{SYS_TRACE, Vm};
use cubestore::HashBackend;
#[test]
fn trace_stored_as_layer_and_replayed_without_source() {
let mut store = CubeStore::new(HashBackend::new());
let entry = Czyx::new(1, 1, 1, 7);
let ops = [
Op::Const(0), Op::Const(1), Op::Syscall(SYS_TRACE),
Op::Const(1), Op::Const(1), Op::Syscall(SYS_TRACE),
Op::Const(2), Op::Const(2), Op::Syscall(SYS_TRACE),
Op::Halt,
];
let mut h = CubeHeader::new();
h.title = Some("solitaire".into());
h.doc_type = Some("fn".into());
h.refresh_flags();
store.put_record(entry, &h, &opcode::encode(&ops));
// 1) Capture the run as a machine-code trace.
let mut vm = Vm::new(store.clone());
let (_r, trace) = vm.run_captured(entry);
let produced = vm.output().to_vec();
assert!(!trace.is_empty());
// 2) Store it as a Kind::Layer AI-artifact record linked to the source.
let trace_coord = Czyx::new(9, 0, 0, 1);
store_trace(&mut store, trace_coord, entry, &trace, "solitaire-run-1");
// 3) The record is a "layer" artifact with an edge back to the source.
let (th, _tb) = store.get_record(&trace_coord).unwrap();
assert_eq!(th.doc_type.as_deref(), Some("layer"));
assert_eq!(th.linked_records, vec![entry]);
// 4) Reproduce the run purely from the stored trace record.
let (result, replayed) = replay_trace(&store, trace_coord).unwrap();
assert_eq!(result, RunResult::Halted { top: None });
assert_eq!(replayed, produced, "stored trace must reproduce the run");
assert!(replayed.contains(&b'\n'));
}
#[test]
fn golden_regression_detects_drift() {
let mut store = CubeStore::new(HashBackend::new());
let entry = Czyx::new(1, 1, 1, 9);
let mk = |val: u8| {
vec![Op::Const(val), Op::Syscall(SYS_TRACE), Op::Halt]
};
let mut h = CubeHeader::new();
h.doc_type = Some("fn".into());
h.refresh_flags();
store.put_record(entry, &h, &opcode::encode(&mk(7)));
let trace_c = Czyx::new(9, 0, 0, 1);
let golden_c = Czyx::new(9, 0, 0, 2);
let out = capture_golden(&mut store, entry, trace_c, golden_c, "regress");
assert_eq!(out, b"\x07\n");
assert!(verify_golden(&store, entry, trace_c, golden_c).is_ok());
// Drift the program: change the emitted value -> fresh run differs.
store.put_record(entry, &h, &opcode::encode(&mk(9)));
let r = verify_golden(&store, entry, trace_c, golden_c);
assert!(r.is_err(), "program drift must be detected: {r:?}");
// The stored trace is immutable: it still replays the ORIGINAL golden.
let (_, rep) = replay_trace(&store, trace_c).unwrap();
assert_eq!(rep, b"\x07\n", "stored trace still reproduces the original run");
}
#[test]
fn lineage_walks_source_data_weights_trace() {
let mut store = CubeStore::new(HashBackend::new());
// Build experiment lineage: source -> data -> weights -> trace -> golden.
let mut mk = |coord: Czyx, doc: &str, title: &str, links: Vec<Czyx>| {
let mut h = CubeHeader::new();
h.title = Some(title.into());
h.doc_type = Some(doc.into());
h.linked_records = links;
h.refresh_flags();
store.put_record(coord, &h, &[]);
};
let source = Czyx::new(1, 1, 1, 1);
let data = Czyx::new(2, 0, 0, 1);
let weights = Czyx::new(3, 0, 0, 1);
let trace = Czyx::new(4, 0, 0, 1);
let golden = Czyx::new(5, 0, 0, 1);
mk(source, "fn", "train", vec![data, weights]);
mk(data, "data", "batch", vec![]);
mk(weights, "layer", "model-v1", vec![]);
mk(trace, "layer", "trace:train", vec![source]);
mk(golden, "golden", "golden:train", vec![trace]);
// Walk from the trace back to its lineage.
let hops = lineage(&store, trace, 4);
assert!(hops.iter().any(|h| h.coord == trace && h.doc_type == "layer"));
assert!(hops.iter().any(|h| h.coord == source && h.doc_type == "fn"));
// Depth-limited: from a leaf with no links, only itself.
let leaf = lineage(&store, data, 3);
assert_eq!(leaf.len(), 1);
assert_eq!(leaf[0].coord, data);
assert_eq!(leaf[0].links, vec![]);
}
}
+141
View File
@@ -90,6 +90,12 @@ pub struct Vm<B: CubeBackend> {
output: Vec<u8>,
/// Maximum nesting of `CALL_LINK` frames.
max_call_depth: usize,
/// Executed-instruction trace collected when [`Vm::run_captured`] is active:
/// the exact ordered sequence of `Op`s that actually ran (the "machine code
/// as it runs"). Used to reproduce behaviour without the source.
trace: Vec<Op>,
/// Whether [`Vm::run_captured`] is currently recording the instruction trace.
capturing: bool,
}
impl<B: CubeBackend> Vm<B> {
@@ -104,6 +110,8 @@ impl<B: CubeBackend> Vm<B> {
store,
output: Vec::new(),
max_call_depth: Self::MAX_CALL_DEPTH,
trace: Vec::new(),
capturing: false,
}
}
@@ -158,6 +166,9 @@ impl<B: CubeBackend> Vm<B> {
while pc < code.len() {
let op = code[pc];
if self.capturing {
self.trace.push(op);
}
match op {
Op::Nop => {}
Op::Halt => {
@@ -258,6 +269,21 @@ impl<B: CubeBackend> Vm<B> {
Ok(stack.pop())
}
/// Run `entry` AND capture the executed-instruction trace (the "machine
/// code as it runs"): the exact ordered sequence of `Op`s executed, across
/// all cells, regardless of source. Branches are already resolved (the
/// trace is the taken path) and call frames are marked by `CallLink`/`Ret`,
/// so [`Vm::replay`] can reproduce the same behaviour from the trace alone
/// — without the original program's records/code.
pub fn run_captured(&mut self, entry: Czyx) -> (RunResult, Vec<Op>) {
self.output.clear();
self.trace.clear();
self.capturing = true;
let res = self.run(entry);
self.capturing = false;
(res, std::mem::take(&mut self.trace))
}
/// 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> {
@@ -288,6 +314,87 @@ impl<B: CubeBackend> Vm<B> {
}
}
/// Replay-time host syscall dispatch (no store/header). Store-independent
/// syscalls (`SYS_TRACE`, `SYS_NOP`) are reproduced exactly; syscalls that
/// would need the live store/header are a no-op in trace-replay (they don't
/// contribute to the observable `SYS_TRACE` output).
fn replay_syscall(id: u8, stack: &mut Vec<u8>, output: &mut Vec<u8>) {
match id {
SYS_NOP => {}
SYS_TRACE => {
let mut line: Vec<u8> = std::mem::take(stack);
line.push(b'\n');
output.extend_from_slice(&line);
}
_ => {}
}
}
/// Replay a captured instruction trace, reproducing the program's actions
/// (data-stack result + `SYS_TRACE` output) WITHOUT accessing any of the
/// original program's records/bytecode. This is the "duplicate behaviour
/// from machine code as it ran" path. Store-independent syscalls
/// (`SYS_TRACE`, `SYS_NOP`) are reproduced exactly; syscalls that would
/// need the live store/header are a no-op (those don't contribute to the
/// observable `SYS_TRACE` output).
pub fn replay(trace: &[Op]) -> (RunResult, Vec<u8>) {
let mut stack: Vec<u8> = Vec::new();
let mut frames: Vec<[u8; LOCALS]> = vec![[0; LOCALS]];
let mut output: Vec<u8> = Vec::new();
let mut top = None;
for &op in trace {
match op {
Op::Nop => {}
Op::Halt => {
top = stack.pop();
break;
}
Op::Const(v) => stack.push(v),
Op::Load(i) => stack.push(frames.last_mut().unwrap()[(i as usize) % LOCALS]),
Op::Store(i) => {
let v = stack.pop().unwrap_or(0);
frames.last_mut().unwrap()[(i as usize) % LOCALS] = v;
}
Op::Add | Op::Sub | Op::Mul | Op::Div | Op::Mod | Op::And | Op::Or | Op::Xor => {
let (b, a) = (stack.pop(), stack.pop());
stack.push(bin_op(op, a.unwrap_or(0), b.unwrap_or(0)).unwrap_or(0));
}
Op::Shl | Op::Shr => {
let (b, a) = (stack.pop(), stack.pop());
stack.push(shift_op(op, a.unwrap_or(0), b.unwrap_or(0)));
}
Op::Eq | Op::Ne | Op::Lt | Op::Gt | Op::Le | Op::Ge => {
let (b, a) = (stack.pop(), stack.pop());
stack.push(cmp_op(op, a.unwrap_or(0), b.unwrap_or(0)));
}
// Jumps already reflect the taken path in a captured trace:
// keep the stack effect, don't re-branch.
Op::Jmp(_) => {}
Op::Jz(_) | Op::Jnz(_) => {
let _ = stack.pop();
}
Op::CallLink(_) => frames.push([0; LOCALS]),
Op::Ret => {
if frames.len() > 1 {
frames.pop();
} else {
top = stack.pop();
break;
}
}
Op::Syscall(id) => replay_syscall(id, &mut stack, &mut output),
Op::Dup => {
let v = *stack.last().unwrap_or(&0);
stack.push(v);
}
Op::Drop => {
stack.pop();
}
}
}
(RunResult::Halted { top }, output)
}
fn b2(v: Option<u8>, cell: Czyx, pc: usize) -> Result<u8, (Czyx, usize, Fault)> {
v.ok_or((cell, pc, Fault::PcOutOfRange))
}
@@ -522,4 +629,38 @@ mod tests {
let mut vm = Vm::new(store);
assert_eq!(vm.run(entry), RunResult::Halted { top: Some(3) });
}
#[test]
fn capture_and_replay_reproduces_behavior_without_source() {
let mut store = CubeStore::new(HashBackend::new());
let entry = Czyx::new(1, 1, 1, 7);
// A tiny "solitaire" state machine: draw cards; even -> foundation
// (score++), odd -> waste. Emit (score, waste) after each play.
let ops = [
Op::Const(0), Op::Const(1), Op::Syscall(SYS_TRACE), // draw odd -> waste
Op::Const(1), Op::Const(1), Op::Syscall(SYS_TRACE), // draw even -> score
Op::Const(1), Op::Const(2), Op::Syscall(SYS_TRACE), // draw odd -> waste
Op::Const(2), Op::Const(2), Op::Syscall(SYS_TRACE), // draw even -> score
Op::Halt,
];
let mut h = CubeHeader::new();
h.title = Some("solitaire".into());
h.doc_type = Some("fn".into());
h.refresh_flags();
store.put_record(entry, &h, &crate::opcode::encode(&ops));
let mut vm = Vm::new(store);
// Capture: run the program AND record the exact machine code executed.
let (result, trace) = vm.run_captured(entry);
assert_eq!(result, RunResult::Halted { top: None });
assert!(!trace.is_empty(), "capture must record the executed ops");
let produced = vm.output().to_vec();
assert_eq!(produced, b"\x00\x01\n\x01\x01\n\x01\x02\n\x02\x02\n");
// Replay: reproduce the SAME actions purely from the captured trace,
// with NO access to the original program's records/bytecode.
let (result2, replayed) = replay(&trace);
assert_eq!(result2, RunResult::Halted { top: None });
assert_eq!(replayed, produced, "replay must reproduce the game's behaviour");
}
}