use alloc::string::{String, ToString};
use core::fmt;
use std::sync::{PoisonError, RwLock};
use crate::error::InternError;
use crate::interner::Interner;
use crate::lookup::Lookup;
use crate::symbol::Symbol;
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub struct ConcurrentInterner {
inner: RwLock<Interner>,
}
impl ConcurrentInterner {
#[must_use]
pub fn new() -> Self {
Self {
inner: RwLock::new(Interner::new()),
}
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
inner: RwLock::new(Interner::with_capacity(capacity)),
}
}
pub fn intern(&self, s: &str) -> Symbol {
{
let guard = self.inner.read().unwrap_or_else(PoisonError::into_inner);
if let Some(symbol) = guard.get(s) {
return symbol;
}
}
let mut guard = self.inner.write().unwrap_or_else(PoisonError::into_inner);
guard.intern(s)
}
pub fn try_intern(&self, s: &str) -> Result<Symbol, InternError> {
{
let guard = self.inner.read().unwrap_or_else(PoisonError::into_inner);
if let Some(symbol) = guard.get(s) {
return Ok(symbol);
}
}
let mut guard = self.inner.write().unwrap_or_else(PoisonError::into_inner);
guard.try_intern(s)
}
#[must_use]
pub fn get(&self, s: &str) -> Option<Symbol> {
self.inner
.read()
.unwrap_or_else(PoisonError::into_inner)
.get(s)
}
pub fn resolve_with<R, F>(&self, symbol: Symbol, f: F) -> Option<R>
where
F: FnOnce(&str) -> R,
{
self.inner
.read()
.unwrap_or_else(PoisonError::into_inner)
.resolve(symbol)
.map(f)
}
#[must_use]
pub fn resolve(&self, symbol: Symbol) -> Option<String> {
self.resolve_with(symbol, ToString::to_string)
}
#[must_use]
pub fn len(&self) -> usize {
self.inner
.read()
.unwrap_or_else(PoisonError::into_inner)
.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Default for ConcurrentInterner {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl Lookup for ConcurrentInterner {
#[inline]
fn get(&self, s: &str) -> Option<Symbol> {
ConcurrentInterner::get(self, s)
}
#[inline]
fn resolve_with<R, F>(&self, symbol: Symbol, f: F) -> Option<R>
where
F: FnOnce(&str) -> R,
{
ConcurrentInterner::resolve_with(self, symbol, f)
}
#[inline]
fn len(&self) -> usize {
ConcurrentInterner::len(self)
}
#[inline]
fn is_empty(&self) -> bool {
ConcurrentInterner::is_empty(self)
}
}
impl fmt::Debug for ConcurrentInterner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ConcurrentInterner")
.field("strings", &self.len())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_intern_deduplicates() {
let interner = ConcurrentInterner::new();
let a = interner.intern("x");
let b = interner.intern("x");
assert_eq!(a, b);
assert_eq!(interner.len(), 1);
}
#[test]
fn test_resolve_roundtrips() {
let interner = ConcurrentInterner::new();
let sym = interner.intern("value");
assert_eq!(interner.resolve(sym).as_deref(), Some("value"));
assert_eq!(interner.resolve_with(sym, str::len), Some(5));
}
#[test]
fn test_get_does_not_intern() {
let interner = ConcurrentInterner::new();
assert_eq!(interner.get("absent"), None);
assert!(interner.is_empty());
}
#[test]
fn test_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ConcurrentInterner>();
}
#[test]
fn test_recovers_from_poisoned_lock() {
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;
let interner = Arc::new(ConcurrentInterner::new());
let first = interner.intern("before");
let poisoner = Arc::clone(&interner);
let _ = catch_unwind(AssertUnwindSafe(|| {
let _guard = poisoner
.inner
.write()
.unwrap_or_else(PoisonError::into_inner);
panic!("poison the lock");
}));
assert_eq!(interner.resolve(first).as_deref(), Some("before"));
let second = interner.intern("after");
assert_eq!(interner.resolve(second).as_deref(), Some("after"));
}
}