//! String interning for scope variable names. //! //! Thread-local string pool maps `&str` → `StringId(u32)`. Scope HashMaps use //! `StringId` as keys instead of `String`, eliminating per-call string allocation, //! hashing, and comparison overhead. use std::cell::RefCell; use std::collections::HashMap; /// Interned string identifier. Copy, Eq, Hash (trivial u32 hash). #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct StringId(u32); impl std::hash::Hash for StringId { #[inline] fn hash(&self, state: &mut H) { // Use the u32 directly — already unique, no need for full hash. state.write_u32(self.0); } } impl std::fmt::Display for StringId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { resolve(*self, |s| write!(f, "{}", s)) } } /// Thread-local string pool. struct StringPool { /// Interned string storage. Index = StringId.0. strings: Vec>, /// Reverse lookup: string content → StringId. lookup: HashMap, StringId>, } impl StringPool { fn new() -> Self { StringPool { strings: Vec::new(), lookup: HashMap::new(), } } fn intern(&mut self, s: &str) -> StringId { if let Some(&id) = self.lookup.get(s) { return id; } let id = StringId(self.strings.len() as u32); let boxed: Box = s.into(); self.lookup.insert(boxed.clone(), id); self.strings.push(boxed); id } fn resolve(&self, id: StringId) -> &str { &self.strings[id.0 as usize] } } thread_local! { static POOL: RefCell = RefCell::new(StringPool::new()); } /// Intern a string, returning its unique `StringId`. /// O(1) amortized (HashMap lookup in pool on hit, insert on miss). #[inline] pub fn intern(s: &str) -> StringId { POOL.with(|pool| pool.borrow_mut().intern(s)) } /// Resolve a `StringId` back to a string, calling `f` with the result. /// Uses a callback to avoid lifetime issues with the thread-local borrow. #[inline] pub fn resolve(id: StringId, f: impl FnOnce(&str) -> R) -> R { POOL.with(|pool| { let pool = pool.borrow(); f(pool.resolve(id)) }) } /// Resolve a `StringId` to an owned String. pub fn resolve_to_string(id: StringId) -> std::string::String { resolve(id, |s| s.to_string()) } #[cfg(test)] mod tests { use super::*; #[test] fn intern_same_string_returns_same_id() { let a = intern("hello"); let b = intern("hello"); assert_eq!(a, b); } #[test] fn intern_different_strings_returns_different_ids() { let a = intern("foo"); let b = intern("bar"); assert_ne!(a, b); } #[test] fn resolve_returns_original_string() { let id = intern("test_string"); resolve(id, |s| assert_eq!(s, "test_string")); } #[test] fn resolve_to_string_works() { let id = intern("owned"); assert_eq!(resolve_to_string(id), "owned"); } #[test] fn hash_equality() { use std::collections::HashSet; let a = intern("x"); let b = intern("x"); let mut set = HashSet::new(); set.insert(a); assert!(set.contains(&b)); } #[test] fn many_strings() { let ids: Vec = (0..100) .map(|i| intern(&format!("var_{}", i))) .collect(); // All unique let unique: std::collections::HashSet = ids.iter().copied().collect(); assert_eq!(unique.len(), 100); // Re-interning returns same ids for i in 0..100 { assert_eq!(intern(&format!("var_{}", i)), ids[i]); } } }