Skip to main content

intern_lang/
interner.rs

1//! The single-threaded [`Interner`]: a contiguous string store with a
2//! deduplicating index.
3
4use alloc::string::String;
5use alloc::vec::Vec;
6use core::fmt;
7
8use crate::symbol::Symbol;
9
10/// Initial number of slots in the dedup index. A power of two so the hash maps to
11/// a slot with a single mask. Sixteen keeps an empty interner cheap while still
12/// avoiding an immediate resize for small inputs.
13const INITIAL_CAPACITY: usize = 16;
14
15/// Where one interned string lives inside the backing buffer. `start` and `len`
16/// are byte offsets into [`Interner::buf`]; the pair is the symbol's permanent
17/// coordinates, and because the buffer only ever appends, they never change once
18/// assigned — that is what keeps a symbol resolving to the same bytes after the
19/// store grows.
20#[derive(Clone, Copy)]
21struct Span {
22    start: usize,
23    len: usize,
24}
25
26/// One slot in the open-addressing dedup index. `id` is the 1-based symbol id, or
27/// `0` for an empty slot (symbol ids start at one, so zero is a free sentinel).
28/// `hash` caches the low 32 bits of the string's hash so a probe can reject a
29/// non-match without touching the backing buffer at all.
30#[derive(Clone, Copy)]
31struct Slot {
32    hash: u32,
33    id: u32,
34}
35
36impl Slot {
37    const EMPTY: Slot = Slot { hash: 0, id: 0 };
38
39    #[inline]
40    fn is_empty(self) -> bool {
41        self.id == 0
42    }
43}
44
45/// A single-threaded string interner.
46///
47/// `Interner` maps each distinct string to a small [`Symbol`], stores the bytes
48/// exactly once in a contiguous buffer, and hands back integer handles. Interning
49/// a string it has already seen is a hash lookup with no allocation and no copy;
50/// resolving a symbol borrows the original bytes straight out of the buffer.
51///
52/// # Design
53///
54/// Bytes live once, appended end to end in a single `String`. A symbol is an
55/// index into a side table of `(start, len)` spans into that buffer, so a symbol
56/// is four bytes regardless of how long its string is. Deduplication runs through
57/// an open-addressing hash index that stores symbol ids, not strings, so it adds
58/// no second copy of the bytes. The buffer only ever appends and the span table
59/// only ever grows, so a symbol issued early keeps resolving to the same string
60/// for the interner's whole lifetime, including after either structure
61/// reallocates — [`resolve`](Interner::resolve) recomputes the slice from the
62/// current buffer on each call rather than holding a borrowed pointer, so growth
63/// can never dangle a previously issued symbol.
64///
65/// # Capacity
66///
67/// Symbol ids span `1..=u32::MAX`, so an interner holds up to `u32::MAX` distinct
68/// strings. Reaching that bound requires interning over four billion *distinct*
69/// strings, which exhausts memory long before the id space — the span table alone
70/// would need tens of gigabytes. A defined, non-panicking exhaustion result is
71/// scheduled for a later release; until then the boundary is unreachable for any
72/// input that fits in memory.
73///
74/// # Examples
75///
76/// ```
77/// use intern_lang::Interner;
78///
79/// let mut interner = Interner::new();
80///
81/// let print = interner.intern("print");
82/// let again = interner.intern("print");
83/// let read = interner.intern("read");
84///
85/// // Deduplication: the same string always yields the same symbol.
86/// assert_eq!(print, again);
87/// assert_ne!(print, read);
88///
89/// // Resolution borrows the stored bytes back out.
90/// assert_eq!(interner.resolve(print), Some("print"));
91/// assert_eq!(interner.len(), 2);
92/// ```
93pub struct Interner {
94    /// Contiguous backing store. Every interned string's bytes are appended here
95    /// once and never moved relative to their span.
96    buf: String,
97    /// Span per symbol, indexed by the symbol's 0-based [`Symbol::index`]. Push
98    /// order is interning order, so `spans.len()` is also the next 1-based id.
99    spans: Vec<Span>,
100    /// Open-addressing dedup index. Length is a power of two; `mask` is
101    /// `len - 1`. Empty until the first insert.
102    table: Vec<Slot>,
103    /// `table.len() - 1`, for mapping a hash to a slot with a single `&`.
104    mask: usize,
105}
106
107impl Interner {
108    /// Creates an empty interner.
109    ///
110    /// No allocation happens until the first string is interned, so an interner
111    /// that is created but never used costs nothing.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use intern_lang::Interner;
117    ///
118    /// let interner = Interner::new();
119    /// assert!(interner.is_empty());
120    /// ```
121    #[inline]
122    #[must_use]
123    pub fn new() -> Self {
124        Self {
125            buf: String::new(),
126            spans: Vec::new(),
127            table: Vec::new(),
128            mask: 0,
129        }
130    }
131
132    /// Creates an empty interner sized to hold about `capacity` distinct strings
133    /// before the dedup index has to grow.
134    ///
135    /// This pre-allocates the span table and the hash index. The backing byte
136    /// buffer is left to grow on demand, since the total byte length cannot be
137    /// predicted from a string count. Use this when the rough number of distinct
138    /// identifiers is known ahead of time — for example, sizing from a previous
139    /// compilation — to avoid a series of reallocations during warm-up.
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use intern_lang::Interner;
145    ///
146    /// let mut interner = Interner::with_capacity(1_024);
147    /// let sym = interner.intern("identifier");
148    /// assert_eq!(interner.resolve(sym), Some("identifier"));
149    /// ```
150    #[must_use]
151    pub fn with_capacity(capacity: usize) -> Self {
152        let mut interner = Self::new();
153        if capacity > 0 {
154            interner.spans.reserve(capacity);
155            let table_cap = table_capacity_for(capacity);
156            interner.resize_table(table_cap);
157        }
158        interner
159    }
160
161    /// Interns `s`, returning its [`Symbol`].
162    ///
163    /// If `s` has been interned before, the existing symbol is returned and
164    /// nothing is allocated or copied. Otherwise the bytes are appended to the
165    /// backing store, a fresh symbol is assigned, and that symbol is returned.
166    /// Either way the result round-trips: `interner.resolve(interner.intern(s))`
167    /// is always `Some(s)`.
168    ///
169    /// # Examples
170    ///
171    /// ```
172    /// use intern_lang::Interner;
173    ///
174    /// let mut interner = Interner::new();
175    /// let a = interner.intern("while");
176    /// let b = interner.intern("while");
177    /// let c = interner.intern("until");
178    ///
179    /// assert_eq!(a, b);            // deduplicated
180    /// assert_ne!(a, c);            // distinct strings, distinct symbols
181    /// assert_eq!(interner.resolve(a), Some("while"));
182    /// ```
183    pub fn intern(&mut self, s: &str) -> Symbol {
184        let hash = hash_bytes(s.as_bytes());
185        if let Some(symbol) = self.lookup(s, hash) {
186            return symbol;
187        }
188        self.insert_new(s, hash)
189    }
190
191    /// Looks up `s` without interning it, returning its [`Symbol`] if it is
192    /// already present.
193    ///
194    /// Unlike [`intern`](Interner::intern), this never mutates the interner: a
195    /// miss returns `None` rather than allocating a new symbol. Use it to ask
196    /// "has this name been seen?" without growing the symbol space.
197    ///
198    /// # Examples
199    ///
200    /// ```
201    /// use intern_lang::Interner;
202    ///
203    /// let mut interner = Interner::new();
204    /// let sym = interner.intern("declared");
205    ///
206    /// assert_eq!(interner.get("declared"), Some(sym));
207    /// assert_eq!(interner.get("undeclared"), None);
208    /// ```
209    #[must_use]
210    pub fn get(&self, s: &str) -> Option<Symbol> {
211        self.lookup(s, hash_bytes(s.as_bytes()))
212    }
213
214    /// Resolves `symbol` back to the string it names, borrowing the bytes from the
215    /// backing store.
216    ///
217    /// Returns `Some(&str)` for any symbol this interner issued, and `None` for a
218    /// symbol whose id is out of range — most often one issued by a different
219    /// interner. A symbol from another interner whose id happens to fall in range
220    /// resolves to *this* interner's string at that id; symbols are only
221    /// meaningful with the interner that produced them.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// use intern_lang::Interner;
227    ///
228    /// let mut interner = Interner::new();
229    /// let sym = interner.intern("resolved");
230    /// assert_eq!(interner.resolve(sym), Some("resolved"));
231    ///
232    /// // A symbol from an interner that issued more symbols is out of range here.
233    /// let mut other = Interner::new();
234    /// let _ = other.intern("a");
235    /// let high = other.intern("b");
236    /// assert_eq!(interner.resolve(high), None);
237    /// ```
238    #[must_use]
239    pub fn resolve(&self, symbol: Symbol) -> Option<&str> {
240        let span = self.spans.get(symbol.index())?;
241        Some(&self.buf[span.start..span.start + span.len])
242    }
243
244    /// Runs `f` against the string `symbol` names, returning its result, or `None`
245    /// if `symbol` is out of range.
246    ///
247    /// This is the [`Lookup`](crate::Lookup) trait's resolution form. For the
248    /// single-threaded interner it is a thin wrapper over
249    /// [`resolve`](Interner::resolve) — prefer `resolve` here, which hands back the
250    /// borrowed slice directly. The closure form exists so the same generic code
251    /// works against the [`ConcurrentInterner`](crate::ConcurrentInterner), where
252    /// the borrow cannot outlive the read lock.
253    ///
254    /// # Examples
255    ///
256    /// ```
257    /// use intern_lang::Interner;
258    ///
259    /// let mut interner = Interner::new();
260    /// let sym = interner.intern("identifier");
261    /// assert_eq!(interner.resolve_with(sym, str::len), Some(10));
262    /// ```
263    pub fn resolve_with<R, F>(&self, symbol: Symbol, f: F) -> Option<R>
264    where
265        F: FnOnce(&str) -> R,
266    {
267        self.resolve(symbol).map(f)
268    }
269
270    /// Returns the number of distinct strings interned so far.
271    ///
272    /// This is also the id that the next newly interned string will receive.
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// use intern_lang::Interner;
278    ///
279    /// let mut interner = Interner::new();
280    /// assert_eq!(interner.len(), 0);
281    /// let _ = interner.intern("x");
282    /// let _ = interner.intern("x"); // duplicate, not counted again
283    /// let _ = interner.intern("y");
284    /// assert_eq!(interner.len(), 2);
285    /// ```
286    #[inline]
287    #[must_use]
288    pub fn len(&self) -> usize {
289        self.spans.len()
290    }
291
292    /// Returns `true` if no strings have been interned.
293    ///
294    /// # Examples
295    ///
296    /// ```
297    /// use intern_lang::Interner;
298    ///
299    /// let mut interner = Interner::new();
300    /// assert!(interner.is_empty());
301    /// let _ = interner.intern("x");
302    /// assert!(!interner.is_empty());
303    /// ```
304    #[inline]
305    #[must_use]
306    pub fn is_empty(&self) -> bool {
307        self.spans.is_empty()
308    }
309
310    /// Probes the dedup index for `s`. Returns its symbol if present.
311    fn lookup(&self, s: &str, hash: u64) -> Option<Symbol> {
312        if self.table.is_empty() {
313            return None;
314        }
315        let fingerprint = hash as u32;
316        let mut idx = (hash as usize) & self.mask;
317        loop {
318            let slot = self.table[idx];
319            if slot.is_empty() {
320                return None;
321            }
322            if slot.hash == fingerprint && self.span_str(slot.id) == s {
323                return Some(Symbol::from_raw(slot.id));
324            }
325            idx = (idx + 1) & self.mask;
326        }
327    }
328
329    /// Appends `s` to the backing store, assigns it a fresh symbol, and records it
330    /// in the dedup index. The caller has already established that `s` is not
331    /// present and computed its `hash`.
332    fn insert_new(&mut self, s: &str, hash: u64) -> Symbol {
333        self.reserve_one();
334
335        let span = Span {
336            start: self.buf.len(),
337            len: s.len(),
338        };
339        self.buf.push_str(s);
340        self.spans.push(span);
341
342        // The 1-based id equals the new length of the span table.
343        let id = id_for(self.spans.len());
344        self.insert_slot(Slot {
345            hash: hash as u32,
346            id,
347        });
348        Symbol::from_raw(id)
349    }
350
351    /// Places `slot` at its first empty probe position. The table is guaranteed to
352    /// have room because [`reserve_one`](Interner::reserve_one) ran first.
353    fn insert_slot(&mut self, slot: Slot) {
354        let mut idx = (slot.hash as usize) & self.mask;
355        while !self.table[idx].is_empty() {
356            idx = (idx + 1) & self.mask;
357        }
358        self.table[idx] = slot;
359    }
360
361    /// Ensures the dedup index has room for one more entry under a 0.75 load
362    /// factor, allocating or doubling the table as needed.
363    fn reserve_one(&mut self) {
364        let occupied_after = self.spans.len() + 1;
365        if self.table.is_empty() {
366            self.resize_table(INITIAL_CAPACITY);
367        } else if occupied_after * 4 > self.table.len() * 3 {
368            self.resize_table(self.table.len() * 2);
369        }
370    }
371
372    /// Reallocates the dedup index to `new_cap` slots (a power of two) and
373    /// re-inserts every existing symbol. The backing buffer and span table are
374    /// untouched, so no symbol changes identity.
375    fn resize_table(&mut self, new_cap: usize) {
376        let mut table = Vec::new();
377        table.resize(new_cap, Slot::EMPTY);
378        let mask = new_cap - 1;
379
380        for (i, span) in self.spans.iter().enumerate() {
381            let s = &self.buf[span.start..span.start + span.len];
382            let hash = hash_bytes(s.as_bytes());
383            let id = id_for(i + 1);
384            let mut idx = (hash as usize) & mask;
385            while !table[idx].is_empty() {
386                idx = (idx + 1) & mask;
387            }
388            table[idx] = Slot {
389                hash: hash as u32,
390                id,
391            };
392        }
393
394        self.table = table;
395        self.mask = mask;
396    }
397
398    /// Returns the string for a 1-based symbol id. Only called with ids the
399    /// interner issued, so the span always exists.
400    #[inline]
401    fn span_str(&self, id: u32) -> &str {
402        let span = self.spans[id as usize - 1];
403        &self.buf[span.start..span.start + span.len]
404    }
405}
406
407impl Default for Interner {
408    #[inline]
409    fn default() -> Self {
410        Self::new()
411    }
412}
413
414impl crate::Lookup for Interner {
415    #[inline]
416    fn get(&self, s: &str) -> Option<Symbol> {
417        Interner::get(self, s)
418    }
419
420    #[inline]
421    fn resolve_with<R, F>(&self, symbol: Symbol, f: F) -> Option<R>
422    where
423        F: FnOnce(&str) -> R,
424    {
425        Interner::resolve_with(self, symbol, f)
426    }
427
428    #[inline]
429    fn len(&self) -> usize {
430        Interner::len(self)
431    }
432
433    #[inline]
434    fn is_empty(&self) -> bool {
435        Interner::is_empty(self)
436    }
437}
438
439impl fmt::Debug for Interner {
440    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441        f.debug_struct("Interner")
442            .field("strings", &self.spans.len())
443            .field("bytes", &self.buf.len())
444            .finish_non_exhaustive()
445    }
446}
447
448/// Converts a span-table length into a 1-based symbol id.
449///
450/// `len` is bounded by available memory — each interned string costs a span, a
451/// table slot, and at least one byte — so it stays within `u32` long before the
452/// cast could lose information. The saturating fallback keeps the conversion free
453/// of `unwrap`/`expect` and panics, and is unreachable for any in-memory input.
454#[inline]
455fn id_for(len: usize) -> u32 {
456    u32::try_from(len).unwrap_or(u32::MAX)
457}
458
459/// Rounds a desired distinct-string count up to a power-of-two table capacity that
460/// holds it under a 0.75 load factor, never below [`INITIAL_CAPACITY`].
461#[inline]
462fn table_capacity_for(strings: usize) -> usize {
463    let target = strings.saturating_mul(4) / 3 + 1;
464    target.max(INITIAL_CAPACITY).next_power_of_two()
465}
466
467/// Hashes `bytes` with an FxHash-style multiply-rotate over 64-bit words.
468///
469/// The string length seeds the state so that strings differing only in trailing
470/// content within a word boundary (for example `"ab"` versus `"ab\0"`) do not
471/// collide on the fast fingerprint. This is a non-cryptographic hash chosen for
472/// throughput on short identifiers; correctness never depends on it, since the
473/// dedup index always confirms a candidate with a full byte comparison.
474#[inline]
475fn hash_bytes(bytes: &[u8]) -> u64 {
476    const K: u64 = 0x517c_c1b7_2722_0a95;
477
478    let mut hash = bytes.len() as u64;
479    let mut chunks = bytes.chunks_exact(8);
480    for chunk in chunks.by_ref() {
481        // `chunks_exact(8)` always yields eight bytes, so the conversion holds;
482        // the fallback is dead and only keeps this free of `unwrap`.
483        let word = u64::from_le_bytes(<[u8; 8]>::try_from(chunk).unwrap_or([0; 8]));
484        hash = (hash.rotate_left(5) ^ word).wrapping_mul(K);
485    }
486
487    let remainder = chunks.remainder();
488    if !remainder.is_empty() {
489        let mut tail = [0u8; 8];
490        tail[..remainder.len()].copy_from_slice(remainder);
491        let word = u64::from_le_bytes(tail);
492        hash = (hash.rotate_left(5) ^ word).wrapping_mul(K);
493    }
494
495    hash
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    #[test]
503    fn test_intern_same_string_returns_same_symbol() {
504        let mut interner = Interner::new();
505        let a = interner.intern("name");
506        let b = interner.intern("name");
507        assert_eq!(a, b);
508        assert_eq!(interner.len(), 1);
509    }
510
511    #[test]
512    fn test_intern_distinct_strings_return_distinct_symbols() {
513        let mut interner = Interner::new();
514        let a = interner.intern("one");
515        let b = interner.intern("two");
516        assert_ne!(a, b);
517        assert_eq!(interner.len(), 2);
518    }
519
520    #[test]
521    fn test_resolve_roundtrips() {
522        let mut interner = Interner::new();
523        for s in ["", "a", "alpha", "a longer identifier with spaces"] {
524            let sym = interner.intern(s);
525            assert_eq!(interner.resolve(sym), Some(s));
526        }
527    }
528
529    #[test]
530    fn test_resolve_out_of_range_symbol_is_none() {
531        let mut issuer = Interner::new();
532        let _ = issuer.intern("a");
533        let high = issuer.intern("b");
534
535        let empty = Interner::new();
536        assert_eq!(empty.resolve(high), None);
537    }
538
539    #[test]
540    fn test_get_does_not_intern() {
541        let mut interner = Interner::new();
542        assert_eq!(interner.get("absent"), None);
543        assert_eq!(interner.len(), 0);
544        let sym = interner.intern("absent");
545        assert_eq!(interner.get("absent"), Some(sym));
546    }
547
548    #[test]
549    fn test_ids_are_sequential_from_one() {
550        let mut interner = Interner::new();
551        assert_eq!(interner.intern("a").as_u32(), 1);
552        assert_eq!(interner.intern("b").as_u32(), 2);
553        assert_eq!(interner.intern("a").as_u32(), 1);
554        assert_eq!(interner.intern("c").as_u32(), 3);
555    }
556
557    #[test]
558    fn test_growth_preserves_earlier_symbols() {
559        let mut interner = Interner::new();
560        let mut remembered = alloc::vec::Vec::new();
561        // Enough distinct strings to force several table resizes and buffer
562        // reallocations.
563        for i in 0..10_000 {
564            let s = alloc::format!("symbol_{i}");
565            remembered.push((interner.intern(&s), s));
566        }
567        for (sym, s) in &remembered {
568            assert_eq!(interner.resolve(*sym), Some(s.as_str()));
569        }
570    }
571
572    #[test]
573    fn test_empty_string_is_interned() {
574        let mut interner = Interner::new();
575        let empty = interner.intern("");
576        assert_eq!(interner.resolve(empty), Some(""));
577        assert_eq!(interner.intern(""), empty);
578    }
579
580    #[test]
581    fn test_unicode_roundtrips() {
582        let mut interner = Interner::new();
583        for s in ["café", "naïve", "日本語", "emoji 🦀", "Ωμέγα"] {
584            let sym = interner.intern(s);
585            assert_eq!(interner.resolve(sym), Some(s));
586        }
587    }
588
589    #[test]
590    fn test_with_capacity_behaves_like_new() {
591        let mut interner = Interner::with_capacity(64);
592        let sym = interner.intern("preallocated");
593        assert_eq!(interner.resolve(sym), Some("preallocated"));
594        assert_eq!(interner.len(), 1);
595    }
596
597    #[test]
598    fn test_strings_differing_only_in_trailing_byte_are_distinct() {
599        let mut interner = Interner::new();
600        let a = interner.intern("ab");
601        let b = interner.intern("ab\0");
602        assert_ne!(a, b);
603        assert_eq!(interner.resolve(a), Some("ab"));
604        assert_eq!(interner.resolve(b), Some("ab\0"));
605    }
606
607    #[test]
608    fn test_default_is_empty() {
609        let interner = Interner::default();
610        assert!(interner.is_empty());
611    }
612
613    #[test]
614    fn test_table_capacity_for_is_power_of_two_and_fits() {
615        for n in [0usize, 1, 12, 13, 100, 1000] {
616            let cap = table_capacity_for(n);
617            assert!(cap.is_power_of_two());
618            assert!(cap >= INITIAL_CAPACITY);
619            assert!(cap * 3 >= n.saturating_mul(4));
620        }
621    }
622}