Skip to main content

ConcurrentInterner

Struct ConcurrentInterner 

Source
pub struct ConcurrentInterner { /* private fields */ }
Available on crate feature std only.
Expand description

A thread-safe string interner that many threads can intern into at once.

ConcurrentInterner wraps the single-threaded Interner in an RwLock and exposes the same operations through &self, so a pool of lexer or parser threads can share one symbol space. It is additive, not a rewrite: the storage, the dedup index, and the symbol stability guarantees are exactly those of Interner; this type only adds the synchronisation.

§Correctness under contention

Interning takes a two-step path. A string that is already present is found under a shared read lock, so the common warm-cache case — most identifiers have been seen before — runs concurrently across threads with no exclusive access. Only a genuinely new string escalates to the exclusive write lock, and the insert re-checks for the string while holding it, so two threads racing to intern the same new string still resolve to one symbol: the writer that loses the race finds the winner’s entry instead of creating a duplicate. The RwLock’s exclusivity is what makes “no duplicate symbols for the same string” hold without a custom lock-free protocol.

Reads dominate once warm, so the RwLock is the right primitive here; writes to new strings serialise. If write contention on a write-heavy workload ever shows up in benchmarks, the storage can be sharded behind this same surface without changing the API.

§Lock poisoning

If a thread panics while holding the lock it becomes poisoned. The interner’s own operations do not panic, so a poisoned lock means a panic originated elsewhere; rather than propagate that as a second panic, every method recovers the guard and continues. The stored data is structurally intact because interning is the only mutator and it does not unwind partway through under any input that fits in memory.

§Examples

use std::sync::Arc;
use std::thread;

use intern_lang::ConcurrentInterner;

let interner = Arc::new(ConcurrentInterner::new());

let mut handles = Vec::new();
for _ in 0..4 {
    let interner = Arc::clone(&interner);
    handles.push(thread::spawn(move || {
        // Every thread interns the same names; they all agree on the symbols.
        (interner.intern("loop"), interner.intern("break"))
    }));
}

let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
let first = results[0];
assert!(results.iter().all(|&pair| pair == first));
assert_eq!(interner.len(), 2); // "loop" and "break", interned once each

Implementations§

Source§

impl ConcurrentInterner

Source

pub fn new() -> Self

Creates an empty concurrent interner. Like Interner::new, no allocation happens until the first string is interned.

§Examples
use intern_lang::ConcurrentInterner;

let interner = ConcurrentInterner::new();
assert!(interner.is_empty());
Source

pub fn with_capacity(capacity: usize) -> Self

Creates an empty concurrent interner sized for about capacity distinct strings before the dedup index grows. See Interner::with_capacity.

§Examples
use intern_lang::ConcurrentInterner;

let interner = ConcurrentInterner::with_capacity(4_096);
assert!(interner.is_empty());
Source

pub fn intern(&self, s: &str) -> Symbol

Interns s from a shared reference, returning its Symbol.

A string already present is resolved under a shared read lock; only a new string takes the exclusive write lock. The same string always yields the same symbol, even when several threads intern it at once.

§Examples
use intern_lang::ConcurrentInterner;

let interner = ConcurrentInterner::new();
let a = interner.intern("shared");
let b = interner.intern("shared");
assert_eq!(a, b);
Source

pub fn try_intern(&self, s: &str) -> Result<Symbol, InternError>

Interns s from a shared reference, returning its Symbol, or an error if the symbol space is exhausted.

The fallible counterpart to intern, with the same two-step locking. A string already present is returned under the read lock and never errors; only a new string at the symbol-space bound returns InternError::SymbolSpaceExhausted.

§Errors

Returns InternError::SymbolSpaceExhausted when s is new and the interner has issued all of its symbols — unreachable for any input that fits in memory.

§Examples
use intern_lang::ConcurrentInterner;

let interner = ConcurrentInterner::new();
let sym = interner.try_intern("name").expect("space available");
assert_eq!(interner.try_intern("name"), Ok(sym));
Source

pub fn get(&self, s: &str) -> Option<Symbol>

Returns the symbol for s if it has already been interned, without interning it.

§Examples
use intern_lang::ConcurrentInterner;

let interner = ConcurrentInterner::new();
let sym = interner.intern("present");
assert_eq!(interner.get("present"), Some(sym));
assert_eq!(interner.get("absent"), None);
Source

pub fn resolve_with<R, F>(&self, symbol: Symbol, f: F) -> Option<R>
where F: FnOnce(&str) -> R,

Runs f against the string symbol names, returning its result, or None if symbol is out of range.

The read lock is held only for the duration of f, so the borrow stays zero-copy without escaping the lock. Keep f short; it runs under the lock.

§Examples
use intern_lang::ConcurrentInterner;

let interner = ConcurrentInterner::new();
let sym = interner.intern("measured");
assert_eq!(interner.resolve_with(sym, str::len), Some(8));
Source

pub fn resolve(&self, symbol: Symbol) -> Option<String>

Resolves symbol to an owned String, or None if it is out of range.

This is the ergonomic counterpart to resolve_with: it copies the bytes out so the result outlives the lock. On a hot path that only inspects the string, prefer resolve_with to avoid the allocation.

§Examples
use intern_lang::ConcurrentInterner;

let interner = ConcurrentInterner::new();
let sym = interner.intern("owned");
assert_eq!(interner.resolve(sym).as_deref(), Some("owned"));
Source

pub fn len(&self) -> usize

Returns the number of distinct strings interned so far.

§Examples
use intern_lang::ConcurrentInterner;

let interner = ConcurrentInterner::new();
let _ = interner.intern("a");
let _ = interner.intern("a");
let _ = interner.intern("b");
assert_eq!(interner.len(), 2);
Source

pub fn is_empty(&self) -> bool

Returns true if no strings have been interned.

§Examples
use intern_lang::ConcurrentInterner;

let interner = ConcurrentInterner::new();
assert!(interner.is_empty());
let _ = interner.intern("x");
assert!(!interner.is_empty());

Trait Implementations§

Source§

impl Debug for ConcurrentInterner

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for ConcurrentInterner

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl Lookup for ConcurrentInterner

Source§

fn get(&self, s: &str) -> Option<Symbol>

Returns the symbol for s if it has already been interned, without interning it.
Source§

fn resolve_with<R, F>(&self, symbol: Symbol, f: F) -> Option<R>
where F: FnOnce(&str) -> R,

Runs f against the string symbol names, returning its result, or None if symbol is out of range for this interner. Read more
Source§

fn len(&self) -> usize

Returns the number of distinct strings interned so far.
Source§

fn is_empty(&self) -> bool

Returns true if no strings have been interned.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.