use crate::ir::{Node, Program};
use crate::serial::wire::encode::{put_node, put_nodes};
pub fn node_subtree_hash(node: &Node) -> Result<[u8; 32], String> {
let mut buf = Vec::with_capacity(64);
put_node(&mut buf, node).map_err(|e| String::from(e))?;
Ok(*blake3::hash(&buf).as_bytes())
}
pub fn program_subtree_hashes(program: &Program) -> Result<Vec<(usize, [u8; 32])>, String> {
program
.entry()
.iter()
.enumerate()
.map(|(idx, node)| node_subtree_hash(node).map(|hash| (idx, hash)))
.collect()
}
pub fn program_diff(before: &Program, after: &Program) -> Result<Vec<Diff>, String> {
let before_hashes = program_subtree_hashes(before)?;
let after_hashes = program_subtree_hashes(after)?;
let mut out = Vec::with_capacity(before_hashes.len().max(after_hashes.len()));
for idx in 0..before_hashes.len().max(after_hashes.len()) {
match (before_hashes.get(idx), after_hashes.get(idx)) {
(Some(b), Some(a)) if b.1 == a.1 => out.push(Diff::Unchanged {
index: idx,
hash: a.1,
}),
(Some(b), Some(a)) => out.push(Diff::Changed {
index: idx,
before_hash: b.1,
after_hash: a.1,
}),
(Some(b), None) => out.push(Diff::Removed {
index: idx,
hash: b.1,
}),
(None, Some(a)) => out.push(Diff::Added {
index: idx,
hash: a.1,
}),
(None, None) => unreachable!("loop bounds prevent both being None"),
}
}
Ok(out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Diff {
Unchanged {
index: usize,
hash: [u8; 32],
},
Changed {
index: usize,
before_hash: [u8; 32],
after_hash: [u8; 32],
},
Removed {
index: usize,
hash: [u8; 32],
},
Added {
index: usize,
hash: [u8; 32],
},
}
#[allow(dead_code)]
fn _reserved_put_nodes_marker(buf: &mut Vec<u8>, body: &[Node]) -> Result<(), String> {
put_nodes(buf, body).map_err(String::from)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
fn program_with(node: Node) -> Program {
Program::wrapped(
vec![
BufferDecl::storage("buf", 0, BufferAccess::ReadWrite, DataType::U32).with_count(4),
],
[1, 1, 1],
vec![node],
)
}
#[test]
fn identical_nodes_share_hash() {
let n1 = Node::store("buf", Expr::u32(0), Expr::u32(7));
let n2 = Node::store("buf", Expr::u32(0), Expr::u32(7));
let h1 = node_subtree_hash(&n1).expect("encoding must succeed for valid Node");
let h2 = node_subtree_hash(&n2).expect("encoding must succeed for valid Node");
assert_eq!(h1, h2, "identical nodes must produce identical hashes");
}
#[test]
fn differing_nodes_differ_in_hash() {
let n1 = Node::store("buf", Expr::u32(0), Expr::u32(7));
let n2 = Node::store("buf", Expr::u32(0), Expr::u32(8));
let h1 = node_subtree_hash(&n1).expect("encoding must succeed for valid Node");
let h2 = node_subtree_hash(&n2).expect("encoding must succeed for valid Node");
assert_ne!(
h1, h2,
"Nodes that differ in any field must produce different hashes (Merkle property)"
);
}
#[test]
fn nested_change_propagates_to_parent_hash() {
let inner1 = vec![Node::store("buf", Expr::u32(0), Expr::u32(7))];
let inner2 = vec![Node::store("buf", Expr::u32(0), Expr::u32(8))];
let outer1 = Node::if_then(Expr::bool(true), inner1);
let outer2 = Node::if_then(Expr::bool(true), inner2);
let h1 = node_subtree_hash(&outer1).expect("encoding must succeed");
let h2 = node_subtree_hash(&outer2).expect("encoding must succeed");
assert_ne!(
h1, h2,
"changing a leaf must change the parent's hash — without this the cache would \
return stale compiled artifacts after a deep rewrite"
);
}
#[test]
fn program_subtree_hashes_indexes_each_top_level_entry() {
let p1 = program_with(Node::store("buf", Expr::u32(0), Expr::u32(7)));
let p2 = program_with(Node::store("buf", Expr::u32(0), Expr::u32(8)));
let hs1 = program_subtree_hashes(&p1).expect("encoding must succeed");
let hs2 = program_subtree_hashes(&p2).expect("encoding must succeed");
assert_eq!(
hs1.len(),
1,
"Program::wrapped exposes one top-level Region"
);
assert_eq!(hs2.len(), 1);
assert_eq!(hs1[0].0, 0, "index ordering follows program.entry()");
assert_ne!(
hs1[0].1, hs2[0].1,
"differing inner content must produce differing top-level hashes"
);
}
#[test]
fn program_diff_marks_unchanged_entries() {
let p1 = program_with(Node::store("buf", Expr::u32(0), Expr::u32(7)));
let p2 = program_with(Node::store("buf", Expr::u32(0), Expr::u32(7)));
let diffs = program_diff(&p1, &p2).expect("encoding must succeed");
assert_eq!(diffs.len(), 1);
assert!(matches!(diffs[0], Diff::Unchanged { index: 0, .. }));
}
#[test]
fn program_diff_marks_changed_entries() {
let p1 = program_with(Node::store("buf", Expr::u32(0), Expr::u32(7)));
let p2 = program_with(Node::store("buf", Expr::u32(0), Expr::u32(99)));
let diffs = program_diff(&p1, &p2).expect("encoding must succeed");
assert_eq!(diffs.len(), 1);
match &diffs[0] {
Diff::Changed {
index,
before_hash,
after_hash,
} => {
assert_eq!(*index, 0);
assert_ne!(before_hash, after_hash);
}
other => panic!(
"expected Diff::Changed for an entry whose inner content differs; got {other:?}"
),
}
}
#[test]
fn hash_is_stable_across_repeated_calls() {
let node = Node::store("buf", Expr::u32(0), Expr::u32(7));
let h1 = node_subtree_hash(&node).expect("encoding must succeed");
let h2 = node_subtree_hash(&node).expect("encoding must succeed");
let h3 = node_subtree_hash(&node).expect("encoding must succeed");
assert_eq!(h1, h2);
assert_eq!(h2, h3);
}
}