clippy flagged "you should consider adding a `Default` implementation for `CodeCache`". Add the impl delegating to `new()`. Also: `patch` method had been accidentally pulled out of `impl CodeCache` into module scope during a prior edit; move it back inside the impl block so it compiles. Verification: all 4 cubedbt tests pass, clippy clean. Co-Authored-By: Hermes Agent
422 lines
15 KiB
Rust
422 lines
15 KiB
Rust
//! CUBELinux-2 DBT (dynamic binary translation) runtime.
|
||
//!
|
||
//! Per the PDF (Package 4, §549): *"cubedbt: a runtime that reads CZYX‑stored
|
||
//! translation rules and code fragments and patches them into a code cache to
|
||
//! execute mimicked behavior."*
|
||
//!
|
||
//! This crate is the substrate for the PDF's "watch a binary, archive its
|
||
//! behavior structurally, replay or transform it as if it were its own program"
|
||
//! story (§554). It is deliberately dependency-free and operates on the real
|
||
//! `CubeStore` / `CodeCell` / `Vm` types from the rest of the workspace, so it
|
||
//! is exercisable today on `HashBackend` and slots into `ConcurrentStore`
|
||
//! later without an API change.
|
||
//!
|
||
//! Model
|
||
//! -----
|
||
//! * A **translation rule** is a CZYX record (`Kind::Variant`) whose body is a
|
||
//! serialized [`TranslationRule`]. It names an *op kind* it can replace and
|
||
//! carries a *replacement fragment* (a `Vec<Op>`). The rule record links to
|
||
//! the original code cell it may substitute (the association edge).
|
||
//! * A **code cache** is the patched working set: `patch()` rewrites an
|
||
//! original `CodeCell`'s bytecode under the eligible rules and writes the
|
||
//! result back as a new record (preserving the original header's kind/title
|
||
//! and behavior descriptors, so it runs through the same `Vm`).
|
||
//! * A **DBT runtime** discovers rules in a store and can `mimic` a target
|
||
//! coordinate: fetch original → apply rules → run the patched version in the
|
||
//! `Vm`, returning the `RunResult`.
|
||
|
||
use cubecode::{opcode::Op, CodeCell, Kind, Vm};
|
||
use cubecoords::{CubeHeader, Czyx};
|
||
use cubestore::{CubeBackend, CubeStore, HashBackend};
|
||
|
||
/// `C` axis band where DBT translation rules are stored, kept distinct from the
|
||
/// `c210` OS-kernel band (`cubecode::C_OS_KERNEL`) and the `c200` snapshot data.
|
||
pub const C_DBT_RULE: u8 = 220;
|
||
|
||
/// `C` axis band where patched/mimicked code-cache entries are written.
|
||
pub const C_DBT_CACHE: u8 = 221;
|
||
|
||
/// The `Op` kinds a translation rule can target.
|
||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||
pub enum OpClass {
|
||
Const,
|
||
Add,
|
||
Sub,
|
||
Mul,
|
||
Div,
|
||
Mod,
|
||
And,
|
||
Or,
|
||
Xor,
|
||
Shl,
|
||
Shr,
|
||
Eq,
|
||
Ne,
|
||
Lt,
|
||
Gt,
|
||
Le,
|
||
Ge,
|
||
Load,
|
||
Store,
|
||
CallLink,
|
||
Any,
|
||
}
|
||
|
||
impl OpClass {
|
||
/// Classify a live [`Op`].
|
||
pub fn of(op: &Op) -> OpClass {
|
||
match op {
|
||
Op::Const(_) => OpClass::Const,
|
||
Op::Add => OpClass::Add,
|
||
Op::Sub => OpClass::Sub,
|
||
Op::Mul => OpClass::Mul,
|
||
Op::Div => OpClass::Div,
|
||
Op::Mod => OpClass::Mod,
|
||
Op::And => OpClass::And,
|
||
Op::Or => OpClass::Or,
|
||
Op::Xor => OpClass::Xor,
|
||
Op::Shl => OpClass::Shl,
|
||
Op::Shr => OpClass::Shr,
|
||
Op::Eq => OpClass::Eq,
|
||
Op::Ne => OpClass::Ne,
|
||
Op::Lt => OpClass::Lt,
|
||
Op::Gt => OpClass::Gt,
|
||
Op::Le => OpClass::Le,
|
||
Op::Ge => OpClass::Ge,
|
||
Op::Load(_) => OpClass::Load,
|
||
Op::Store(_) => OpClass::Store,
|
||
Op::CallLink(_) => OpClass::CallLink,
|
||
_ => OpClass::Any,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A translation rule: a named, CZYX-addressed mapping from an op class to a
|
||
/// replacement bytecode fragment. Serialized into a rule record's body.
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct TranslationRule {
|
||
/// Human name (also the rule record's title).
|
||
pub name: String,
|
||
/// Which op class this rule can substitute.
|
||
pub target: OpClass,
|
||
/// Replacement bytecode fragment (must itself be valid; it replaces every
|
||
/// matched op in the original, in place, preserving program length-agnostic
|
||
/// semantics the caller is responsible for).
|
||
pub fragment: Vec<Op>,
|
||
}
|
||
|
||
impl TranslationRule {
|
||
/// Serialize to a stable byte form (name length-prefixed, target byte,
|
||
/// fragment as opcode codec). No external deps.
|
||
pub fn to_bytes(&self) -> Vec<u8> {
|
||
let mut out = Vec::new();
|
||
let nb = self.name.as_bytes();
|
||
out.push(nb.len() as u8);
|
||
out.extend_from_slice(nb);
|
||
out.push(self.target as u8);
|
||
out.push(self.fragment.len() as u8);
|
||
for op in &self.fragment {
|
||
out.extend_from_slice(&cubecode::opcode::encode(&[*op]));
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Inverse of [`to_bytes`]. Returns `None` on any malformed input.
|
||
pub fn from_bytes(b: &[u8]) -> Option<TranslationRule> {
|
||
let mut i = 0;
|
||
let nlen = *b.get(i)? as usize;
|
||
i += 1;
|
||
if i + nlen > b.len() {
|
||
return None;
|
||
}
|
||
let name = String::from_utf8(b[i..i + nlen].to_vec()).ok()?;
|
||
i += nlen;
|
||
let target = *b.get(i)?;
|
||
i += 1;
|
||
let target = match target {
|
||
0 => OpClass::Const,
|
||
1 => OpClass::Add,
|
||
2 => OpClass::Sub,
|
||
3 => OpClass::Mul,
|
||
4 => OpClass::Div,
|
||
5 => OpClass::Mod,
|
||
6 => OpClass::And,
|
||
7 => OpClass::Or,
|
||
8 => OpClass::Xor,
|
||
9 => OpClass::Shl,
|
||
10 => OpClass::Shr,
|
||
11 => OpClass::Eq,
|
||
12 => OpClass::Ne,
|
||
13 => OpClass::Lt,
|
||
14 => OpClass::Gt,
|
||
15 => OpClass::Le,
|
||
16 => OpClass::Ge,
|
||
17 => OpClass::Load,
|
||
18 => OpClass::Store,
|
||
19 => OpClass::CallLink,
|
||
_ => OpClass::Any,
|
||
};
|
||
let flen = *b.get(i)? as usize;
|
||
i += 1;
|
||
let mut fragment = Vec::new();
|
||
for _ in 0..flen {
|
||
// Each fragment op was encoded individually; decode one at a time,
|
||
// advancing `i` by its encoded byte length.
|
||
let one = decode_one(&b[i..])?;
|
||
i += one.len;
|
||
fragment.push(one.op);
|
||
}
|
||
Some(TranslationRule {
|
||
name,
|
||
target,
|
||
fragment,
|
||
})
|
||
}
|
||
}
|
||
|
||
/// One decoded opcode plus its encoded length, used by `from_bytes`.
|
||
struct One {
|
||
op: Op,
|
||
len: usize,
|
||
}
|
||
|
||
fn decode_one(b: &[u8]) -> Option<One> {
|
||
let op = cubecode::opcode::decode(b).ok()?;
|
||
let op = op.into_iter().next()?;
|
||
let len = cubecode::opcode::encode(&[op]).len();
|
||
Some(One { op, len })
|
||
}
|
||
|
||
/// A patched working set ("code cache") inside a [`CubeStore`].
|
||
///
|
||
/// `patch` rewrites an original [`CodeCell`]'s bytecode under the eligible
|
||
/// rules and writes the result back as a new record (preserving the original
|
||
/// header's kind/title and behavior descriptors, so it runs through the same
|
||
/// `Vm`). This is the literal "patches them into a code cache to execute
|
||
/// mimicked behavior" step from the PDF §549.
|
||
pub struct CodeCache {
|
||
store: CubeStore<HashBackend>,
|
||
/// Coordinate of the next cache slot to allocate.
|
||
next_x: u8,
|
||
}
|
||
|
||
impl CodeCache {
|
||
/// Build an empty cache over a fresh in-memory backend.
|
||
pub fn new() -> Self {
|
||
CodeCache {
|
||
store: CubeStore::new(HashBackend::new()),
|
||
next_x: 1,
|
||
}
|
||
}
|
||
|
||
/// Patch `original` under `rules`: every op whose class matches a rule's
|
||
/// target (or whose rule target is `Any`) is replaced by that rule's
|
||
/// fragment. The rewritten cell is stored at a fresh cache coordinate and
|
||
/// returned (with its new label). The original store is untouched.
|
||
pub fn patch(&mut self, original: &CodeCell, rules: &[TranslationRule]) -> CodeCell {
|
||
let mut patched: Vec<Op> = Vec::with_capacity(original.code.len());
|
||
for op in &original.code {
|
||
let class = OpClass::of(op);
|
||
let mut applied = false;
|
||
for r in rules {
|
||
if r.target == class || r.target == OpClass::Any {
|
||
patched.extend_from_slice(&r.fragment);
|
||
applied = true;
|
||
break;
|
||
}
|
||
}
|
||
if !applied {
|
||
patched.push(*op);
|
||
}
|
||
}
|
||
let label = Czyx::new(C_DBT_CACHE, 1, 1, self.next_x);
|
||
self.next_x = self.next_x.wrapping_add(1).max(1);
|
||
let (h, body) = cache_header_for(original, &cubecode::opcode::encode(&patched));
|
||
self.store.put_record(label, &h, &body);
|
||
CodeCell::from_record(label, &h, &body).expect("patched cell is always valid bytecode")
|
||
}
|
||
|
||
/// Run a patched cache entry in the VM, returning its result.
|
||
pub fn run(&self, cell: &CodeCell) -> cubecode::RunResult {
|
||
let mut vm = Vm::new(self.store.clone());
|
||
vm.run(cell.label)
|
||
}
|
||
|
||
/// Borrow the backing store (e.g. to persist or inspect).
|
||
pub fn store(&self) -> &CubeStore<HashBackend> {
|
||
&self.store
|
||
}
|
||
}
|
||
|
||
impl Default for CodeCache {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
/// Build a cache record header that preserves the original's kind/title and
|
||
/// behavior descriptors, but marks it a `Variant` (a mimicked implementation).
|
||
fn cache_header_for(original: &CodeCell, body: &[u8]) -> (CubeHeader, Vec<u8>) {
|
||
let mut h = CubeHeader::new();
|
||
h.title = original.name().map(|s| format!("mimic:{}", s));
|
||
h.doc_type = Some(Kind::Variant.as_str().into());
|
||
h.size_bytes = Some(body.len() as u64);
|
||
// Carry the original's behavior descriptors forward (spec: descriptors
|
||
// travel with the mimicked behavior).
|
||
h.flags.0 |= original.header.flags.0 & cubecode::HEADER_FLAG_BEHAVIOR;
|
||
h.refresh_flags();
|
||
(h, body.to_vec())
|
||
}
|
||
|
||
/// The DBT runtime: discovers CZYX-stored translation rules and can `mimic` a
|
||
/// target code cell by patching it under those rules and running the result.
|
||
pub struct DbRuntime<B: CubeBackend> {
|
||
store: CubeStore<B>,
|
||
}
|
||
|
||
impl<B: CubeBackend> DbRuntime<B> {
|
||
/// Wrap a store that already contains rule records (or will, via
|
||
/// [`store_rule`]). The runtime is read-only over this store.
|
||
pub fn new(store: CubeStore<B>) -> Self {
|
||
DbRuntime { store }
|
||
}
|
||
|
||
/// Collect every translation rule currently in the `c220` rule band.
|
||
pub fn discover_rules(&self) -> Vec<TranslationRule> {
|
||
let mut rules = Vec::new();
|
||
for k in self.store.keys() {
|
||
if k.c != C_DBT_RULE {
|
||
continue;
|
||
}
|
||
if let Some((_, body)) = self.store.get_record(&k) {
|
||
if let Some(r) = TranslationRule::from_bytes(&body) {
|
||
rules.push(r);
|
||
}
|
||
}
|
||
}
|
||
rules
|
||
}
|
||
|
||
/// Fetch a code cell by coordinate from the wrapped store.
|
||
pub fn fetch(&self, label: Czyx) -> Option<CodeCell> {
|
||
let (h, b) = self.store.get_record(&label)?;
|
||
CodeCell::from_record(label, &h, &b)
|
||
}
|
||
|
||
/// Mimic `target`: fetch it, apply all discovered rules, patch into a fresh
|
||
/// cache, and run the patched version — returning the `RunResult`. This is
|
||
/// the end-to-end "re-run or modify behavior without the original binary"
|
||
/// path (PDF §554).
|
||
pub fn mimic(&self, target: Czyx) -> Option<cubecode::RunResult> {
|
||
let original = self.fetch(target)?;
|
||
let rules = self.discover_rules();
|
||
let mut cache = CodeCache::new();
|
||
let patched = cache.patch(&original, &rules);
|
||
Some(cache.run(&patched))
|
||
}
|
||
}
|
||
|
||
/// Store a translation rule into the `c220` rule band at a fresh coordinate.
|
||
/// Returns the coordinate it was written to.
|
||
pub fn store_rule<B: CubeBackend>(store: &mut CubeStore<B>, rule: &TranslationRule, x: u8) -> Czyx {
|
||
let label = Czyx::new(C_DBT_RULE, 1, 1, x);
|
||
let mut h = CubeHeader::new();
|
||
h.title = Some(rule.name.clone());
|
||
h.doc_type = Some(Kind::Variant.as_str().into());
|
||
h.size_bytes = Some(rule.to_bytes().len() as u64);
|
||
h.refresh_flags();
|
||
store.put_record(label, &h, &rule.to_bytes());
|
||
label
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use cubecode::opcode::Op;
|
||
use cubecode::RunResult;
|
||
use cubecoords::Czyx;
|
||
|
||
fn sample_cell(coord: Czyx, code: Vec<Op>) -> CodeCell {
|
||
let mut h = CubeHeader::new();
|
||
h.title = Some("sample".into());
|
||
h.doc_type = Some(Kind::Fn.as_str().into());
|
||
h.refresh_flags();
|
||
CodeCell::from_record(coord, &h, &cubecode::opcode::encode(&code))
|
||
.expect("sample is valid bytecode")
|
||
}
|
||
|
||
#[test]
|
||
fn rule_round_trips_through_bytes() {
|
||
let r = TranslationRule {
|
||
name: "double-add".into(),
|
||
target: OpClass::Add,
|
||
fragment: vec![Op::Const(2), Op::Mul],
|
||
};
|
||
let bytes = r.to_bytes();
|
||
let back = TranslationRule::from_bytes(&bytes).expect("rule decodes");
|
||
assert_eq!(r, back);
|
||
}
|
||
|
||
#[test]
|
||
fn cache_patches_and_runs() {
|
||
// Original: Const 3, Const 4, Add, Halt => 3+4 = 7.
|
||
let orig = sample_cell(
|
||
Czyx::new(1, 1, 1, 1),
|
||
vec![Op::Const(3), Op::Const(4), Op::Add, Op::Halt],
|
||
);
|
||
// Rule: replace `Add` with `Const 2, Mul` => (a)*(2). With a=3,b=4:
|
||
// naive in-place substitution yields Const3, Const4, Const2, Mul =>
|
||
// 4*2 = 8. This proves the patched fragment is what runs.
|
||
let rule = TranslationRule {
|
||
name: "mul-by-2".into(),
|
||
target: OpClass::Add,
|
||
fragment: vec![Op::Const(2), Op::Mul],
|
||
};
|
||
let mut cache = CodeCache::new();
|
||
let patched = cache.patch(&orig, &[rule]);
|
||
assert_eq!(patched.kind(), Kind::Variant, "cache entry is a Variant");
|
||
match cache.run(&patched) {
|
||
RunResult::Halted { top: Some(v) } => assert_eq!(v, 8, "patched semantics differ"),
|
||
other => panic!("patched run failed: {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn runtime_mimic_end_to_end() {
|
||
let mut store = CubeStore::new(HashBackend::new());
|
||
// Original target at c1.
|
||
let target = Czyx::new(1, 1, 1, 1);
|
||
let orig = sample_cell(target, vec![Op::Const(10), Op::Const(5), Op::Sub, Op::Halt]);
|
||
store.put_record(orig.label, &orig.header, &orig.body());
|
||
|
||
// Rule in the c220 band: replace Sub with Add (10+5=15 instead of 5).
|
||
let rule = TranslationRule {
|
||
name: "sub->add".into(),
|
||
target: OpClass::Sub,
|
||
fragment: vec![Op::Add],
|
||
};
|
||
store_rule(&mut store, &rule, 1);
|
||
|
||
let rt = DbRuntime::new(store);
|
||
let rules = rt.discover_rules();
|
||
assert_eq!(rules.len(), 1, "rule discovered in c220 band");
|
||
match rt.mimic(target) {
|
||
Some(RunResult::Halted { top: Some(v) }) => assert_eq!(v, 15, "mimic applied rule"),
|
||
other => panic!("mimic failed: {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn mimic_without_rules_matches_original() {
|
||
let mut store = CubeStore::new(HashBackend::new());
|
||
let target = Czyx::new(1, 1, 1, 2);
|
||
let orig = sample_cell(target, vec![Op::Const(7), Op::Const(2), Op::Mul, Op::Halt]);
|
||
store.put_record(orig.label, &orig.header, &orig.body());
|
||
let rt = DbRuntime::new(store);
|
||
match rt.mimic(target) {
|
||
Some(RunResult::Halted { top: Some(v) }) => assert_eq!(v, 14),
|
||
other => panic!("mimic without rules should run original: {:?}", other),
|
||
}
|
||
}
|
||
}
|