use std::collections::HashMap;
use std::collections::hash_map::RandomState;
use std::hash::BuildHasher;
use concinnity_memory::InlineVec;
#[derive(Clone, Copy, Default)]
struct Span {
start: u32,
len: u32,
}
pub(crate) struct NameInterner<S = RandomState> {
blob: String,
spans: Vec<Span>,
buckets: HashMap<u64, InlineVec<u32>>,
hasher: S,
}
impl<S: Default> Default for NameInterner<S> {
fn default() -> Self {
Self {
blob: String::new(),
spans: Vec::new(),
buckets: HashMap::new(),
hasher: S::default(),
}
}
}
impl<S: BuildHasher> NameInterner<S> {
pub(crate) fn is_empty(&self) -> bool {
self.spans.is_empty()
}
pub(crate) fn name(&self, id: u32) -> &str {
self.spans
.get(id as usize)
.map_or("", |&span| self.slice(span))
}
pub(crate) fn names(&self) -> impl Iterator<Item = &str> {
self.spans.iter().map(|&span| self.slice(span))
}
pub(crate) fn lookup(&self, name: &str) -> Option<u32> {
self.buckets
.get(&self.hasher.hash_one(name))?
.iter()
.copied()
.find(|&id| self.name(id) == name)
}
pub(crate) fn intern(&mut self, name: &str) -> u32 {
if let Some(id) = self.lookup(name) {
return id;
}
let id = self.spans.len() as u32;
let span = self.append(name);
self.spans.push(span);
self.record(name, id);
id
}
pub(crate) fn prime(&mut self, pairs: &[(u32, String)]) -> bool {
if !self.is_empty() {
return false;
}
let len = pairs.iter().map(|(id, _)| id + 1).max().unwrap_or(0) as usize;
self.spans = vec![Span::default(); len];
for (id, name) in pairs {
let span = self.append(name);
self.spans[*id as usize] = span;
self.record(name, *id);
}
true
}
fn slice(&self, span: Span) -> &str {
&self.blob[span.start as usize..span.start as usize + span.len as usize]
}
fn append(&mut self, name: &str) -> Span {
let start = u32::try_from(self.blob.len()).expect("interned names exceed 4 GiB");
let len = u32::try_from(name.len()).expect("interned name exceeds 4 GiB");
self.blob.push_str(name);
Span { start, len }
}
fn record(&mut self, name: &str, id: u32) {
let hash = self.hasher.hash_one(name);
self.buckets.entry(hash).or_default().push(id);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::hash::Hasher;
fn interner() -> NameInterner {
NameInterner::default()
}
#[derive(Default)]
struct CollidingHasher;
impl Hasher for CollidingHasher {
fn write(&mut self, _bytes: &[u8]) {}
fn finish(&self) -> u64 {
0
}
}
#[derive(Default)]
struct AllCollide;
impl BuildHasher for AllCollide {
type Hasher = CollidingHasher;
fn build_hasher(&self) -> CollidingHasher {
CollidingHasher
}
}
#[test]
fn ids_are_dense_in_insertion_order_and_interning_is_idempotent() {
let mut names = interner();
assert_eq!(names.intern("floor"), 0);
assert_eq!(names.intern("wall"), 1);
assert_eq!(names.intern("floor"), 0, "a repeat returns the first id");
assert_eq!(names.intern("lamp"), 2);
assert_eq!(names.names().count(), 3);
}
#[test]
fn ids_round_trip_back_to_their_names() {
let mut names = interner();
names.intern("floor");
names.intern("wall");
assert_eq!(names.name(0), "floor");
assert_eq!(names.name(1), "wall");
assert_eq!(names.names().collect::<Vec<_>>(), ["floor", "wall"]);
assert_eq!(names.name(9), "", "an id past the table reads blank");
}
#[test]
fn each_distinct_name_is_stored_exactly_once() {
let mut names = interner();
for name in ["aa", "bbb", "aa", "bbb", "c"] {
names.intern(name);
}
assert_eq!(names.blob, "aabbbc");
assert_eq!(names.names().count(), 3);
}
#[test]
fn lookup_resolves_interned_names_and_never_inserts() {
let mut names = interner();
names.intern("floor");
assert_eq!(names.lookup("floor"), Some(0));
assert_eq!(names.lookup("missing"), None);
assert_eq!(names.names().count(), 1, "a miss must not grow the table");
assert_eq!(names.blob, "floor", "a miss must not grow the blob");
}
#[test]
fn a_substring_of_the_blob_is_not_an_interned_name() {
let mut names = interner();
names.intern("floorboard");
names.intern("board");
assert_eq!(names.lookup("floor"), None);
assert_eq!(names.lookup("oorbo"), None);
assert_eq!(names.lookup("board"), Some(1));
}
#[test]
fn a_sparse_prime_leaves_blank_slots_that_do_not_resolve() {
let mut names = interner();
assert!(names.prime(&[(0, "floor".to_string()), (2, "lamp".to_string())]));
assert_eq!(names.name(0), "floor");
assert_eq!(names.name(1), "", "the unrecorded slot stays blank");
assert_eq!(names.name(2), "lamp");
assert_eq!(names.lookup("lamp"), Some(2));
assert_eq!(names.lookup(""), None, "a blank slot is not lookupable");
assert_eq!(names.intern("new"), 3);
}
#[test]
fn priming_a_populated_table_is_refused_and_changes_nothing() {
let mut names = interner();
names.intern("floor");
assert!(!names.prime(&[(0, "other".to_string())]));
assert_eq!(names.name(0), "floor");
assert_eq!(names.lookup("other"), None);
}
#[test]
fn priming_nothing_leaves_an_empty_table() {
let mut names = interner();
assert!(names.prime(&[]));
assert!(names.is_empty());
}
#[test]
fn colliding_names_stay_distinct() {
let mut names: NameInterner<AllCollide> = NameInterner::default();
assert_eq!(names.intern("floor"), 0);
assert_eq!(names.intern("wall"), 1);
assert_eq!(names.intern("lamp"), 2);
assert_eq!(names.intern("wall"), 1, "a repeat still finds its own id");
assert_eq!(names.lookup("floor"), Some(0));
assert_eq!(names.lookup("wall"), Some(1));
assert_eq!(names.lookup("lamp"), Some(2));
assert_eq!(names.lookup("missing"), None);
assert_eq!(names.names().collect::<Vec<_>>(), ["floor", "wall", "lamp"]);
assert_eq!(names.buckets.len(), 1, "the test hasher collides them all");
}
#[test]
fn an_empty_name_is_interned_like_any_other_when_asked_for() {
let mut names = interner();
assert_eq!(names.intern(""), 0);
assert_eq!(names.lookup(""), Some(0));
assert_eq!(names.name(0), "");
}
}