#![allow(unsafe_code)]
use bumpalo::Bump;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::ptr::NonNull;
pub struct StringInterner<'arena> {
arena: &'arena Bump,
map: HashMap<&'arena str, InternedString>,
}
impl<'arena> StringInterner<'arena> {
pub fn new(arena: &'arena Bump) -> Self {
Self {
arena,
map: HashMap::new(),
}
}
pub fn with_capacity(arena: &'arena Bump, capacity: usize) -> Self {
Self {
arena,
map: HashMap::with_capacity(capacity),
}
}
pub fn intern(&mut self, s: &str) -> InternedString {
if let Some(&interned) = self.map.get(s) {
return interned;
}
let arena_bytes = self.arena.alloc_slice_copy(s.as_bytes());
let interned = InternedString {
ptr: NonNull::new(arena_bytes.as_ptr() as *mut u8)
.expect("arena allocation is non-null"),
len: s.len(),
};
let arena_str = unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
interned.ptr.as_ptr(),
interned.len,
))
};
self.map.insert(arena_str, interned);
interned
}
pub fn len(&self) -> usize {
self.map.len()
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
pub fn stats(&self) -> InternerStats {
let unique_count = self.map.len();
let total_bytes = self.map.keys().map(|s| s.len()).sum();
InternerStats {
unique_strings: unique_count,
total_bytes_stored: total_bytes,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InternerStats {
pub unique_strings: usize,
pub total_bytes_stored: usize,
}
#[derive(Copy, Clone)]
pub struct InternedString {
ptr: NonNull<u8>,
len: usize,
}
impl InternedString {
pub fn as_str(&self) -> &str {
unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.ptr.as_ptr(), self.len))
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn as_ptr(&self) -> *const u8 {
self.ptr.as_ptr()
}
}
impl PartialEq for InternedString {
fn eq(&self, other: &Self) -> bool {
if self.ptr == other.ptr && self.len == other.len {
return true;
}
self.as_str() == other.as_str()
}
}
impl Eq for InternedString {}
impl Hash for InternedString {
fn hash<H: Hasher>(&self, state: &mut H) {
self.ptr.hash(state);
self.len.hash(state);
}
}
impl std::fmt::Debug for InternedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.as_str())
}
}
impl std::fmt::Display for InternedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl PartialEq<str> for InternedString {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<&str> for InternedString {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<String> for InternedString {
fn eq(&self, other: &String) -> bool {
self.as_str() == other
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_intern_basic() {
let arena = Bump::new();
let mut interner = StringInterner::new(&arena);
let s1 = interner.intern("hello");
assert_eq!(s1.as_str(), "hello");
assert_eq!(s1.len(), 5);
assert!(!s1.is_empty());
}
#[test]
fn test_intern_deduplication() {
let arena = Bump::new();
let mut interner = StringInterner::new(&arena);
let s1 = interner.intern("Person");
let s2 = interner.intern("Person");
let s3 = interner.intern("Team");
assert_eq!(s1.as_ptr(), s2.as_ptr());
assert_ne!(s1.as_ptr(), s3.as_ptr());
assert_eq!(s1, s2);
assert_ne!(s1, s3);
assert_eq!(interner.len(), 2);
}
#[test]
fn test_intern_empty_string() {
let arena = Bump::new();
let mut interner = StringInterner::new(&arena);
let s = interner.intern("");
assert_eq!(s.as_str(), "");
assert_eq!(s.len(), 0);
assert!(s.is_empty());
}
#[test]
fn test_intern_unicode() {
let arena = Bump::new();
let mut interner = StringInterner::new(&arena);
let s1 = interner.intern("hello 世界");
let s2 = interner.intern("emoji 🚀");
assert_eq!(s1.as_str(), "hello 世界");
assert_eq!(s2.as_str(), "emoji 🚀");
let s3 = interner.intern("hello 世界");
assert_eq!(s1.as_ptr(), s3.as_ptr());
}
#[test]
fn test_intern_many_strings() {
let arena = Bump::new();
let mut interner = StringInterner::new(&arena);
for i in 0..1000 {
let s = format!("string_{}", i);
interner.intern(&s);
}
assert_eq!(interner.len(), 1000);
for i in 0..1000 {
let s = format!("string_{}", i);
interner.intern(&s);
}
assert_eq!(interner.len(), 1000);
}
#[test]
fn test_intern_with_capacity() {
let arena = Bump::new();
let interner = StringInterner::with_capacity(&arena, 100);
assert_eq!(interner.len(), 0);
assert!(interner.is_empty());
}
#[test]
fn test_interned_string_equality() {
let arena = Bump::new();
let mut interner = StringInterner::new(&arena);
let s = interner.intern("test");
assert_eq!(s, "test");
assert_ne!(s, "other");
assert_eq!(s, String::from("test"));
assert_ne!(s, String::from("other"));
}
#[test]
fn test_interned_string_to_string() {
let arena = Bump::new();
let mut interner = StringInterner::new(&arena);
let s = interner.intern("hello");
let owned = s.to_string();
assert_eq!(owned, "hello");
assert_eq!(owned, s.as_str());
}
#[test]
fn test_interned_string_copy() {
let arena = Bump::new();
let mut interner = StringInterner::new(&arena);
let s1 = interner.intern("hello");
let s2 = s1;
assert_eq!(s1, s2);
assert_eq!(s1.as_ptr(), s2.as_ptr());
}
#[test]
fn test_interned_string_debug() {
let arena = Bump::new();
let mut interner = StringInterner::new(&arena);
let s = interner.intern("test");
let debug = format!("{:?}", s);
assert_eq!(debug, "\"test\"");
}
#[test]
fn test_interned_string_display() {
let arena = Bump::new();
let mut interner = StringInterner::new(&arena);
let s = interner.intern("test");
let display = format!("{}", s);
assert_eq!(display, "test");
}
#[test]
fn test_interner_stats() {
let arena = Bump::new();
let mut interner = StringInterner::new(&arena);
interner.intern("hello"); interner.intern("world"); interner.intern("hello");
let stats = interner.stats();
assert_eq!(stats.unique_strings, 2);
assert_eq!(stats.total_bytes_stored, 10);
}
#[test]
fn test_massive_deduplication() {
let arena = Bump::new();
let mut interner = StringInterner::new(&arena);
for _ in 0..10_000 {
interner.intern("Person");
}
let stats = interner.stats();
assert_eq!(stats.unique_strings, 1);
assert_eq!(stats.total_bytes_stored, 6);
}
#[test]
fn test_hash_consistency() {
use std::collections::HashSet;
let arena = Bump::new();
let mut interner = StringInterner::new(&arena);
let s1 = interner.intern("test");
let s2 = interner.intern("test");
let mut set = HashSet::new();
set.insert(s1);
assert!(set.contains(&s2)); }
}