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:
@@ -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
@@ -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};
|
||||
|
||||
@@ -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![]);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user