Skip to main content

formualizer_eval/engine/arena/
string_interner.rs

1/// String interning for deduplication of text values and identifiers
2/// Uses FxHashMap for fast lookups and Box<str> to minimize allocations
3use rustc_hash::FxHashMap;
4use std::{fmt, sync::Arc};
5
6/// Reference to an interned string
7#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
8pub struct StringId(u32);
9
10impl StringId {
11    /// Maximum number of strings that can be interned (2^32 - 1)
12    pub const MAX: u32 = u32::MAX - 1;
13
14    /// Invalid/null string ID
15    pub const INVALID: StringId = StringId(u32::MAX);
16
17    pub fn as_u32(self) -> u32 {
18        self.0
19    }
20
21    pub fn from_raw(raw: u32) -> Self {
22        StringId(raw)
23    }
24
25    pub fn is_valid(self) -> bool {
26        self.0 != u32::MAX
27    }
28}
29
30impl fmt::Display for StringId {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        write!(f, "StringId({})", self.0)
33    }
34}
35
36/// Texts freed by [`StringInterner::free_dead`], to drop wherever is
37/// cheapest (dropping it frees them).
38#[derive(Debug, Default)]
39pub(crate) struct StringGarbage {
40    strings: Vec<Arc<str>>,
41    lookup: FxHashMap<Arc<str>, StringId>,
42}
43
44impl StringGarbage {
45    /// Number of freed texts.
46    pub(crate) fn len(&self) -> usize {
47        self.strings.len()
48    }
49}
50
51/// String interner for deduplicating strings
52#[derive(Debug)]
53pub struct StringInterner {
54    /// Storage for interned strings
55    strings: Vec<Arc<str>>,
56    /// Map from string content to ID for deduplication
57    lookup: FxHashMap<Arc<str>, StringId>,
58}
59
60impl StringInterner {
61    pub fn new() -> Self {
62        Self {
63            strings: Vec::new(),
64            lookup: FxHashMap::default(),
65        }
66    }
67
68    pub fn with_capacity(cap: usize) -> Self {
69        Self {
70            strings: Vec::with_capacity(cap),
71            lookup: FxHashMap::with_capacity_and_hasher(cap, Default::default()),
72        }
73    }
74
75    /// Intern a string, returning its ID
76    /// If the string is already interned, returns the existing ID
77    pub fn intern(&mut self, s: &str) -> StringId {
78        // Check if already interned
79        if let Some(&id) = self.lookup.get(s) {
80            return id;
81        }
82
83        // Check for overflow
84        let index = self.strings.len() as u32;
85        if index > StringId::MAX {
86            panic!("String interner overflow: too many strings");
87        }
88
89        // Add new string
90        let id = StringId(index);
91        let boxed: Arc<str> = s.into();
92
93        // We need to clone the boxed string for the lookup key
94        // This is safe because Box<str> is cheap to clone (just pointer copy)
95        self.lookup.insert(boxed.clone(), id);
96        self.strings.push(boxed);
97
98        id
99    }
100
101    /// Get a string by its ID
102    #[inline]
103    pub fn get(&self, id: StringId) -> Option<&str> {
104        if id.is_valid() {
105            self.strings.get(id.0 as usize).map(|s| &**s)
106        } else {
107            None
108        }
109    }
110
111    /// Get a string by its ID, panicking if invalid
112    #[inline]
113    pub fn resolve(&self, id: StringId) -> &str {
114        self.get(id)
115            .unwrap_or_else(|| panic!("Invalid string ID: {id:?}"))
116    }
117
118    /// Check if a string is already interned
119    /// Free the text of every id whose `live` flag is false (ids past
120    /// `live` are kept). Ids stay stable: a freed slot resolves to `""` and
121    /// its text is no longer found by `get_id`/`intern` (re-interning makes
122    /// a new id). Callers guarantee freed ids are never resolved again.
123    ///
124    /// Returns the freed texts instead of dropping them: freeing many small
125    /// allocations is the expensive part, and the caller may do it off the
126    /// critical path (`StringGarbage`).
127    pub(crate) fn free_dead(&mut self, live: &[bool]) -> StringGarbage {
128        let kept = self
129            .strings
130            .iter()
131            .enumerate()
132            .filter(|(i, _)| live.get(*i).copied().unwrap_or(true))
133            .count();
134        if kept == self.strings.len() {
135            return StringGarbage::default();
136        }
137        let empty: Arc<str> = Arc::from("");
138        let mut lookup = FxHashMap::with_capacity_and_hasher(kept, Default::default());
139        let mut dead = Vec::with_capacity(self.strings.len() - kept);
140        for (i, slot) in self.strings.iter_mut().enumerate() {
141            if live.get(i).copied().unwrap_or(true) {
142                lookup.entry(slot.clone()).or_insert(StringId(i as u32));
143            } else {
144                dead.push(std::mem::replace(slot, empty.clone()));
145            }
146        }
147        let old_lookup = std::mem::replace(&mut self.lookup, lookup);
148        StringGarbage {
149            strings: dead,
150            lookup: old_lookup,
151        }
152    }
153
154    pub fn contains(&self, s: &str) -> bool {
155        self.lookup.contains_key(s)
156    }
157
158    /// Get the ID of an already-interned string without interning
159    pub fn get_id(&self, s: &str) -> Option<StringId> {
160        self.lookup.get(s).copied()
161    }
162
163    /// Returns the number of interned strings
164    pub fn len(&self) -> usize {
165        self.strings.len()
166    }
167
168    /// Returns true if no strings are interned
169    pub fn is_empty(&self) -> bool {
170        self.strings.is_empty()
171    }
172
173    /// Returns memory usage in bytes (approximate)
174    pub fn memory_usage(&self) -> usize {
175        // Vector overhead
176        let vec_overhead = self.strings.capacity() * std::mem::size_of::<Box<str>>();
177
178        // String data
179        let string_data: usize = self
180            .strings
181            .iter()
182            .map(|s| s.len() + std::mem::size_of::<Box<str>>())
183            .sum();
184
185        // HashMap overhead (approximate)
186        let map_overhead = self.lookup.capacity()
187            * (std::mem::size_of::<Box<str>>() + std::mem::size_of::<StringId>());
188
189        vec_overhead + string_data + map_overhead
190    }
191
192    /// Clear all interned strings
193    pub fn clear(&mut self) {
194        self.strings.clear();
195        self.lookup.clear();
196    }
197
198    /// Iterate over all interned strings with their IDs
199    pub fn iter(&self) -> impl Iterator<Item = (StringId, &str)> + '_ {
200        self.strings
201            .iter()
202            .enumerate()
203            .map(|(i, s)| (StringId(i as u32), &**s))
204    }
205
206    /// Get statistics about the interner
207    pub fn stats(&self) -> InternerStats {
208        let total_bytes: usize = self.strings.iter().map(|s| s.len()).sum();
209        let unique_count = self.strings.len();
210
211        InternerStats {
212            unique_count,
213            total_bytes,
214            average_length: if unique_count > 0 {
215                total_bytes as f64 / unique_count as f64
216            } else {
217                0.0
218            },
219        }
220    }
221}
222
223impl Default for StringInterner {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229/// Statistics about the string interner
230#[derive(Debug, Clone)]
231pub struct InternerStats {
232    pub unique_count: usize,
233    pub total_bytes: usize,
234    pub average_length: f64,
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn test_string_interning() {
243        let mut interner = StringInterner::new();
244
245        let s1 = interner.intern("Hello");
246        let s2 = interner.intern("Hello");
247        let s3 = interner.intern("World");
248
249        assert_eq!(s1, s2);
250        assert_ne!(s1, s3);
251        assert_eq!(interner.get(s1), Some("Hello"));
252        assert_eq!(interner.get(s3), Some("World"));
253    }
254
255    #[test]
256    fn test_string_memory_efficiency() {
257        let mut interner = StringInterner::new();
258
259        // Intern same string 1000 times
260        let refs: Vec<_> = (0..1000).map(|_| interner.intern("repeated")).collect();
261
262        // Should only store string once
263        assert_eq!(interner.len(), 1);
264
265        // All refs should be the same
266        for r in &refs[1..] {
267            assert_eq!(*r, refs[0]);
268        }
269
270        // Memory usage should be much less than 9000 bytes
271        assert!(interner.memory_usage() < 1000);
272    }
273
274    #[test]
275    fn test_string_interner_capacity() {
276        let mut interner = StringInterner::with_capacity(100);
277
278        // Intern many different strings
279        for i in 0..1000 {
280            let s = format!("string_{i}");
281            let id = interner.intern(&s);
282            assert_eq!(interner.get(id), Some(s.as_str()));
283        }
284
285        assert_eq!(interner.len(), 1000);
286    }
287
288    #[test]
289    fn test_string_interner_contains() {
290        let mut interner = StringInterner::new();
291
292        interner.intern("exists");
293
294        assert!(interner.contains("exists"));
295        assert!(!interner.contains("not_exists"));
296    }
297
298    #[test]
299    fn test_string_interner_get_id() {
300        let mut interner = StringInterner::new();
301
302        let id = interner.intern("test");
303
304        assert_eq!(interner.get_id("test"), Some(id));
305        assert_eq!(interner.get_id("not_interned"), None);
306    }
307
308    #[test]
309    fn test_string_interner_clear() {
310        let mut interner = StringInterner::new();
311
312        interner.intern("one");
313        interner.intern("two");
314        interner.intern("three");
315
316        assert_eq!(interner.len(), 3);
317
318        interner.clear();
319
320        assert_eq!(interner.len(), 0);
321        assert!(interner.is_empty());
322    }
323
324    #[test]
325    fn test_string_interner_iter() {
326        let mut interner = StringInterner::new();
327
328        let id1 = interner.intern("first");
329        let id2 = interner.intern("second");
330        let id3 = interner.intern("third");
331
332        let items: Vec<_> = interner.iter().collect();
333
334        assert_eq!(items.len(), 3);
335        assert_eq!(items[0], (id1, "first"));
336        assert_eq!(items[1], (id2, "second"));
337        assert_eq!(items[2], (id3, "third"));
338    }
339
340    #[test]
341    fn test_string_interner_stats() {
342        let mut interner = StringInterner::new();
343
344        interner.intern("short");
345        interner.intern("medium_length");
346        interner.intern("this_is_a_longer_string");
347
348        let stats = interner.stats();
349
350        assert_eq!(stats.unique_count, 3);
351        assert_eq!(stats.total_bytes, 5 + 13 + 23);
352        assert!((stats.average_length - 13.67).abs() < 0.01);
353    }
354
355    #[test]
356    fn test_invalid_string_id() {
357        let interner = StringInterner::new();
358
359        let invalid = StringId::INVALID;
360        assert_eq!(invalid.0, u32::MAX);
361        assert!(!StringId::INVALID.is_valid());
362        assert_eq!(interner.get(StringId::INVALID), None);
363    }
364
365    #[test]
366    #[should_panic(expected = "Invalid string ID")]
367    fn test_resolve_invalid_id() {
368        let interner = StringInterner::new();
369        interner.resolve(StringId::INVALID);
370    }
371
372    #[test]
373    fn test_string_id_ordering() {
374        let mut interner = StringInterner::new();
375
376        let id1 = interner.intern("a");
377        let id2 = interner.intern("b");
378        let id3 = interner.intern("c");
379
380        assert!(id1 < id2);
381        assert!(id2 < id3);
382        assert!(id1 < id3);
383    }
384
385    #[test]
386    fn test_empty_string() {
387        let mut interner = StringInterner::new();
388
389        let id = interner.intern("");
390        assert_eq!(interner.get(id), Some(""));
391
392        // Empty string should also be deduplicated
393        let id2 = interner.intern("");
394        assert_eq!(id, id2);
395    }
396
397    #[test]
398    fn test_unicode_strings() {
399        let mut interner = StringInterner::new();
400
401        let id1 = interner.intern("Hello δΈ–η•Œ");
402        let id2 = interner.intern("πŸ¦€ Rust");
403        let id3 = interner.intern("Hello δΈ–η•Œ");
404
405        assert_eq!(id1, id3);
406        assert_ne!(id1, id2);
407
408        assert_eq!(interner.get(id1), Some("Hello δΈ–η•Œ"));
409        assert_eq!(interner.get(id2), Some("πŸ¦€ Rust"));
410    }
411}