Skip to main content

lechange_core/
interner.rs

1//! String interner for zero-copy path deduplication
2//!
3//! Uses a `Box<str>` arena for single-allocation storage with a hash-keyed map
4//! for O(1) deduplication. No `Arc` refcount overhead.
5
6use crate::types::InternedString;
7use parking_lot::RwLock;
8use std::collections::HashMap;
9use std::hash::{Hash, Hasher};
10
11/// Compute a 64-bit hash of a string using the default hasher.
12#[inline]
13fn hash_str(s: &str) -> u64 {
14    let mut hasher = std::collections::hash_map::DefaultHasher::new();
15    s.hash(&mut hasher);
16    hasher.finish()
17}
18
19/// Thread-safe string interner using `Box<str>` arena for single-allocation storage.
20///
21/// Strings are stored in an append-only `Vec<Box<str>>` (pointer-stable on heap).
22/// A hash-keyed map provides O(1) lookup with collision chaining.
23///
24/// `Send` and `Sync` are derived from `RwLock` — no manual unsafe impl needed.
25pub struct StringInterner {
26    /// Owned storage — append-only, pointer-stable (Box is heap-allocated)
27    strings: RwLock<Vec<Box<str>>>,
28    /// hash(str) → list of InternedString ids (collision chain)
29    map: RwLock<HashMap<u64, Vec<InternedString>>>,
30}
31
32impl StringInterner {
33    /// Create a new string interner
34    pub fn new() -> Self {
35        Self {
36            strings: RwLock::new(Vec::new()),
37            map: RwLock::new(HashMap::new()),
38        }
39    }
40
41    /// Create a new string interner with the given capacity
42    pub fn with_capacity(capacity: usize) -> Self {
43        Self {
44            strings: RwLock::new(Vec::with_capacity(capacity)),
45            map: RwLock::new(HashMap::with_capacity(capacity)),
46        }
47    }
48
49    /// Intern a string, returning its handle.
50    ///
51    /// Fast path: read lock to check existence via hash.
52    /// Slow path: write lock, double-check, allocate `Box<str>`.
53    pub fn intern(&self, s: &str) -> InternedString {
54        let h = hash_str(s);
55
56        // Fast path: check if already interned (read lock only)
57        {
58            let map = self.map.read();
59            if let Some(chain) = map.get(&h) {
60                let strings = self.strings.read();
61                for &id in chain {
62                    if &*strings[id.0 as usize] == s {
63                        return id;
64                    }
65                }
66            }
67        }
68
69        // Slow path: insert new string (write lock)
70        let mut map = self.map.write();
71        let mut strings = self.strings.write();
72
73        // Double-check after acquiring write lock
74        if let Some(chain) = map.get(&h) {
75            for &id in chain {
76                if &*strings[id.0 as usize] == s {
77                    return id;
78                }
79            }
80        }
81
82        let id = InternedString(strings.len() as u32);
83        strings.push(Box::from(s));
84        map.entry(h).or_default().push(id);
85
86        id
87    }
88
89    /// Resolve an interned string back to `&str`.
90    ///
91    /// # Safety argument
92    ///
93    /// We return a `&str` whose lifetime is tied to `&self` (the interner),
94    /// not to the read-lock guard. This is safe because:
95    /// 1. Strings are never removed (append-only vec).
96    /// 2. `Box<str>` is heap-allocated and pointer-stable — `push` to the vec
97    ///    does not move existing boxes.
98    /// 3. The interner outlives all returned references (borrow checker enforces this).
99    #[inline]
100    pub fn resolve(&self, id: InternedString) -> Option<&str> {
101        unsafe {
102            let strings = self.strings.read();
103            let s = strings.get(id.0 as usize)?;
104            // Transmute to extend lifetime from the lock guard to &self.
105            Some(std::mem::transmute::<&str, &str>(&**s))
106        }
107    }
108
109    /// Get current number of interned strings
110    #[inline]
111    pub fn len(&self) -> usize {
112        self.strings.read().len()
113    }
114
115    /// Check if the interner is empty
116    #[inline]
117    pub fn is_empty(&self) -> bool {
118        self.strings.read().is_empty()
119    }
120}
121
122impl Default for StringInterner {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_intern_resolve() {
134        let interner = StringInterner::new();
135        let id1 = interner.intern("foo");
136        let id2 = interner.intern("bar");
137        let id3 = interner.intern("foo"); // Should reuse id1
138
139        assert_eq!(id1, id3);
140        assert_ne!(id1, id2);
141        assert_eq!(interner.resolve(id1), Some("foo"));
142        assert_eq!(interner.resolve(id2), Some("bar"));
143    }
144
145    #[test]
146    fn test_thread_safety() {
147        use std::sync::Arc;
148        use std::thread;
149
150        let interner = Arc::new(StringInterner::new());
151        let mut handles = vec![];
152
153        // Spawn 10 threads, each interning the same 100 strings
154        for _ in 0..10 {
155            let interner = Arc::clone(&interner);
156            let handle = thread::spawn(move || {
157                for i in 0..100 {
158                    interner.intern(&format!("string_{}", i));
159                }
160            });
161            handles.push(handle);
162        }
163
164        for handle in handles {
165            handle.join().unwrap();
166        }
167
168        // Should have exactly 100 unique strings (deduplication worked)
169        assert_eq!(interner.len(), 100);
170    }
171
172    #[test]
173    fn test_with_capacity() {
174        let interner = StringInterner::with_capacity(1000);
175        assert_eq!(interner.len(), 0);
176
177        // Add 500 strings
178        for i in 0..500 {
179            interner.intern(&format!("str_{}", i));
180        }
181
182        assert_eq!(interner.len(), 500);
183    }
184
185    // --- New Phase 1.1 tests ---
186
187    #[test]
188    fn test_box_arena_intern_resolve() {
189        let interner = StringInterner::new();
190        let id = interner.intern("hello");
191        assert_eq!(interner.resolve(id), Some("hello"));
192    }
193
194    #[test]
195    fn test_box_arena_dedup() {
196        let interner = StringInterner::new();
197        let id1 = interner.intern("a");
198        let id2 = interner.intern("a");
199        assert_eq!(id1, id2);
200    }
201
202    #[test]
203    fn test_box_arena_distinct() {
204        let interner = StringInterner::new();
205        let id1 = interner.intern("a");
206        let id2 = interner.intern("b");
207        assert_ne!(id1, id2);
208    }
209
210    #[test]
211    fn test_no_arc_in_struct() {
212        let interner = StringInterner::new();
213        let type_name = std::any::type_name_of_val(&interner);
214        assert!(
215            !type_name.contains("Arc"),
216            "StringInterner should not contain Arc: {}",
217            type_name
218        );
219    }
220
221    #[test]
222    fn test_thread_safety_concurrent_intern() {
223        use std::sync::Arc;
224        use std::thread;
225
226        let interner = Arc::new(StringInterner::new());
227        let mut handles = vec![];
228
229        for t in 0..8 {
230            let interner = Arc::clone(&interner);
231            let handle = thread::spawn(move || {
232                for i in 0..1000 {
233                    let s = format!("t{}_{}", t, i);
234                    let id = interner.intern(&s);
235                    assert_eq!(interner.resolve(id), Some(s.as_str()));
236                }
237            });
238            handles.push(handle);
239        }
240
241        for handle in handles {
242            handle.join().unwrap();
243        }
244
245        // 8 threads * 1000 unique strings each
246        assert_eq!(interner.len(), 8000);
247    }
248
249    #[test]
250    fn test_capacity_growth() {
251        let interner = StringInterner::new();
252        for i in 0..10000 {
253            let s = format!("string_{}", i);
254            let id = interner.intern(&s);
255            assert_eq!(interner.resolve(id), Some(s.as_str()));
256        }
257        assert_eq!(interner.len(), 10000);
258    }
259}