use alloc::boxed::Box;
use alloc::sync::Arc;
use core::hash::{BuildHasher, Hasher};
use rustc_hash::FxBuildHasher;
use crate::reader::Reader;
use crate::shard::{Shard, ShardReadGuard};
use crate::sym::{NUM_SHARDS, SHARD_BITS, Sym};
use crate::threaded_reader::ThreadedReader;
const MIX: u64 = 0x9E37_79B9_7F4A_7C15;
const MIN_PREALLOCATED_STRINGS_PER_SHARD: usize = 8;
pub struct ThreadedLexicon<S = FxBuildHasher>(Arc<ThreadedLexiconInner<S>>);
impl<S> Clone for ThreadedLexicon<S> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl Default for ThreadedLexicon {
fn default() -> Self {
Self::new()
}
}
impl<S: BuildHasher> core::fmt::Debug for ThreadedLexicon<S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ThreadedLexicon")
.field("len", &self.0.len())
.finish_non_exhaustive()
}
}
impl ThreadedLexicon {
#[must_use]
pub fn new() -> Self {
Self(Arc::new(ThreadedLexiconInner::new()))
}
#[must_use]
pub fn with_capacity(strings: usize, bytes: usize) -> Self {
Self::with_capacity_and_hasher(strings, bytes, FxBuildHasher)
}
pub(crate) fn with_capacity_for_size_hint(strings: usize) -> Self {
if strings / NUM_SHARDS < MIN_PREALLOCATED_STRINGS_PER_SHARD {
Self::new()
} else {
Self::with_capacity(strings, 0)
}
}
}
impl<S: BuildHasher> ThreadedLexicon<S> {
pub fn with_hasher(hasher: S) -> Self {
Self(Arc::new(ThreadedLexiconInner::with_hasher(hasher)))
}
pub fn with_capacity_and_hasher(strings: usize, bytes: usize, hasher: S) -> Self {
Self(Arc::new(ThreadedLexiconInner::with_capacity_and_hasher(strings, bytes, hasher)))
}
#[inline]
pub fn intern(&self, s: impl AsRef<str>) -> Sym {
self.0.intern(s.as_ref())
}
#[inline]
pub fn intern_bytes(&self, bytes: &[u8]) -> Result<Sym, core::str::Utf8Error> {
self.0.intern_bytes(bytes)
}
#[inline]
#[must_use]
pub fn get(&self, s: impl AsRef<str>) -> Option<Sym> {
self.0.get(s.as_ref())
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn freeze(self) -> ThreadedReader {
self.into_reader()
}
fn into_reader(self) -> ThreadedReader {
match Arc::try_unwrap(self.0) {
Ok(inner) => inner.into_reader(),
Err(arc) => arc.build_reader(),
}
}
pub(crate) fn into_boxed_reader(self) -> Box<dyn Reader> {
Box::new(self.into_reader())
}
}
impl<S: BuildHasher, T: AsRef<str>> Extend<T> for ThreadedLexicon<S> {
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
for s in iter {
self.intern(s.as_ref());
}
}
}
impl<T: AsRef<str>> FromIterator<T> for ThreadedLexicon {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let iter = iter.into_iter();
let (strings, _) = iter.size_hint();
let lexicon = Self::with_capacity_for_size_hint(strings);
for s in iter {
lexicon.intern(s.as_ref());
}
lexicon
}
}
struct ThreadedLexiconInner<S = FxBuildHasher> {
shards: [Shard; NUM_SHARDS],
hasher: S,
}
impl ThreadedLexiconInner {
fn new() -> Self {
Self::with_hasher(FxBuildHasher)
}
}
impl<S: BuildHasher> ThreadedLexiconInner<S> {
fn with_hasher(hasher: S) -> Self {
Self::with_capacity_and_hasher(0, 0, hasher)
}
fn with_capacity_and_hasher(strings: usize, bytes: usize, hasher: S) -> Self {
let strings_per_shard = strings.div_ceil(NUM_SHARDS);
let bytes_per_shard = bytes.div_ceil(NUM_SHARDS);
Self {
shards: core::array::from_fn(|_| Shard::with_capacity(strings_per_shard, bytes_per_shard)),
hasher,
}
}
#[inline]
#[cfg_attr(test, mutants::skip)] fn shard_of(h: u64) -> usize {
(h.wrapping_mul(MIX) >> (64 - SHARD_BITS)) as usize
}
#[inline]
#[cfg_attr(test, mutants::skip)] fn hash_bytes(&self, b: &[u8]) -> u64 {
let mut hasher = self.hasher.build_hasher();
hasher.write(b);
hasher.finish()
}
#[inline]
fn hash_str(&self, s: &str) -> u64 {
self.hash_bytes(s.as_bytes())
}
#[inline]
fn intern(&self, s: &str) -> Sym {
let h = self.hash_str(s);
let idx = Self::shard_of(h);
self.shards[idx].intern(idx, h, s, &|t: &[u8]| self.hash_bytes(t))
}
#[inline]
fn intern_bytes(&self, bytes: &[u8]) -> Result<Sym, core::str::Utf8Error> {
let h = self.hash_bytes(bytes);
let idx = Self::shard_of(h);
self.shards[idx].intern_bytes(idx, h, bytes, &|t: &[u8]| self.hash_bytes(t))
}
#[inline]
fn get(&self, s: &str) -> Option<Sym> {
let h = self.hash_str(s);
self.shards[Self::shard_of(h)].get(h, s)
}
fn len(&self) -> usize {
self.shards.iter().map(Shard::len).sum()
}
fn into_reader(self) -> ThreadedReader {
ThreadedReader::new(Box::new(self.shards.map(Shard::freeze)))
}
fn build_reader(&self) -> ThreadedReader {
let guards: [ShardReadGuard<'_>; NUM_SHARDS] = core::array::from_fn(|i| self.shards[i].read_guard());
let readers = core::array::from_fn(|i| Shard::snapshot_locked(&guards[i]));
ThreadedReader::new(Box::new(readers))
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::{NUM_SHARDS, ThreadedLexicon};
#[test]
fn from_iter_preallocates_from_large_lower_size_hint() {
let strings = NUM_SHARDS * 8;
let lexicon: ThreadedLexicon = (0..strings).map(|_| "same").collect();
for shard in &lexicon.0.shards {
let (dedup, offsets, _) = shard.capacities();
assert!(dedup >= 8);
assert!(offsets >= 9);
}
}
#[test]
fn from_iter_does_not_preallocate_every_shard_for_small_hint() {
let lexicon: ThreadedLexicon = core::iter::once("same").collect();
let allocated_shards = lexicon.0.shards.iter().filter(|shard| shard.capacities().0 != 0).count();
assert_eq!(allocated_shards, 1);
}
}