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