use std::hash::{Hash, Hasher};
use hashbrown::HashMap;
use rustc_hash::FxHasher;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
type U64NoHashMap<V> = HashMap<u64, V, rustc_hash::FxBuildHasher>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
pub struct StringId(u32);
impl StringId {
#[inline]
pub const fn new(id: u32) -> Self {
Self(id)
}
#[inline]
pub const fn as_u32(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, Default)]
pub struct StringInterner {
arena: Vec<u8>,
lookup: U64NoHashMap<SmallVec<[StringId; 1]>>,
offsets: Vec<(u32, u16)>,
}
impl StringInterner {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_capacity(string_bytes: usize, num_strings: usize) -> Self {
Self {
arena: Vec::with_capacity(string_bytes),
lookup: U64NoHashMap::with_capacity_and_hasher(num_strings, rustc_hash::FxBuildHasher),
offsets: Vec::with_capacity(num_strings),
}
}
pub fn intern_bytes(&mut self, s: &[u8]) -> StringId {
let hash = Self::hash_bytes(s);
if let Some(ids) = self.lookup.get(&hash) {
for &id in ids {
if self.get_bytes(id) == Some(s) {
return id;
}
}
}
let start = self.arena.len() as u32;
let len = s.len() as u16;
self.arena.extend_from_slice(s);
let id = StringId::new(self.offsets.len() as u32);
self.offsets.push((start, len));
self.lookup.entry(hash).or_default().push(id);
id
}
#[inline]
pub fn intern(&mut self, s: &str) -> StringId {
self.intern_bytes(s.as_bytes())
}
pub fn get_bytes_id(&self, s: &[u8]) -> Option<StringId> {
let hash = Self::hash_bytes(s);
if let Some(ids) = self.lookup.get(&hash) {
for &id in ids {
if self.get_bytes(id) == Some(s) {
return Some(id);
}
}
}
None
}
#[inline]
pub fn get_id(&self, s: &str) -> Option<StringId> {
self.get_bytes_id(s.as_bytes())
}
pub fn get_bytes(&self, id: StringId) -> Option<&[u8]> {
let (start, len) = *self.offsets.get(id.0 as usize)?;
self.arena.get(start as usize..(start as usize + len as usize))
}
pub fn get(&self, id: StringId) -> Option<&str> {
self.get_bytes(id).and_then(|b| std::str::from_utf8(b).ok())
}
#[inline]
pub fn len(&self) -> usize {
self.offsets.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.offsets.is_empty()
}
#[inline]
fn hash_bytes(s: &[u8]) -> u64 {
let mut hasher = FxHasher::default();
s.hash(&mut hasher);
hasher.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_interning() {
let mut interner = StringInterner::new();
let id1 = interner.intern("src");
let id2 = interner.intern("lib");
let id3 = interner.intern("src");
assert_eq!(id1, id3);
assert_ne!(id1, id2);
assert_eq!(interner.get(id1), Some("src"));
assert_eq!(interner.len(), 2);
}
#[test]
fn test_bytes_interning() {
let mut interner = StringInterner::new();
let id1 = interner.intern_bytes(b"hello");
assert_eq!(interner.get(id1), Some("hello"));
}
#[test]
fn test_get_id() {
let mut interner = StringInterner::new();
let id_src = interner.intern("src");
assert_eq!(interner.get_id("src"), Some(id_src));
assert_eq!(interner.get_id("nonexistent"), None);
}
}