Skip to main content

logicaffeine_base/
intern.rs

1//! String interning for O(1) equality comparison.
2//!
3//! Symbols are lightweight integer handles that point to interned strings.
4//! By storing each unique string exactly once and comparing integer handles,
5//! we achieve O(1) equality checks regardless of string length.
6//!
7//! ## Example
8//!
9//! ```
10//! use logicaffeine_base::{Interner, Symbol};
11//!
12//! let mut interner = Interner::new();
13//!
14//! let s1 = interner.intern("hello");
15//! let s2 = interner.intern("hello");  // Same string
16//! let s3 = interner.intern("world");  // Different string
17//!
18//! // Same strings produce same symbols (O(1) comparison)
19//! assert_eq!(s1, s2);
20//! assert_ne!(s1, s3);
21//!
22//! // Resolve back to strings when needed
23//! assert_eq!(interner.resolve(s1), "hello");
24//! ```
25//!
26//! ## Use Cases
27//!
28//! - **Variable names**: Compared frequently during scope lookup
29//! - **Keywords**: Compared during lexing and parsing
30//! - **Type names**: Compared during type checking
31
32use std::collections::HashMap;
33
34/// A lightweight handle to an interned string.
35///
36/// Symbols are `Copy` and compare in O(1) time regardless of string length.
37/// Use [`Interner::resolve`] to retrieve the original string.
38#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
39pub struct Symbol(u32);
40
41impl Symbol {
42    /// The empty string symbol, always at index 0.
43    pub const EMPTY: Symbol = Symbol(0);
44
45    /// Returns the internal index of this symbol.
46    ///
47    /// Useful for dense storage (e.g., indexing into a `Vec` instead of a `HashMap`).
48    pub fn index(self) -> usize {
49        self.0 as usize
50    }
51
52    /// Reconstructs a symbol from an internal index — the inverse of
53    /// [`Symbol::index`]. Sound only for an index produced by `index()` on a
54    /// symbol from the same interner (the bounds prover round-trips symbols
55    /// through their dense indices as `LinearExpr` variable ids).
56    pub fn from_index(i: usize) -> Symbol {
57        Symbol(i as u32)
58    }
59}
60
61impl Default for Symbol {
62    fn default() -> Self {
63        Self::EMPTY
64    }
65}
66
67/// A string interner providing O(1) equality comparison via [`Symbol`] handles.
68///
69/// Each unique string is stored exactly once. Interning the same string twice
70/// returns the same symbol, enabling fast equality checks by comparing integers.
71#[derive(Clone)]
72pub struct Interner {
73    map: HashMap<String, Symbol>,
74    vec: Vec<String>,
75}
76
77impl Interner {
78    /// Creates an interner with only the empty string pre-interned.
79    pub fn new() -> Self {
80        let mut interner = Interner {
81            map: HashMap::new(),
82            vec: Vec::new(),
83        };
84        interner.vec.push(String::new());
85        interner
86    }
87
88    /// Interns a string, returning its symbol.
89    ///
90    /// Returns the existing symbol if the string was already interned.
91    pub fn intern(&mut self, s: &str) -> Symbol {
92        if let Some(&sym) = self.map.get(s) {
93            return sym;
94        }
95        let sym = Symbol(self.vec.len() as u32);
96        self.vec.push(s.to_string());
97        self.map.insert(s.to_string(), sym);
98        sym
99    }
100
101    /// Returns the string for the given symbol.
102    ///
103    /// # Panics
104    ///
105    /// Panics if `sym` was not created by this interner.
106    pub fn resolve(&self, sym: Symbol) -> &str {
107        &self.vec[sym.0 as usize]
108    }
109
110    /// Looks up an existing interned string without creating a new entry.
111    ///
112    /// Returns `None` if the string has not been interned.
113    pub fn lookup(&self, s: &str) -> Option<Symbol> {
114        self.map.get(s).copied()
115    }
116
117    /// Returns the number of interned strings, including the empty string.
118    pub fn len(&self) -> usize {
119        self.vec.len()
120    }
121
122    /// Returns `true` if no strings have been interned (only the empty string is present).
123    pub fn is_empty(&self) -> bool {
124        self.vec.len() <= 1
125    }
126}
127
128impl Default for Interner {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134/// Convenience trait for comparing a [`Symbol`] to a string literal.
135///
136/// Avoids the need to call `interner.resolve(sym) == "..."` repeatedly.
137pub trait SymbolEq {
138    /// Returns `true` if this symbol resolves to the given string.
139    fn is(&self, interner: &Interner, s: &str) -> bool;
140}
141
142impl SymbolEq for Symbol {
143    #[inline]
144    fn is(&self, interner: &Interner, s: &str) -> bool {
145        interner.resolve(*self) == s
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn intern_returns_same_symbol_for_same_string() {
155        let mut interner = Interner::new();
156        let s1 = interner.intern("hello");
157        let s2 = interner.intern("hello");
158        assert_eq!(s1, s2);
159    }
160
161    #[test]
162    fn intern_returns_different_symbols_for_different_strings() {
163        let mut interner = Interner::new();
164        let s1 = interner.intern("hello");
165        let s2 = interner.intern("world");
166        assert_ne!(s1, s2);
167    }
168
169    #[test]
170    fn resolve_returns_original_string() {
171        let mut interner = Interner::new();
172        let sym = interner.intern("test");
173        assert_eq!(interner.resolve(sym), "test");
174    }
175
176    #[test]
177    fn empty_symbol_resolves_to_empty_string() {
178        let interner = Interner::new();
179        assert_eq!(interner.resolve(Symbol::EMPTY), "");
180    }
181
182    #[test]
183    fn symbols_are_copy() {
184        let mut interner = Interner::new();
185        let s1 = interner.intern("copy_test");
186        let s2 = s1;
187        assert_eq!(s1, s2);
188        assert_eq!(interner.resolve(s1), interner.resolve(s2));
189    }
190
191    #[test]
192    fn symbol_equality_is_fast() {
193        let mut interner = Interner::new();
194        let s1 = interner.intern("a_very_long_string_that_would_be_slow_to_compare");
195        let s2 = interner.intern("a_very_long_string_that_would_be_slow_to_compare");
196        assert_eq!(s1, s2);
197    }
198
199    #[test]
200    fn len_tracks_interned_count() {
201        let mut interner = Interner::new();
202        assert_eq!(interner.len(), 1);
203        interner.intern("first");
204        assert_eq!(interner.len(), 2);
205        interner.intern("second");
206        assert_eq!(interner.len(), 3);
207        interner.intern("first");
208        assert_eq!(interner.len(), 3);
209    }
210
211    #[test]
212    fn is_empty_after_new() {
213        let interner = Interner::new();
214        assert!(interner.is_empty());
215    }
216
217    #[test]
218    fn not_empty_after_intern() {
219        let mut interner = Interner::new();
220        interner.intern("something");
221        assert!(!interner.is_empty());
222    }
223
224    #[test]
225    fn symbol_index_matches_position() {
226        let mut interner = Interner::new();
227        let s1 = interner.intern("first");
228        let s2 = interner.intern("second");
229        assert_eq!(s1.index(), 1);
230        assert_eq!(s2.index(), 2);
231    }
232
233    #[test]
234    fn symbol_is_matches_interned_string() {
235        let mut interner = Interner::new();
236        let sym = interner.intern("test");
237        assert!(sym.is(&interner, "test"));
238    }
239
240    #[test]
241    fn symbol_is_rejects_different_string() {
242        let mut interner = Interner::new();
243        let sym = interner.intern("hello");
244        assert!(!sym.is(&interner, "world"));
245    }
246
247    #[test]
248    fn symbol_is_case_sensitive() {
249        let mut interner = Interner::new();
250        let sym = interner.intern("Test");
251        assert!(!sym.is(&interner, "test"));
252        assert!(sym.is(&interner, "Test"));
253    }
254
255    #[test]
256    fn symbol_empty_is_empty_string() {
257        let interner = Interner::new();
258        assert!(Symbol::EMPTY.is(&interner, ""));
259    }
260}