#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![no_std]
pub mod float;
pub mod from_ref;
pub mod arena_string;
pub use arena_string::ArenaString;
pub use float::{HashableF32, HashableF64};
pub use from_ref::FromRef;
extern crate alloc;
use alloc::{
borrow::{Cow, ToOwned},
string::String,
vec::Vec,
};
use core::{
borrow::Borrow,
fmt,
hash::{BuildHasher, Hash},
marker::PhantomData,
};
use indexmap::IndexSet;
#[derive(Debug, thiserror::Error)]
pub enum InternerError {
#[error("Interner handle space exhausted")]
Overflow,
}
pub struct Interner<T, S, H = u32>
where
T: Eq + Hash,
S: BuildHasher,
H: Copy + TryFrom<usize>, usize: TryFrom<H>, {
items: IndexSet<T, S>,
_handle: PhantomData<H>,
}
impl<T, S, H> Default for Interner<T, S, H>
where
T: Eq + Hash,
S: BuildHasher + Default,
H: Copy + TryFrom<usize>,
usize: TryFrom<H>,
{
#[inline]
fn default() -> Self {
Self::new(S::default())
}
}
impl<T, S, H> fmt::Debug for Interner<T, S, H>
where
T: Eq + Hash,
S: BuildHasher,
H: Copy + TryFrom<usize>,
usize: TryFrom<H>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Interner")
.field("len", &self.len())
.field("capacity", &self.capacity())
.finish()
}
}
impl<T, S, H> Interner<T, S, H>
where
T: Eq + Hash,
S: BuildHasher,
H: Copy + TryFrom<usize>,
usize: TryFrom<H>,
{
#[must_use]
pub const fn new(hasher: S) -> Self {
Self {
items: IndexSet::with_hasher(hasher),
_handle: PhantomData,
}
}
#[must_use]
pub fn with_capacity(hasher: S, capacity: usize) -> Self {
Self {
items: IndexSet::with_capacity_and_hasher(capacity, hasher),
_handle: PhantomData,
}
}
pub fn intern_owned(&mut self, item: T) -> Result<H, InternerError> {
if let Some(idx) = self.items.get_index_of(&item) {
return Self::idx_to_handle(idx);
}
let handle = Self::idx_to_handle(self.items.len())?;
self.items.insert(item);
Ok(handle)
}
pub fn intern_ref<Q>(&mut self, item: &Q) -> Result<H, InternerError>
where
T: Borrow<Q> + FromRef<Q>,
Q: Hash + Eq + ?Sized,
{
if let Some(idx) = self.items.get_index_of(item) {
return Self::idx_to_handle(idx);
}
let h = Self::idx_to_handle(self.items.len())?;
self.items.insert(T::from_ref(item));
Ok(h)
}
pub fn intern_cow<Q>(&mut self, item: Cow<'_, Q>) -> Result<H, InternerError>
where
T: Borrow<Q> + Clone,
Q: ToOwned<Owned = T> + Hash + Eq + ?Sized,
{
if let Some(idx) = self.items.get_index_of(item.as_ref()) {
return Self::idx_to_handle(idx);
}
let h = Self::idx_to_handle(self.items.len())?;
self.items.insert(item.into_owned());
Ok(h)
}
pub fn intern_ref_or_insert_with<Q, F>(&mut self, key: &Q, make: F) -> Result<H, InternerError>
where
T: Borrow<Q> + Clone,
Q: Hash + Eq + ?Sized,
F: FnOnce() -> T,
{
if let Some(idx) = self.items.get_index_of(key) {
return Self::idx_to_handle(idx);
}
let h = Self::idx_to_handle(self.items.len())?;
self.items.insert(make());
Ok(h)
}
#[inline]
pub fn lookup_handle<Q>(&self, item: &Q) -> Result<Option<H>, InternerError>
where
T: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.items
.get_index_of(item)
.map_or(Ok(None), |idx| Ok(Some(Self::idx_to_handle(idx)?)))
}
#[inline]
pub fn contains<Q>(&self, item: &Q) -> bool
where
T: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.items.contains(item)
}
pub fn remove<Q>(&mut self, item: &Q) -> Option<(H, T)>
where
T: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let (idx, val) = self.items.shift_remove_full(item)?;
let handle = H::try_from(idx).ok()?;
Some((handle, val))
}
pub fn remove_handle(&mut self, handle: H) -> Option<T> {
let idx = usize::try_from(handle).ok()?;
self.items.shift_remove_index(idx)
}
pub fn repair_handles<'a, I>(&self, removed: H, handles: I)
where
I: IntoIterator<Item = &'a mut H>,
H: 'a + PartialOrd,
{
for h in handles {
if *h > removed {
if let Ok(idx) = usize::try_from(*h)
&& let Ok(shifted) = H::try_from(idx - 1)
{
*h = shifted;
}
}
}
}
#[inline]
pub fn capacity(&self) -> usize {
self.items.capacity()
}
#[inline]
pub fn reserve(&mut self, additional: usize) {
self.items.reserve(additional);
}
#[inline]
pub fn shrink_to_fit(&mut self) {
self.items.shrink_to_fit();
}
#[inline]
pub fn clear(&mut self) {
self.items.clear();
}
#[inline]
fn idx_to_handle(idx: usize) -> Result<H, InternerError> {
H::try_from(idx).map_err(|_| InternerError::Overflow)
}
#[must_use]
#[inline]
pub fn resolve(&self, handle: H) -> Option<&T> {
let idx: usize = usize::try_from(handle).ok()?;
self.items.get_index(idx)
}
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.items.len()
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
#[inline]
pub fn iter(&self) -> indexmap::set::Iter<'_, T> {
self.items.iter()
}
#[doc(alias = "into_vec")]
#[must_use]
pub fn export(self) -> Vec<T> {
self.items.into_iter().collect()
}
}
impl<'a, T, S, H> IntoIterator for &'a Interner<T, S, H>
where
T: Eq + Hash,
S: BuildHasher,
H: Copy + TryFrom<usize>,
usize: TryFrom<H>,
{
type Item = &'a T;
type IntoIter = indexmap::set::Iter<'a, T>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.items.iter()
}
}
impl<T, S, H> IntoIterator for Interner<T, S, H>
where
T: Eq + Hash,
S: BuildHasher,
H: Copy + TryFrom<usize>,
usize: TryFrom<H>,
{
type Item = T;
type IntoIter = indexmap::set::IntoIter<T>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.items.into_iter()
}
}
impl<T, S, H> Interner<T, S, H>
where
T: Eq + Hash + AsRef<str>,
S: BuildHasher,
H: Copy + TryFrom<usize>,
usize: TryFrom<H>,
{
pub fn export_arena(self) -> Result<(String, Vec<H>), InternerError> {
let total_bytes: usize = self.items.iter().map(|s| s.as_ref().len()).sum();
let count = self.items.len();
let mut arena = String::with_capacity(total_bytes);
let mut offsets = Vec::with_capacity(count + 1);
offsets.push(H::try_from(0usize).map_err(|_| InternerError::Overflow)?);
for item in self.items {
arena.push_str(item.as_ref());
offsets.push(H::try_from(arena.len()).map_err(|_| InternerError::Overflow)?);
}
Ok((arena, offsets))
}
}
#[cfg(test)]
mod tests {
use alloc::{
borrow::Cow,
boxed::Box,
rc::Rc,
string::{String, ToString as _},
sync::Arc,
vec::Vec,
};
use core::hash::BuildHasherDefault;
use ahash::RandomState;
use rustc_hash::FxHasher;
use super::{Interner, InternerError};
fn create_string_interner() -> Interner<String, RandomState> {
Interner::new(RandomState::new())
}
#[test]
fn test_new_and_empty() {
let interner = create_string_interner();
assert!(interner.is_empty());
assert_eq!(interner.len(), 0);
}
#[test]
fn test_intern_owned_and_resolve() {
let mut interner = create_string_interner();
let item = "hello".to_string();
let handle = interner.intern_owned(item.clone()).unwrap();
assert!(!interner.is_empty());
assert_eq!(interner.len(), 1);
assert_eq!(interner.resolve(handle), Some(&item));
}
#[test]
fn test_intern_owned_duplicate_returns_same_handle() {
let mut interner = create_string_interner();
let item1 = "hello".to_string();
let item2 = "hello".to_string();
let handle1 = interner.intern_owned(item1).unwrap();
let handle2 = interner.intern_owned(item2).unwrap();
assert_eq!(handle1, handle2);
assert_eq!(interner.len(), 1);
}
#[test]
fn test_intern_ref_and_resolve() {
let mut interner = create_string_interner();
let item = "world".to_string();
let handle = interner.intern_ref(&item).unwrap();
assert_eq!(interner.len(), 1);
assert_eq!(interner.resolve(handle), Some(&item));
}
#[test]
fn test_intern_ref_and_resolve_box_str() {
let mut interner = Interner::<Box<str>, RandomState>::new(RandomState::new());
let item = "world";
let handle = interner.intern_ref(item).unwrap();
assert_eq!(interner.len(), 1);
assert_eq!(interner.resolve(handle).map(|s| &**s), Some(item));
}
#[test]
fn test_intern_ref_and_resolve_rc_str() {
let mut interner = Interner::<Rc<str>, RandomState>::new(RandomState::new());
let item = "world";
let handle = interner.intern_ref(item).unwrap();
assert_eq!(interner.len(), 1);
assert_eq!(interner.resolve(handle).map(|s| &**s), Some(item));
}
#[test]
fn test_intern_ref_and_resolve_arc_str() {
let mut interner = Interner::<Arc<str>, RandomState>::new(RandomState::new());
let item = "world";
let handle = interner.intern_ref(item).unwrap();
assert_eq!(interner.len(), 1);
assert_eq!(interner.resolve(handle).map(|s| &**s), Some(item));
}
#[test]
fn test_intern_ref_and_resolve_vec_u8() {
let mut interner = Interner::<Vec<u8>, RandomState>::new(RandomState::new());
let item = "world";
let handle = interner.intern_ref(item.as_bytes()).unwrap();
assert_eq!(interner.len(), 1);
assert_eq!(
interner.resolve(handle).map(alloc::vec::Vec::as_slice),
Some(item.as_bytes()),
);
}
#[test]
fn test_intern_ref_duplicate_returns_same_handle() {
let mut interner = create_string_interner();
let item = "world".to_string();
let handle_owned = interner.intern_owned(item.clone()).unwrap();
assert_eq!(interner.len(), 1);
let handle_ref = interner.intern_ref(&item).unwrap();
assert_eq!(handle_owned, handle_ref);
assert_eq!(interner.len(), 1);
}
#[test]
fn test_intern_cow_variants() {
let mut interner = create_string_interner();
let item = "cow".to_string();
let handle1 = interner
.intern_cow(Cow::<String>::Owned(item.clone()))
.unwrap();
assert_eq!(interner.len(), 1);
assert_eq!(interner.resolve(handle1), Some(&item));
let handle2 = interner.intern_cow(Cow::Borrowed(&item)).unwrap();
assert_eq!(handle1, handle2);
assert_eq!(interner.len(), 1);
let new_item = "new_cow".to_string();
let handle3 = interner.intern_cow(Cow::Borrowed(&new_item)).unwrap();
assert_ne!(handle1, handle3);
assert_eq!(interner.len(), 2);
assert_eq!(interner.resolve(handle3), Some(&new_item));
}
#[test]
fn test_mixed_interning_provides_consistent_handles() {
let mut interner = create_string_interner();
let val = "test".to_string();
let h_owned = interner.intern_owned(val.clone()).unwrap();
let h_ref = interner.intern_ref(&val).unwrap();
let h_cow = interner.intern_cow(Cow::Borrowed(&val)).unwrap();
assert_eq!(h_owned, h_ref);
assert_eq!(h_ref, h_cow);
assert_eq!(interner.len(), 1);
}
#[test]
fn test_resolve_invalid_handle_returns_none() {
let interner = create_string_interner();
let invalid_handle: u32 = 999;
assert_eq!(interner.resolve(invalid_handle), None);
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
struct TestStruct {
id: u32,
name: String,
}
#[test]
fn test_with_custom_struct_type() {
let mut interner: Interner<TestStruct, RandomState> = Interner::new(RandomState::new());
let item1 = TestStruct {
id: 1,
name: "one".into(),
};
let item2 = TestStruct {
id: 1,
name: "one".into(),
};
let item3 = TestStruct {
id: 2,
name: "two".into(),
};
let h1 = interner.intern_ref(&item1).unwrap();
let h2 = interner.intern_ref(&item2).unwrap();
let h3 = interner.intern_ref(&item3).unwrap();
assert_eq!(h1, h2);
assert_ne!(h1, h3);
assert_eq!(interner.len(), 2);
assert_eq!(interner.resolve(h1), Some(&item1));
}
#[test]
fn test_custom_handle_type_u16() {
let mut interner: Interner<i32, RandomState, u16> = Interner::new(RandomState::new());
let h1 = interner.intern_owned(100).unwrap();
let h2 = interner.intern_owned(200).unwrap();
let h3 = interner.intern_owned(100).unwrap();
assert_eq!(h1, 0u16);
assert_eq!(h2, 1u16);
assert_eq!(h1, h3);
assert_eq!(interner.len(), 2);
}
#[test]
fn test_handle_overflow_error() {
let mut interner: Interner<u16, RandomState, u8> = Interner::new(RandomState::new());
for i in 0..=255 {
let handle_res = interner.intern_owned(i as u16);
assert!(handle_res.is_ok());
assert_eq!(handle_res.unwrap(), i as u8);
}
assert_eq!(interner.len(), 256);
let overflow_res = interner.intern_owned(256);
assert!(matches!(overflow_res, Err(InternerError::Overflow)));
assert_eq!(interner.len(), 256);
}
#[test]
fn test_custom_hasher_fxhash() {
type FxBuildHasher = BuildHasherDefault<FxHasher>;
let mut interner: Interner<i64, FxBuildHasher> = Interner::new(FxBuildHasher::default());
let h1 = interner.intern_owned(12345).unwrap();
let h2 = interner.intern_owned(12345).unwrap();
assert_eq!(h1, h2);
assert_eq!(interner.len(), 1);
}
#[test]
fn test_export_preserves_insertion_order() {
let mut interner = create_string_interner();
let h1 = interner.intern_owned("first".to_string()).unwrap();
let h2 = interner.intern_owned("second".to_string()).unwrap();
let _ = interner.intern_owned("first".to_string()).unwrap();
let exported_data = interner.export();
let expected = alloc::vec!["first".to_string(), "second".to_string()];
assert_eq!(exported_data, expected);
let idx1: usize = h1.try_into().ok().unwrap();
let idx2: usize = h2.try_into().ok().unwrap();
assert_eq!(exported_data[idx1], "first");
assert_eq!(exported_data[idx2], "second");
}
#[test]
fn test_into_iterator_ref() {
let mut interner = create_string_interner();
interner.intern_ref("a").unwrap();
interner.intern_ref("b").unwrap();
let mut collected = Vec::new();
for s in &interner {
collected.push(s.as_str());
}
assert_eq!(collected, alloc::vec!["a", "b"]);
}
#[test]
fn test_get_does_not_insert() {
let mut interner = create_string_interner();
assert!(interner.lookup_handle("x").is_ok_and(|h| h.is_none()));
assert!(interner.is_empty());
let h = interner.intern_ref("x").unwrap();
assert_eq!(interner.lookup_handle("x").unwrap(), Some(h));
assert_eq!(interner.len(), 1);
}
#[test]
fn test_contains() {
let mut interner = create_string_interner();
interner.intern_ref("abc").unwrap();
assert!(interner.contains("abc"));
assert!(!interner.contains("def"));
}
#[test]
fn test_interner_utilities() {
let mut interner = Interner::<String, RandomState>::with_capacity(RandomState::new(), 10);
assert!(interner.capacity() >= 10);
interner.intern_ref("a").unwrap();
interner.intern_ref("b").unwrap();
interner.reserve(100);
assert!(interner.capacity() >= 102);
interner.shrink_to_fit();
assert!(interner.capacity() >= 2);
let debug_str = alloc::format!("{interner:?}");
assert!(debug_str.contains("Interner"));
assert!(debug_str.contains("len: 2"));
interner.clear();
assert!(interner.is_empty());
assert_eq!(interner.len(), 0);
}
#[test]
fn test_export_arena() {
let mut interner = create_string_interner();
let h1 = interner.intern_ref("hello").unwrap();
let h2 = interner.intern_ref("world").unwrap();
let (arena, offsets) = interner.export_arena().unwrap();
assert_eq!(arena, "helloworld");
assert_eq!(offsets, alloc::vec![0, 5, 10]);
let idx1: usize = h1.try_into().unwrap();
let s1 = &arena[offsets[idx1] as usize..offsets[idx1 + 1] as usize];
assert_eq!(s1, "hello");
let idx2: usize = h2.try_into().unwrap();
let s2 = &arena[offsets[idx2] as usize..offsets[idx2 + 1] as usize];
assert_eq!(s2, "world");
}
#[test]
fn test_intern_ref_or_insert_with() {
let mut interner = create_string_interner();
let h1 = interner
.intern_ref_or_insert_with("key", || "key_computed".to_string())
.unwrap();
assert_eq!(interner.resolve(h1), Some(&"key_computed".to_string()));
let mut called = false;
let h2 = interner
.intern_ref_or_insert_with("key_computed", || {
called = true;
"should_not_exist".to_string()
})
.unwrap();
assert_eq!(h1, h2);
assert!(!called, "Closure should not be called if item exists");
}
#[test]
fn test_error_display() {
let err = InternerError::Overflow;
assert_eq!(alloc::format!("{err}"), "Interner handle space exhausted");
}
#[test]
fn test_into_iterator_owned() {
let mut interner = create_string_interner();
interner.intern_ref("a").unwrap();
interner.intern_ref("b").unwrap();
let vec: Vec<String> = interner.into_iter().collect();
assert_eq!(vec, alloc::vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn test_export_arena_empty() {
let interner = create_string_interner();
let (arena, offsets) = interner.export_arena().unwrap();
assert_eq!(arena, "");
assert_eq!(offsets, alloc::vec![0]); }
#[test]
fn test_lookup_handle_non_existent() {
let interner = create_string_interner();
let res = interner.lookup_handle("ghost");
assert!(res.is_ok());
assert!(res.unwrap().is_none());
}
#[test]
fn test_default_impl() {
let interner: Interner<String, RandomState> = Interner::default();
assert!(interner.is_empty());
}
#[test]
fn test_explicit_iter() {
let mut interner = create_string_interner();
interner.intern_ref("A").unwrap();
let mut iter = interner.iter();
assert_eq!(iter.next(), Some(&"A".to_string()));
assert_eq!(iter.next(), None);
}
#[test]
fn test_error_debug_impl() {
let err = InternerError::Overflow;
let debug_output = alloc::format!("{err:?}");
assert_eq!(debug_output, "Overflow");
}
#[test]
fn test_lookup_handle_success() {
let mut interner = create_string_interner();
let h = interner.intern_ref("A").unwrap();
let found = interner.lookup_handle("A").unwrap();
assert_eq!(found, Some(h));
}
#[test]
fn test_remove_handle_shifts_indices() {
let mut interner = create_string_interner();
let h_a = interner.intern_ref("A").unwrap(); let h_b = interner.intern_ref("B").unwrap(); let h_c = interner.intern_ref("C").unwrap();
assert_eq!(interner.len(), 3);
let removed = interner.remove_handle(h_b);
assert_eq!(removed, Some("B".to_string()));
assert_eq!(interner.len(), 2);
assert_eq!(interner.resolve(h_a), Some(&"A".to_string()));
assert_eq!(interner.resolve(h_c), None);
assert_eq!(interner.resolve(h_b), Some(&"C".to_string()));
}
#[test]
fn test_remove_and_recover_handles() {
let mut interner = create_string_interner();
let mut handles = alloc::vec![
interner.intern_ref("A").unwrap(), interner.intern_ref("B").unwrap(), interner.intern_ref("C").unwrap(), interner.intern_ref("D").unwrap(), ];
let (removed_handle, val) = interner.remove("B").unwrap();
assert_eq!(val, "B");
assert_eq!(removed_handle, 1);
for h in &mut handles {
if *h > removed_handle {
*h -= 1;
}
}
assert_eq!(interner.resolve(handles[0]), Some(&"A".to_string()));
assert_eq!(interner.resolve(handles[1]), Some(&"C".to_string()));
assert_eq!(handles[2], 1);
assert_eq!(interner.resolve(handles[2]), Some(&"C".to_string()));
assert_eq!(handles[3], 2);
assert_eq!(interner.resolve(handles[3]), Some(&"D".to_string()));
}
#[test]
fn test_remove_and_recover_handles_helper() {
let mut interner = create_string_interner();
let mut handles = alloc::vec![
interner.intern_ref("A").unwrap(), interner.intern_ref("B").unwrap(), interner.intern_ref("C").unwrap(), interner.intern_ref("D").unwrap(), ];
let (removed_handle, val) = interner.remove("B").unwrap();
assert_eq!(val, "B");
interner.repair_handles(removed_handle, &mut handles);
assert_eq!(interner.resolve(handles[0]), Some(&"A".to_string()));
assert_eq!(interner.resolve(handles[1]), Some(&"C".to_string())); assert_eq!(interner.resolve(handles[2]), Some(&"C".to_string())); assert_eq!(interner.resolve(handles[3]), Some(&"D".to_string())); }
#[test]
fn test_repair_handles_in_structs() {
struct User {
name_handle: u32,
_score: i32,
}
let mut interner = create_string_interner();
let h_a = interner.intern_ref("A").unwrap(); let h_b = interner.intern_ref("B").unwrap(); let h_c = interner.intern_ref("C").unwrap();
let mut users = alloc::vec![
User {
name_handle: h_a,
_score: 10,
},
User {
name_handle: h_b,
_score: 20,
},
User {
name_handle: h_c,
_score: 30,
},
];
let (removed, _) = interner.remove("A").unwrap();
interner.repair_handles(removed, users.iter_mut().map(|u| &mut u.name_handle));
assert_eq!(users[1].name_handle, 0);
assert_eq!(
interner.resolve(users[1].name_handle),
Some(&"B".to_string())
);
assert_eq!(users[2].name_handle, 1);
assert_eq!(
interner.resolve(users[2].name_handle),
Some(&"C".to_string())
);
}
}