foundry_evm_symbolic/runtime/expr/
hashcons.rs1use 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
10pub(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 #[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
94pub(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}