Skip to main content

intern_lang/
concurrent.rs

1//! The thread-safe [`ConcurrentInterner`].
2
3use alloc::string::{String, ToString};
4use core::fmt;
5use std::sync::{PoisonError, RwLock};
6
7use crate::error::InternError;
8use crate::interner::Interner;
9use crate::lookup::Lookup;
10use crate::symbol::Symbol;
11
12/// A thread-safe string interner that many threads can intern into at once.
13///
14/// `ConcurrentInterner` wraps the single-threaded [`Interner`] in an `RwLock` and
15/// exposes the same operations through `&self`, so a pool of lexer or parser
16/// threads can share one symbol space. It is additive, not a rewrite: the storage,
17/// the dedup index, and the symbol stability guarantees are exactly those of
18/// [`Interner`]; this type only adds the synchronisation.
19///
20/// # Correctness under contention
21///
22/// Interning takes a two-step path. A string that is already present is found
23/// under a *shared read lock*, so the common warm-cache case — most identifiers
24/// have been seen before — runs concurrently across threads with no exclusive
25/// access. Only a genuinely new string escalates to the *exclusive write lock*,
26/// and the insert re-checks for the string while holding it, so two threads
27/// racing to intern the same new string still resolve to one symbol: the writer
28/// that loses the race finds the winner's entry instead of creating a duplicate.
29/// The `RwLock`'s exclusivity is what makes "no duplicate symbols for the same
30/// string" hold without a custom lock-free protocol.
31///
32/// Reads dominate once warm, so the `RwLock` is the right primitive here; writes
33/// to new strings serialise. If write contention on a write-heavy workload ever
34/// shows up in benchmarks, the storage can be sharded behind this same surface
35/// without changing the API.
36///
37/// # Lock poisoning
38///
39/// If a thread panics while holding the lock it becomes poisoned. The interner's
40/// own operations do not panic, so a poisoned lock means a panic originated
41/// elsewhere; rather than propagate that as a second panic, every method recovers
42/// the guard and continues. The stored data is structurally intact because
43/// interning is the only mutator and it does not unwind partway through under any
44/// input that fits in memory.
45///
46/// # Examples
47///
48/// ```
49/// use std::sync::Arc;
50/// use std::thread;
51///
52/// use intern_lang::ConcurrentInterner;
53///
54/// let interner = Arc::new(ConcurrentInterner::new());
55///
56/// let mut handles = Vec::new();
57/// for _ in 0..4 {
58///     let interner = Arc::clone(&interner);
59///     handles.push(thread::spawn(move || {
60///         // Every thread interns the same names; they all agree on the symbols.
61///         (interner.intern("loop"), interner.intern("break"))
62///     }));
63/// }
64///
65/// let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
66/// let first = results[0];
67/// assert!(results.iter().all(|&pair| pair == first));
68/// assert_eq!(interner.len(), 2); // "loop" and "break", interned once each
69/// ```
70#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
71pub struct ConcurrentInterner {
72    inner: RwLock<Interner>,
73}
74
75impl ConcurrentInterner {
76    /// Creates an empty concurrent interner. Like [`Interner::new`], no allocation
77    /// happens until the first string is interned.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use intern_lang::ConcurrentInterner;
83    ///
84    /// let interner = ConcurrentInterner::new();
85    /// assert!(interner.is_empty());
86    /// ```
87    #[must_use]
88    pub fn new() -> Self {
89        Self {
90            inner: RwLock::new(Interner::new()),
91        }
92    }
93
94    /// Creates an empty concurrent interner sized for about `capacity` distinct
95    /// strings before the dedup index grows. See [`Interner::with_capacity`].
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use intern_lang::ConcurrentInterner;
101    ///
102    /// let interner = ConcurrentInterner::with_capacity(4_096);
103    /// assert!(interner.is_empty());
104    /// ```
105    #[must_use]
106    pub fn with_capacity(capacity: usize) -> Self {
107        Self {
108            inner: RwLock::new(Interner::with_capacity(capacity)),
109        }
110    }
111
112    /// Interns `s` from a shared reference, returning its [`Symbol`].
113    ///
114    /// A string already present is resolved under a shared read lock; only a new
115    /// string takes the exclusive write lock. The same string always yields the
116    /// same symbol, even when several threads intern it at once.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use intern_lang::ConcurrentInterner;
122    ///
123    /// let interner = ConcurrentInterner::new();
124    /// let a = interner.intern("shared");
125    /// let b = interner.intern("shared");
126    /// assert_eq!(a, b);
127    /// ```
128    pub fn intern(&self, s: &str) -> Symbol {
129        // Fast path: a string that already exists is found under a shared lock,
130        // so concurrent readers do not block one another.
131        {
132            let guard = self.inner.read().unwrap_or_else(PoisonError::into_inner);
133            if let Some(symbol) = guard.get(s) {
134                return symbol;
135            }
136        }
137        // Slow path: take the exclusive lock and intern. `Interner::intern`
138        // re-checks for the string, so a racer that inserted it between our read
139        // and write returns the existing symbol rather than a duplicate.
140        let mut guard = self.inner.write().unwrap_or_else(PoisonError::into_inner);
141        guard.intern(s)
142    }
143
144    /// Interns `s` from a shared reference, returning its [`Symbol`], or an error
145    /// if the symbol space is exhausted.
146    ///
147    /// The fallible counterpart to [`intern`](Self::intern), with the same
148    /// two-step locking. A string already present is returned under the read lock
149    /// and never errors; only a new string at the symbol-space bound returns
150    /// [`InternError::SymbolSpaceExhausted`].
151    ///
152    /// # Errors
153    ///
154    /// Returns [`InternError::SymbolSpaceExhausted`] when `s` is new and the
155    /// interner has issued all of its symbols — unreachable for any input that
156    /// fits in memory.
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// use intern_lang::ConcurrentInterner;
162    ///
163    /// let interner = ConcurrentInterner::new();
164    /// let sym = interner.try_intern("name").expect("space available");
165    /// assert_eq!(interner.try_intern("name"), Ok(sym));
166    /// ```
167    pub fn try_intern(&self, s: &str) -> Result<Symbol, InternError> {
168        {
169            let guard = self.inner.read().unwrap_or_else(PoisonError::into_inner);
170            if let Some(symbol) = guard.get(s) {
171                return Ok(symbol);
172            }
173        }
174        let mut guard = self.inner.write().unwrap_or_else(PoisonError::into_inner);
175        guard.try_intern(s)
176    }
177
178    /// Returns the symbol for `s` if it has already been interned, without
179    /// interning it.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// use intern_lang::ConcurrentInterner;
185    ///
186    /// let interner = ConcurrentInterner::new();
187    /// let sym = interner.intern("present");
188    /// assert_eq!(interner.get("present"), Some(sym));
189    /// assert_eq!(interner.get("absent"), None);
190    /// ```
191    #[must_use]
192    pub fn get(&self, s: &str) -> Option<Symbol> {
193        self.inner
194            .read()
195            .unwrap_or_else(PoisonError::into_inner)
196            .get(s)
197    }
198
199    /// Runs `f` against the string `symbol` names, returning its result, or `None`
200    /// if `symbol` is out of range.
201    ///
202    /// The read lock is held only for the duration of `f`, so the borrow stays
203    /// zero-copy without escaping the lock. Keep `f` short; it runs under the lock.
204    ///
205    /// # Examples
206    ///
207    /// ```
208    /// use intern_lang::ConcurrentInterner;
209    ///
210    /// let interner = ConcurrentInterner::new();
211    /// let sym = interner.intern("measured");
212    /// assert_eq!(interner.resolve_with(sym, str::len), Some(8));
213    /// ```
214    pub fn resolve_with<R, F>(&self, symbol: Symbol, f: F) -> Option<R>
215    where
216        F: FnOnce(&str) -> R,
217    {
218        self.inner
219            .read()
220            .unwrap_or_else(PoisonError::into_inner)
221            .resolve(symbol)
222            .map(f)
223    }
224
225    /// Resolves `symbol` to an owned `String`, or `None` if it is out of range.
226    ///
227    /// This is the ergonomic counterpart to [`resolve_with`](Self::resolve_with):
228    /// it copies the bytes out so the result outlives the lock. On a hot path that
229    /// only inspects the string, prefer `resolve_with` to avoid the allocation.
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use intern_lang::ConcurrentInterner;
235    ///
236    /// let interner = ConcurrentInterner::new();
237    /// let sym = interner.intern("owned");
238    /// assert_eq!(interner.resolve(sym).as_deref(), Some("owned"));
239    /// ```
240    #[must_use]
241    pub fn resolve(&self, symbol: Symbol) -> Option<String> {
242        self.resolve_with(symbol, ToString::to_string)
243    }
244
245    /// Returns the number of distinct strings interned so far.
246    ///
247    /// # Examples
248    ///
249    /// ```
250    /// use intern_lang::ConcurrentInterner;
251    ///
252    /// let interner = ConcurrentInterner::new();
253    /// let _ = interner.intern("a");
254    /// let _ = interner.intern("a");
255    /// let _ = interner.intern("b");
256    /// assert_eq!(interner.len(), 2);
257    /// ```
258    #[must_use]
259    pub fn len(&self) -> usize {
260        self.inner
261            .read()
262            .unwrap_or_else(PoisonError::into_inner)
263            .len()
264    }
265
266    /// Returns `true` if no strings have been interned.
267    ///
268    /// # Examples
269    ///
270    /// ```
271    /// use intern_lang::ConcurrentInterner;
272    ///
273    /// let interner = ConcurrentInterner::new();
274    /// assert!(interner.is_empty());
275    /// let _ = interner.intern("x");
276    /// assert!(!interner.is_empty());
277    /// ```
278    #[must_use]
279    pub fn is_empty(&self) -> bool {
280        self.len() == 0
281    }
282}
283
284impl Default for ConcurrentInterner {
285    #[inline]
286    fn default() -> Self {
287        Self::new()
288    }
289}
290
291impl Lookup for ConcurrentInterner {
292    #[inline]
293    fn get(&self, s: &str) -> Option<Symbol> {
294        ConcurrentInterner::get(self, s)
295    }
296
297    #[inline]
298    fn resolve_with<R, F>(&self, symbol: Symbol, f: F) -> Option<R>
299    where
300        F: FnOnce(&str) -> R,
301    {
302        ConcurrentInterner::resolve_with(self, symbol, f)
303    }
304
305    #[inline]
306    fn len(&self) -> usize {
307        ConcurrentInterner::len(self)
308    }
309
310    #[inline]
311    fn is_empty(&self) -> bool {
312        ConcurrentInterner::is_empty(self)
313    }
314}
315
316impl fmt::Debug for ConcurrentInterner {
317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318        f.debug_struct("ConcurrentInterner")
319            .field("strings", &self.len())
320            .finish_non_exhaustive()
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_intern_deduplicates() {
330        let interner = ConcurrentInterner::new();
331        let a = interner.intern("x");
332        let b = interner.intern("x");
333        assert_eq!(a, b);
334        assert_eq!(interner.len(), 1);
335    }
336
337    #[test]
338    fn test_resolve_roundtrips() {
339        let interner = ConcurrentInterner::new();
340        let sym = interner.intern("value");
341        assert_eq!(interner.resolve(sym).as_deref(), Some("value"));
342        assert_eq!(interner.resolve_with(sym, str::len), Some(5));
343    }
344
345    #[test]
346    fn test_get_does_not_intern() {
347        let interner = ConcurrentInterner::new();
348        assert_eq!(interner.get("absent"), None);
349        assert!(interner.is_empty());
350    }
351
352    #[test]
353    fn test_is_send_and_sync() {
354        fn assert_send_sync<T: Send + Sync>() {}
355        assert_send_sync::<ConcurrentInterner>();
356    }
357
358    #[test]
359    fn test_recovers_from_poisoned_lock() {
360        use std::panic::{AssertUnwindSafe, catch_unwind};
361        use std::sync::Arc;
362
363        let interner = Arc::new(ConcurrentInterner::new());
364        let first = interner.intern("before");
365
366        // Poison the lock by panicking while holding the write guard.
367        let poisoner = Arc::clone(&interner);
368        let _ = catch_unwind(AssertUnwindSafe(|| {
369            let _guard = poisoner
370                .inner
371                .write()
372                .unwrap_or_else(PoisonError::into_inner);
373            panic!("poison the lock");
374        }));
375
376        // The interner still works and earlier symbols still resolve.
377        assert_eq!(interner.resolve(first).as_deref(), Some("before"));
378        let second = interner.intern("after");
379        assert_eq!(interner.resolve(second).as_deref(), Some("after"));
380    }
381}