use super::pool::InternPool;
use std::sync::{Arc, Mutex, OnceLock};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct InternedStr(u32);
impl InternedStr {
#[inline]
pub fn from_raw(idx: u32) -> Self {
Self(idx)
}
#[inline]
pub fn raw(self) -> u32 {
self.0
}
}
impl std::fmt::Display for InternedStr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match try_resolve(*self) {
Some(s) => f.write_str(s),
None => write!(f, "<InternedStr#{}>", self.0),
}
}
}
pub struct StringInterner {
pool: Arc<Mutex<InternPool>>,
}
impl StringInterner {
pub fn new() -> Self {
Self {
pool: Arc::new(Mutex::new(InternPool::new())),
}
}
pub fn shared_with(other: &StringInterner) -> Self {
Self {
pool: Arc::clone(&other.pool),
}
}
pub fn intern(&self, s: &str) -> InternedStr {
let mut guard = self
.pool
.lock()
.expect("StringInterner pool mutex was poisoned");
InternedStr::from_raw(guard.intern_str(s))
}
pub fn resolve(&self, handle: InternedStr) -> Option<&'static str> {
let mut guard = self
.pool
.lock()
.expect("StringInterner pool mutex was poisoned");
guard.resolve_str(handle.raw())
}
pub fn len(&self) -> usize {
let guard = self
.pool
.lock()
.expect("StringInterner pool mutex was poisoned");
guard.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn total_bytes(&self) -> usize {
let guard = self
.pool
.lock()
.expect("StringInterner pool mutex was poisoned");
guard.total_bytes()
}
pub fn arc_clone(&self) -> Arc<Mutex<InternPool>> {
Arc::clone(&self.pool)
}
}
impl Default for StringInterner {
fn default() -> Self {
Self::new()
}
}
static GLOBAL_POOL: OnceLock<Arc<Mutex<InternPool>>> = OnceLock::new();
fn global_pool() -> &'static Arc<Mutex<InternPool>> {
GLOBAL_POOL.get_or_init(|| Arc::new(Mutex::new(InternPool::new())))
}
pub fn intern(s: &str) -> InternedStr {
let mut guard = global_pool()
.lock()
.expect("global StringInterner mutex was poisoned");
InternedStr::from_raw(guard.intern_str(s))
}
pub fn resolve(handle: InternedStr) -> &'static str {
let mut guard = global_pool()
.lock()
.expect("global StringInterner mutex was poisoned");
guard
.resolve_str(handle.raw())
.expect("InternedStr handle is out of range in the global pool")
}
pub fn try_resolve(handle: InternedStr) -> Option<&'static str> {
let mut guard = global_pool().lock().ok()?;
guard.resolve_str(handle.raw())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interned_str_is_copy() {
let h = InternedStr::from_raw(0);
let h2 = h; assert_eq!(h, h2);
}
#[test]
fn test_interned_str_raw_round_trip() {
let h = InternedStr::from_raw(42);
assert_eq!(h.raw(), 42);
}
#[test]
fn test_interned_str_ordering() {
let h0 = InternedStr::from_raw(0);
let h1 = InternedStr::from_raw(1);
assert!(h0 < h1);
}
#[test]
fn test_local_interner_deduplicates() {
let si = StringInterner::new();
let h1 = si.intern("apple");
let h2 = si.intern("apple");
assert_eq!(h1, h2);
}
#[test]
fn test_local_interner_unique_for_distinct_strings() {
let si = StringInterner::new();
let h1 = si.intern("cat");
let h2 = si.intern("dog");
assert_ne!(h1, h2);
}
#[test]
fn test_local_interner_resolve() {
let si = StringInterner::new();
let h = si.intern("oxilean");
assert_eq!(si.resolve(h), Some("oxilean"));
}
#[test]
fn test_local_interner_len() {
let si = StringInterner::new();
si.intern("x");
si.intern("y");
si.intern("x"); assert_eq!(si.len(), 2);
}
#[test]
fn test_local_interner_total_bytes() {
let si = StringInterner::new();
si.intern("abc"); si.intern("de"); assert_eq!(si.total_bytes(), 5);
}
#[test]
fn test_local_interner_is_empty_initially() {
let si = StringInterner::new();
assert!(si.is_empty());
}
#[test]
fn test_local_interner_shared_with() {
let si1 = StringInterner::new();
let h1 = si1.intern("shared");
let si2 = StringInterner::shared_with(&si1);
let h2 = si2.intern("shared");
assert_eq!(h1, h2, "shared interners must return identical handles");
}
#[test]
fn test_local_interner_resolve_out_of_range() {
let si = StringInterner::new();
assert!(si.resolve(InternedStr::from_raw(999)).is_none());
}
#[test]
fn test_local_interner_empty_string() {
let si = StringInterner::new();
let h = si.intern("");
assert_eq!(si.resolve(h), Some(""));
}
#[test]
fn test_local_interner_unicode() {
let si = StringInterner::new();
let h = si.intern("αβγ");
assert_eq!(si.resolve(h), Some("αβγ"));
}
#[test]
fn test_local_interner_many_strings() {
let si = StringInterner::new();
let strings: Vec<String> = (0..100).map(|i| format!("str_{}", i)).collect();
let handles: Vec<InternedStr> = strings.iter().map(|s| si.intern(s)).collect();
let unique: std::collections::HashSet<InternedStr> = handles.iter().copied().collect();
assert_eq!(unique.len(), 100);
for (i, h) in handles.iter().enumerate() {
let resolved = si.resolve(*h).expect("should resolve");
assert_eq!(resolved, strings[i].as_str());
}
}
#[test]
fn test_global_intern_deduplicates() {
let h1 = intern("__global_test_string_A__");
let h2 = intern("__global_test_string_A__");
assert_eq!(h1, h2);
}
#[test]
fn test_global_resolve_returns_correct_content() {
let h = intern("__global_test_string_B__");
assert_eq!(resolve(h), "__global_test_string_B__");
}
#[test]
fn test_try_resolve_missing_handle_returns_none() {
let result = try_resolve(InternedStr::from_raw(u32::MAX));
assert!(result.is_none());
}
#[test]
fn test_global_intern_concurrent() {
use std::thread;
let handles: Vec<_> = (0..8)
.map(|_| thread::spawn(|| intern("concurrent_intern_test")))
.collect();
let results: Vec<InternedStr> = handles
.into_iter()
.map(|h| h.join().expect("thread should not panic"))
.collect();
let first = results[0];
assert!(results.iter().all(|&h| h == first));
}
#[test]
fn test_display_interned_str() {
let h = intern("display_test");
let s = format!("{}", h);
assert_eq!(s, "display_test");
}
}