Skip to main content

oxilean_kernel/hash_cons/
arena.rs

1//! `HashConsArena` — deduplicated `Arena<Expr>`.
2
3use crate::arena::{Arena, Idx};
4use crate::{BinderInfo, Expr, FVarId, Level, Literal, Name};
5use std::collections::HashMap;
6
7use super::key::ExprKey;
8use super::stats::HashConsStats;
9
10/// A hash-consing wrapper around `Arena<Expr>`.
11///
12/// Every `mk_*` method checks a `HashMap<ExprKey, Idx<Expr>>` before
13/// allocating. If an equal node already exists in the arena the existing
14/// `Idx` is returned (a *hit*). Otherwise the node is allocated and
15/// indexed (a *miss*).
16///
17/// # Structural sharing guarantee
18///
19/// For any two calls `mk_foo(…args…)` with structurally identical arguments,
20/// the returned `Idx<Expr>` values are equal. This means:
21///
22/// - `O(1)` equality of deduplicated nodes via index comparison
23/// - Significantly reduced arena memory for expression-heavy workloads
24///   (e.g. type checking with many repeated sub-expressions)
25pub struct HashConsArena {
26    /// Backing arena; all nodes live here.
27    arena: Arena<Expr>,
28    /// Cache: expression content → arena index.
29    cache: HashMap<ExprKey, Idx<Expr>>,
30    /// Hit counter.
31    hits: u64,
32    /// Miss counter.
33    misses: u64,
34}
35
36impl HashConsArena {
37    /// Creates an empty `HashConsArena`.
38    pub fn new() -> Self {
39        Self {
40            arena: Arena::new(),
41            cache: HashMap::new(),
42            hits: 0,
43            misses: 0,
44        }
45    }
46
47    /// Creates a `HashConsArena` with pre-allocated capacity for `cap` nodes.
48    pub fn with_capacity(cap: usize) -> Self {
49        Self {
50            arena: Arena::with_capacity(cap),
51            cache: HashMap::with_capacity(cap),
52            hits: 0,
53            misses: 0,
54        }
55    }
56
57    // -----------------------------------------------------------------------
58    // Internal helpers
59    // -----------------------------------------------------------------------
60
61    /// Core deduplication: look up `key`; allocate `expr` on a miss.
62    ///
63    /// Takes both `key` (for lookup) and `expr` (for insertion) separately so
64    /// the caller can build the key cheaply and only clone the full `Expr`
65    /// when a miss occurs.
66    fn get_or_insert(&mut self, key: ExprKey, expr: Expr) -> Idx<Expr> {
67        if let Some(idx) = self.cache.get(&key) {
68            self.hits += 1;
69            return idx.clone();
70        }
71        self.misses += 1;
72        let idx = self.arena.alloc(expr);
73        self.cache.insert(key, idx.clone());
74        idx
75    }
76
77    // -----------------------------------------------------------------------
78    // Public read API
79    // -----------------------------------------------------------------------
80
81    /// Retrieve the `Expr` stored at `idx`.
82    pub fn get(&self, idx: Idx<Expr>) -> &Expr {
83        self.arena.get(idx)
84    }
85
86    /// Returns the number of distinct nodes in the arena.
87    pub fn len(&self) -> usize {
88        self.arena.len()
89    }
90
91    /// Returns `true` when no nodes have been allocated.
92    pub fn is_empty(&self) -> bool {
93        self.arena.is_empty()
94    }
95
96    /// Returns a statistics snapshot.
97    pub fn stats(&self) -> HashConsStats {
98        HashConsStats {
99            hits: self.hits,
100            misses: self.misses,
101            total: self.hits + self.misses,
102            unique_nodes: self.arena.len(),
103        }
104    }
105
106    /// Borrow the underlying `Arena<Expr>` for read-only access.
107    pub fn arena(&self) -> &Arena<Expr> {
108        &self.arena
109    }
110
111    // -----------------------------------------------------------------------
112    // mk_* constructors
113    // -----------------------------------------------------------------------
114
115    /// Allocate (or deduplicate) `Expr::Sort(level)`.
116    pub fn mk_sort(&mut self, level: Level) -> Idx<Expr> {
117        let key = ExprKey::sort(level.clone());
118        let expr = Expr::Sort(level);
119        self.get_or_insert(key, expr)
120    }
121
122    /// Allocate (or deduplicate) `Expr::BVar(idx)`.
123    pub fn mk_bvar(&mut self, idx: u32) -> Idx<Expr> {
124        let key = ExprKey::bvar(idx);
125        let expr = Expr::BVar(idx);
126        self.get_or_insert(key, expr)
127    }
128
129    /// Allocate (or deduplicate) `Expr::FVar(id)`.
130    pub fn mk_fvar(&mut self, id: FVarId) -> Idx<Expr> {
131        let key = ExprKey::fvar(id);
132        let expr = Expr::FVar(id);
133        self.get_or_insert(key, expr)
134    }
135
136    /// Allocate (or deduplicate) `Expr::Const(name, levels)`.
137    pub fn mk_const(&mut self, name: Name, levels: Vec<Level>) -> Idx<Expr> {
138        let key = ExprKey::const_(name.clone(), levels.clone());
139        let expr = Expr::Const(name, levels);
140        self.get_or_insert(key, expr)
141    }
142
143    /// Allocate (or deduplicate) `Expr::App(f_expr, a_expr)`.
144    ///
145    /// `f_expr` and `a_expr` are cloned once to build the `ExprKey`; no
146    /// extra clones occur on a cache hit.
147    pub fn mk_app(&mut self, f_expr: Expr, a_expr: Expr) -> Idx<Expr> {
148        let key = ExprKey::app(f_expr.clone(), a_expr.clone());
149        let expr = Expr::App(Box::new(f_expr), Box::new(a_expr));
150        self.get_or_insert(key, expr)
151    }
152
153    /// Allocate (or deduplicate) `Expr::Lam(bi, name, dom, body)`.
154    pub fn mk_lam(&mut self, bi: BinderInfo, name: Name, dom: Expr, body: Expr) -> Idx<Expr> {
155        let key = ExprKey::lam(bi, name.clone(), dom.clone(), body.clone());
156        let expr = Expr::Lam(bi, name, Box::new(dom), Box::new(body));
157        self.get_or_insert(key, expr)
158    }
159
160    /// Allocate (or deduplicate) `Expr::Pi(bi, name, dom, cod)`.
161    pub fn mk_pi(&mut self, bi: BinderInfo, name: Name, dom: Expr, cod: Expr) -> Idx<Expr> {
162        let key = ExprKey::pi(bi, name.clone(), dom.clone(), cod.clone());
163        let expr = Expr::Pi(bi, name, Box::new(dom), Box::new(cod));
164        self.get_or_insert(key, expr)
165    }
166
167    /// Allocate (or deduplicate) `Expr::Let(name, ty, val, body)`.
168    pub fn mk_let(&mut self, name: Name, ty: Expr, val: Expr, body: Expr) -> Idx<Expr> {
169        let key = ExprKey::let_(name.clone(), ty.clone(), val.clone(), body.clone());
170        let expr = Expr::Let(name, Box::new(ty), Box::new(val), Box::new(body));
171        self.get_or_insert(key, expr)
172    }
173
174    /// Allocate (or deduplicate) `Expr::Lit(lit)`.
175    pub fn mk_lit(&mut self, lit: Literal) -> Idx<Expr> {
176        let key = ExprKey::lit(lit.clone());
177        let expr = Expr::Lit(lit);
178        self.get_or_insert(key, expr)
179    }
180
181    /// Allocate (or deduplicate) `Expr::Proj(name, field_idx, struct_expr)`.
182    pub fn mk_proj(&mut self, name: Name, field_idx: u32, struct_expr: Expr) -> Idx<Expr> {
183        let key = ExprKey::proj(name.clone(), field_idx, struct_expr.clone());
184        let expr = Expr::Proj(name, field_idx, Box::new(struct_expr));
185        self.get_or_insert(key, expr)
186    }
187}
188
189impl Default for HashConsArena {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195// ---------------------------------------------------------------------------
196// Tests
197// ---------------------------------------------------------------------------
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::{Level, Name};
203
204    // ---- mk_sort -----------------------------------------------------------
205
206    #[test]
207    fn test_mk_sort_deduplicates() {
208        let mut hc = HashConsArena::new();
209        let i1 = hc.mk_sort(Level::Zero);
210        let i2 = hc.mk_sort(Level::Zero);
211        assert_eq!(i1, i2);
212        assert_eq!(hc.len(), 1);
213    }
214
215    #[test]
216    fn test_mk_sort_distinct_levels() {
217        let mut hc = HashConsArena::new();
218        let i0 = hc.mk_sort(Level::Zero);
219        let i1 = hc.mk_sort(Level::succ(Level::Zero));
220        assert_ne!(i0, i1);
221        assert_eq!(hc.len(), 2);
222    }
223
224    // ---- mk_bvar -----------------------------------------------------------
225
226    #[test]
227    fn test_mk_bvar_deduplicates() {
228        let mut hc = HashConsArena::new();
229        let i1 = hc.mk_bvar(0);
230        let i2 = hc.mk_bvar(0);
231        assert_eq!(i1, i2);
232    }
233
234    #[test]
235    fn test_mk_bvar_distinct_indices() {
236        let mut hc = HashConsArena::new();
237        let i0 = hc.mk_bvar(0);
238        let i1 = hc.mk_bvar(1);
239        assert_ne!(i0, i1);
240    }
241
242    // ---- mk_fvar -----------------------------------------------------------
243
244    #[test]
245    fn test_mk_fvar_deduplicates() {
246        let mut hc = HashConsArena::new();
247        let id = FVarId::new(7);
248        let i1 = hc.mk_fvar(id);
249        let i2 = hc.mk_fvar(id);
250        assert_eq!(i1, i2);
251    }
252
253    // ---- mk_const ----------------------------------------------------------
254
255    #[test]
256    fn test_mk_const_deduplicates() {
257        let mut hc = HashConsArena::new();
258        let name = Name::str("Nat");
259        let i1 = hc.mk_const(name.clone(), vec![]);
260        let i2 = hc.mk_const(name, vec![]);
261        assert_eq!(i1, i2);
262    }
263
264    #[test]
265    fn test_mk_const_different_names_distinct() {
266        let mut hc = HashConsArena::new();
267        let i1 = hc.mk_const(Name::str("Nat"), vec![]);
268        let i2 = hc.mk_const(Name::str("Int"), vec![]);
269        assert_ne!(i1, i2);
270    }
271
272    // ---- mk_app ------------------------------------------------------------
273
274    #[test]
275    fn test_mk_app_deduplicates() {
276        let mut hc = HashConsArena::new();
277        let f = Expr::BVar(0);
278        let a = Expr::BVar(1);
279        let i1 = hc.mk_app(f.clone(), a.clone());
280        let i2 = hc.mk_app(f, a);
281        assert_eq!(i1, i2);
282    }
283
284    #[test]
285    fn test_mk_app_different_args_distinct() {
286        let mut hc = HashConsArena::new();
287        let i1 = hc.mk_app(Expr::BVar(0), Expr::BVar(1));
288        let i2 = hc.mk_app(Expr::BVar(0), Expr::BVar(2));
289        assert_ne!(i1, i2);
290    }
291
292    // ---- mk_lam ------------------------------------------------------------
293
294    #[test]
295    fn test_mk_lam_deduplicates() {
296        let mut hc = HashConsArena::new();
297        let name = Name::str("x");
298        let dom = Expr::Sort(Level::Zero);
299        let body = Expr::BVar(0);
300        let i1 = hc.mk_lam(BinderInfo::Default, name.clone(), dom.clone(), body.clone());
301        let i2 = hc.mk_lam(BinderInfo::Default, name, dom, body);
302        assert_eq!(i1, i2);
303    }
304
305    // ---- mk_pi -------------------------------------------------------------
306
307    #[test]
308    fn test_mk_pi_deduplicates() {
309        let mut hc = HashConsArena::new();
310        let name = Name::str("A");
311        let dom = Expr::Sort(Level::Zero);
312        let cod = Expr::BVar(0);
313        let i1 = hc.mk_pi(BinderInfo::Default, name.clone(), dom.clone(), cod.clone());
314        let i2 = hc.mk_pi(BinderInfo::Default, name, dom, cod);
315        assert_eq!(i1, i2);
316    }
317
318    // ---- mk_let ------------------------------------------------------------
319
320    #[test]
321    fn test_mk_let_deduplicates() {
322        let mut hc = HashConsArena::new();
323        let name = Name::str("n");
324        let ty = Expr::Sort(Level::Zero);
325        let val = Expr::BVar(0);
326        let body = Expr::BVar(1);
327        let i1 = hc.mk_let(name.clone(), ty.clone(), val.clone(), body.clone());
328        let i2 = hc.mk_let(name, ty, val, body);
329        assert_eq!(i1, i2);
330    }
331
332    // ---- mk_lit / mk_proj --------------------------------------------------
333
334    #[test]
335    fn test_mk_lit_nat_deduplicates() {
336        let mut hc = HashConsArena::new();
337        let i1 = hc.mk_lit(Literal::Nat(42));
338        let i2 = hc.mk_lit(Literal::Nat(42));
339        assert_eq!(i1, i2);
340    }
341
342    #[test]
343    fn test_mk_lit_str_deduplicates() {
344        let mut hc = HashConsArena::new();
345        let i1 = hc.mk_lit(Literal::Str("hello".to_string()));
346        let i2 = hc.mk_lit(Literal::Str("hello".to_string()));
347        assert_eq!(i1, i2);
348    }
349
350    #[test]
351    fn test_mk_proj_deduplicates() {
352        let mut hc = HashConsArena::new();
353        let name = Name::str("fst");
354        let se = Expr::BVar(0);
355        let i1 = hc.mk_proj(name.clone(), 0, se.clone());
356        let i2 = hc.mk_proj(name, 0, se);
357        assert_eq!(i1, i2);
358    }
359
360    // ---- stats -------------------------------------------------------------
361
362    #[test]
363    fn test_stats_hit_miss_tracking() {
364        let mut hc = HashConsArena::new();
365        hc.mk_sort(Level::Zero); // miss
366        hc.mk_sort(Level::Zero); // hit
367        hc.mk_sort(Level::Zero); // hit
368        hc.mk_bvar(0); // miss
369        let s = hc.stats();
370        assert_eq!(s.misses, 2);
371        assert_eq!(s.hits, 2);
372        assert_eq!(s.total, 4);
373        assert_eq!(s.unique_nodes, 2);
374    }
375
376    #[test]
377    fn test_stats_dedup_ratio() {
378        let mut hc = HashConsArena::new();
379        hc.mk_bvar(0); // miss
380        hc.mk_bvar(0); // hit
381        hc.mk_bvar(0); // hit
382        hc.mk_bvar(0); // hit
383        let s = hc.stats();
384        // dedup_ratio = misses / total = 1/4 = 0.25
385        assert!((s.dedup_ratio() - 0.25).abs() < 1e-10);
386    }
387
388    #[test]
389    fn test_get_returns_correct_expr() {
390        let mut hc = HashConsArena::new();
391        let idx = hc.mk_bvar(5);
392        assert_eq!(hc.get(idx), &Expr::BVar(5));
393    }
394
395    #[test]
396    fn test_is_empty_initially() {
397        let hc = HashConsArena::new();
398        assert!(hc.is_empty());
399    }
400
401    #[test]
402    fn test_with_capacity_works() {
403        let mut hc = HashConsArena::with_capacity(64);
404        hc.mk_sort(Level::Zero);
405        assert_eq!(hc.len(), 1);
406    }
407
408    #[test]
409    fn test_default_is_empty() {
410        let hc = HashConsArena::default();
411        assert!(hc.is_empty());
412    }
413
414    #[test]
415    fn test_cross_variant_no_collision() {
416        let mut hc = HashConsArena::new();
417        // BVar(0) and Sort(Zero) must NOT collide even though both are "zero-ish"
418        let bv = hc.mk_bvar(0);
419        let sv = hc.mk_sort(Level::Zero);
420        assert_ne!(bv, sv);
421    }
422
423    #[test]
424    fn test_binder_info_distinguishes_lam() {
425        let mut hc = HashConsArena::new();
426        let name = Name::str("x");
427        let dom = Expr::Sort(Level::Zero);
428        let body = Expr::BVar(0);
429        let explicit = hc.mk_lam(BinderInfo::Default, name.clone(), dom.clone(), body.clone());
430        let implicit = hc.mk_lam(BinderInfo::Implicit, name, dom, body);
431        assert_ne!(explicit, implicit);
432    }
433
434    #[test]
435    fn test_complex_expression_sharing() {
436        let mut hc = HashConsArena::new();
437        // Build `id : Π (A : Type 0), A → A` structurally, twice.
438        let prop = Expr::Sort(Level::Zero);
439        let bv0 = Expr::BVar(0);
440        // A → A  =  Π (_ : A), A
441        let arr = Expr::Pi(
442            BinderInfo::Default,
443            Name::Anonymous,
444            Box::new(bv0.clone()),
445            Box::new(bv0.clone()),
446        );
447        let id_type_1 = hc.mk_pi(
448            BinderInfo::Default,
449            Name::str("A"),
450            prop.clone(),
451            arr.clone(),
452        );
453        let id_type_2 = hc.mk_pi(
454            BinderInfo::Default,
455            Name::str("A"),
456            prop.clone(),
457            arr.clone(),
458        );
459        assert_eq!(
460            id_type_1, id_type_2,
461            "identical Pi types must be deduplicated"
462        );
463        assert_eq!(hc.len(), 1, "only one node in the arena");
464    }
465}