Skip to main content

foundry_evm_symbolic/runtime/expr/
hashcons.rs

1use alloy_primitives::map::foldhash::fast::FixedState;
2use hashbrown::{HashTable, hash_table::Entry};
3use std::{
4    cmp::Ordering,
5    fmt,
6    hash::{BuildHasher, Hash, Hasher},
7    sync::{Arc, Weak},
8};
9
10/// Shared handle for a hash-consed value.
11///
12/// Equality is pointer equality only. Hashing writes the cached structural hash
13/// instead of walking the value.
14pub(in crate::runtime) struct HashConsed<T> {
15    inner: Arc<HashConsedInner<T>>,
16}
17
18struct HashConsedInner<T> {
19    hash: u64,
20    value: T,
21}
22
23impl<T> HashConsed<T> {
24    #[inline]
25    pub(in crate::runtime::expr) fn stable_hash_cmp(&self, other: &Self) -> Ordering {
26        self.inner.hash.cmp(&other.inner.hash)
27    }
28
29    /// Orders nodes within one hash-consing context without inspecting or rendering their value.
30    ///
31    /// The cached structural hash handles the common case. Pointer identity is only a tie-breaker
32    /// for distinct nodes with the same hash; structurally equal values share one node.
33    #[inline]
34    pub(in crate::runtime::expr) fn identity_cmp(&self, other: &Self) -> Ordering {
35        self.inner.hash.cmp(&other.inner.hash).then_with(|| {
36            let left = Arc::as_ptr(&self.inner);
37            let right = Arc::as_ptr(&other.inner);
38            left.cmp(&right)
39        })
40    }
41
42    #[inline]
43    pub(in crate::runtime) fn value(&self) -> &T {
44        &self.inner.value
45    }
46}
47
48impl<T> Clone for HashConsed<T> {
49    #[inline]
50    fn clone(&self) -> Self {
51        Self { inner: self.inner.clone() }
52    }
53}
54
55impl<T> PartialEq for HashConsed<T> {
56    #[inline]
57    fn eq(&self, other: &Self) -> bool {
58        Arc::ptr_eq(&self.inner, &other.inner)
59    }
60}
61
62impl<T> Eq for HashConsed<T> {}
63
64impl<T> Hash for HashConsed<T> {
65    #[inline]
66    fn hash<H: Hasher>(&self, state: &mut H) {
67        self.inner.hash.hash(state);
68    }
69}
70
71impl<T: PartialOrd> PartialOrd for HashConsed<T> {
72    #[inline]
73    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
74        self.value().partial_cmp(other.value())
75    }
76}
77
78impl<T: Ord> Ord for HashConsed<T> {
79    #[inline]
80    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
81        self.value().cmp(other.value())
82    }
83}
84
85impl<T: fmt::Debug> fmt::Debug for HashConsed<T> {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        self.value().fmt(f)
88    }
89}
90
91type HashConsHasher = FixedState;
92const MIN_GC_THRESHOLD: usize = 1024;
93
94/// Hash-consing table for sharing structurally equal immutable values.
95///
96/// The table stores weak references so interned values disappear when the rest of
97/// the symbolic state stops using them. `make` removes dead entries encountered
98/// during lookup and periodically sweeps distinct dead values before they can
99/// grow the table without bound.
100pub(in crate::runtime) struct HashCons<T> {
101    table: HashTable<HashConsEntry<T>>,
102    hash_builder: HashConsHasher,
103    gc_threshold: usize,
104}
105
106struct HashConsEntry<T> {
107    hash: u64,
108    value: Weak<HashConsedInner<T>>,
109}
110
111impl<T> HashConsEntry<T> {
112    const fn hash(&self) -> u64 {
113        self.hash
114    }
115}
116
117impl<T> HashCons<T> {
118    pub(in crate::runtime) fn new() -> Self {
119        Self {
120            table: HashTable::new(),
121            hash_builder: HashConsHasher::default(),
122            gc_threshold: MIN_GC_THRESHOLD,
123        }
124    }
125
126    fn hash<Q: Hash + ?Sized>(&self, value: &Q) -> u64 {
127        self.hash_builder.hash_one(value)
128    }
129}
130
131impl<T: Eq + Hash> HashCons<T> {
132    pub(in crate::runtime) fn make(&mut self, value: T) -> HashConsed<T> {
133        if self.table.len() >= self.gc_threshold {
134            self.table.retain(|entry| entry.value.strong_count() != 0);
135            self.gc_threshold = self.table.len().saturating_mul(2).max(MIN_GC_THRESHOLD);
136        }
137
138        let hash = self.hash(&value);
139        loop {
140            let mut found = None;
141            let mut matched_dead_entry = false;
142            match self.table.entry(
143                hash,
144                |entry| {
145                    if entry.hash != hash {
146                        return false;
147                    }
148                    match entry.value.upgrade() {
149                        Some(existing) if existing.value == value => {
150                            found = Some(existing);
151                            true
152                        }
153                        None => {
154                            matched_dead_entry = true;
155                            true
156                        }
157                        Some(_) => false,
158                    }
159                },
160                HashConsEntry::hash,
161            ) {
162                Entry::Occupied(entry) => {
163                    if let Some(inner) = found {
164                        return HashConsed { inner };
165                    }
166                    debug_assert!(matched_dead_entry);
167                    let _ = entry.remove();
168                }
169                Entry::Vacant(entry) => {
170                    let inner = Arc::new(HashConsedInner { hash, value });
171                    entry.insert(HashConsEntry { hash, value: Arc::downgrade(&inner) });
172                    return HashConsed { inner };
173                }
174            }
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn make_reuses_existing_value() {
185        let mut table = HashCons::<String>::new();
186
187        let first = table.make("same".to_string());
188        let second = table.make("same".to_string());
189
190        assert_eq!(first, second);
191        assert_eq!(first.inner.hash, second.inner.hash);
192    }
193
194    #[test]
195    fn make_keeps_distinct_values_apart() {
196        let mut table = HashCons::<String>::new();
197
198        let first = table.make("first".to_string());
199        let second = table.make("second".to_string());
200
201        assert_ne!(first, second);
202    }
203
204    #[test]
205    fn dropped_values_are_not_reused() {
206        let mut table = HashCons::<String>::new();
207
208        let first = table.make("same".to_string());
209        let weak = Arc::downgrade(&first.inner);
210        drop(first);
211        assert!(weak.upgrade().is_none());
212
213        let second = table.make("same".to_string());
214
215        assert_eq!(second.value().as_str(), "same");
216        assert!(weak.upgrade().is_none());
217    }
218
219    #[test]
220    fn make_reclaims_repeatedly_dropped_values() {
221        let mut table = HashCons::<String>::new();
222
223        for _ in 0..128 {
224            drop(table.make("same".to_string()));
225        }
226
227        assert_eq!(table.table.len(), 1);
228    }
229
230    #[test]
231    fn make_reclaims_distinct_dropped_values() {
232        let mut table = HashCons::<String>::new();
233        let retained = table.make("retained".to_string());
234
235        for value in 0..MIN_GC_THRESHOLD - 1 {
236            drop(table.make(value.to_string()));
237        }
238        let same = table.make("retained".to_string());
239
240        assert_eq!(table.table.len(), 1);
241        assert_eq!(retained, same);
242    }
243
244    #[test]
245    fn equality_is_pointer_only() {
246        let mut first_table = HashCons::<String>::new();
247        let mut second_table = HashCons::<String>::new();
248
249        let first = first_table.make("same".to_string());
250        let second = second_table.make("same".to_string());
251
252        assert_ne!(first, second);
253        assert_eq!(first.value(), second.value());
254    }
255}