aleph_syntax_tree/
hash.rs1use crate::syntax::AlephTree;
2
3pub type NodeHash = [u8; 32];
6
7pub fn content_hash(node: &AlephTree) -> NodeHash {
18 let canonical = serde_json::to_vec(node).expect("AlephTree always serializes");
19 *blake3::hash(&canonical).as_bytes()
20}
21
22pub fn content_hash_hex(node: &AlephTree) -> String {
24 blake3::Hash::from(content_hash(node)).to_hex().to_string()
25}
26
27#[cfg(test)]
28mod tests {
29 use super::*;
30 use crate::syntax::AlephTree;
31
32 fn sample() -> AlephTree {
33 AlephTree::LetRec {
34 name: "square".to_string(),
35 args: vec![Box::new(AlephTree::Ident { value: "n".to_string() })],
36 body: Box::new(AlephTree::Mul {
37 number_expr1: Box::new(AlephTree::Ident { value: "n".to_string() }),
38 number_expr2: Box::new(AlephTree::Ident { value: "n".to_string() }),
39 }),
40 }
41 }
42
43 #[test]
44 fn same_structural_tree_hashes_identically() {
45 assert_eq!(content_hash(&sample()), content_hash(&sample()));
46 }
47
48 #[test]
49 fn different_tree_hashes_differently() {
50 let other = match sample() {
51 AlephTree::LetRec { args, body, .. } => AlephTree::LetRec {
52 name: "cube".to_string(),
53 args,
54 body,
55 },
56 _ => unreachable!(),
57 };
58 assert_ne!(content_hash(&sample()), content_hash(&other));
59 }
60
61 #[test]
62 fn hash_is_32_bytes() {
63 assert_eq!(content_hash(&sample()).len(), 32);
64 }
65
66 #[test]
67 fn hex_form_is_64_lowercase_hex_chars() {
68 let hex = content_hash_hex(&sample());
69 assert_eq!(hex.len(), 64);
70 assert!(hex.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
71 }
72
73 #[test]
74 fn tree_with_extra_arg_hashes_differently() {
75 let with_extra_arg = AlephTree::LetRec {
76 name: "square".to_string(),
77 args: vec![
78 Box::new(AlephTree::Ident { value: "n".to_string() }),
79 Box::new(AlephTree::Ident { value: "unused".to_string() }),
80 ],
81 body: Box::new(AlephTree::Mul {
82 number_expr1: Box::new(AlephTree::Ident { value: "n".to_string() }),
83 number_expr2: Box::new(AlephTree::Ident { value: "n".to_string() }),
84 }),
85 };
86 assert_ne!(content_hash(&sample()), content_hash(&with_extra_arg));
87 }
88
89 #[test]
90 fn with_effects_hash_is_independent_of_effect_insertion_order() {
91 use crate::effects::{Effect, EffectSet};
92
93 let a = AlephTree::WithEffects {
94 inner: Box::new(sample()),
95 effects: EffectSet::from([Effect::Io, Effect::Net]),
96 };
97 let b = AlephTree::WithEffects {
98 inner: Box::new(sample()),
99 effects: EffectSet::from([Effect::Net, Effect::Io]),
100 };
101 assert_eq!(content_hash(&a), content_hash(&b));
102 }
103
104 #[test]
105 fn known_input_hashes_to_a_pinned_value() {
106 assert_eq!(
111 content_hash_hex(&sample()),
112 "1c5065803a4fbd2950297d8a3aa98b16f4dfc878e96a1492d462b6fee7f3c257"
113 );
114 }
115}