use std::collections::HashMap;
use std::hash::Hash;
pub struct IdInterner<Id> {
forward: HashMap<Id, u32>,
reverse: Vec<Option<Id>>,
free_list: Vec<u32>,
}
impl<Id: Clone + Eq + Hash> IdInterner<Id> {
pub fn new() -> Self {
Self {
forward: HashMap::new(),
reverse: Vec::new(),
free_list: Vec::new(),
}
}
pub fn with_capacity(cap: usize) -> Self {
Self {
forward: HashMap::with_capacity(cap),
reverse: Vec::with_capacity(cap),
free_list: Vec::new(),
}
}
pub fn intern(&mut self, id: &Id) -> u32 {
if let Some(&existing) = self.forward.get(id) {
return existing;
}
let slot = if let Some(reused) = self.free_list.pop() {
self.reverse[reused as usize] = Some(id.clone());
reused
} else {
let new_slot = self.reverse.len() as u32;
self.reverse.push(Some(id.clone()));
new_slot
};
self.forward.insert(id.clone(), slot);
slot
}
pub fn lookup(&self, id: &Id) -> Option<u32> {
self.forward.get(id).copied()
}
pub fn id_of(&self, slot: u32) -> Option<&Id> {
self.reverse.get(slot as usize).and_then(|s| s.as_ref())
}
pub fn release(&mut self, id: &Id) {
if let Some(slot) = self.forward.remove(id) {
self.reverse[slot as usize] = None;
self.free_list.push(slot);
}
}
pub fn len(&self) -> usize {
self.forward.len()
}
pub fn is_empty(&self) -> bool {
self.forward.is_empty()
}
}
impl<Id: Clone + Eq + Hash> Default for IdInterner<Id> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn intern_returns_dense_slots() {
let mut interner = IdInterner::<String>::new();
assert_eq!(interner.intern(&"a".into()), 0);
assert_eq!(interner.intern(&"b".into()), 1);
assert_eq!(interner.intern(&"c".into()), 2);
}
#[test]
fn intern_is_idempotent() {
let mut interner = IdInterner::<String>::new();
let first = interner.intern(&"a".into());
let second = interner.intern(&"a".into());
assert_eq!(first, second);
assert_eq!(interner.len(), 1);
}
#[test]
fn release_then_intern_reuses_slot() {
let mut interner = IdInterner::<String>::new();
let a = interner.intern(&"a".into());
let b = interner.intern(&"b".into());
interner.release(&"a".into());
let c = interner.intern(&"c".into());
assert_eq!(c, a, "freed slot should be reused");
assert_eq!(b, 1);
}
#[test]
fn lookup_after_release_returns_none() {
let mut interner = IdInterner::<String>::new();
interner.intern(&"a".into());
interner.release(&"a".into());
assert_eq!(interner.lookup(&"a".into()), None);
}
#[test]
fn id_of_roundtrips() {
let mut interner = IdInterner::<String>::new();
let slot = interner.intern(&"hello".into());
assert_eq!(interner.id_of(slot), Some(&"hello".to_string()));
}
}