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    /// Returns the number of distinct strings interned so far.
245    ///
246    /// This is also the id that the next newly interned string will receive.
247    ///
248    /// # Examples
249    ///
250    /// ```
251    /// use intern_lang::Interner;
252    ///
253    /// let mut interner = Interner::new();
254    /// assert_eq!(interner.len(), 0);
255    /// let _ = interner.intern("x");
256    /// let _ = interner.intern("x"); // duplicate, not counted again
257    /// let _ = interner.intern("y");
258    /// assert_eq!(interner.len(), 2);
259    /// ```
260    #[inline]
261    #[must_use]
262    pub fn len(&self) -> usize {
263        self.spans.len()
264    }
265
266    /// Returns `true` if no strings have been interned.
267    ///
268    /// # Examples
269    ///
270    /// ```
271    /// use intern_lang::Interner;
272    ///
273    /// let mut interner = Interner::new();
274    /// assert!(interner.is_empty());
275    /// let _ = interner.intern("x");
276    /// assert!(!interner.is_empty());
277    /// ```
278    #[inline]
279    #[must_use]
280    pub fn is_empty(&self) -> bool {
281        self.spans.is_empty()
282    }
283
284    /// Probes the dedup index for `s`. Returns its symbol if present.
285    fn lookup(&self, s: &str, hash: u64) -> Option<Symbol> {
286        if self.table.is_empty() {
287            return None;
288        }
289        let fingerprint = hash as u32;
290        let mut idx = (hash as usize) & self.mask;
291        loop {
292            let slot = self.table[idx];
293            if slot.is_empty() {
294                return None;
295            }
296            if slot.hash == fingerprint && self.span_str(slot.id) == s {
297                return Some(Symbol::from_raw(slot.id));
298            }
299            idx = (idx + 1) & self.mask;
300        }
301    }
302
303    /// Appends `s` to the backing store, assigns it a fresh symbol, and records it
304    /// in the dedup index. The caller has already established that `s` is not
305    /// present and computed its `hash`.
306    fn insert_new(&mut self, s: &str, hash: u64) -> Symbol {
307        self.reserve_one();
308
309        let span = Span {
310            start: self.buf.len(),
311            len: s.len(),
312        };
313        self.buf.push_str(s);
314        self.spans.push(span);
315
316        // The 1-based id equals the new length of the span table.
317        let id = id_for(self.spans.len());
318        self.insert_slot(Slot {
319            hash: hash as u32,
320            id,
321        });
322        Symbol::from_raw(id)
323    }
324
325    /// Places `slot` at its first empty probe position. The table is guaranteed to
326    /// have room because [`reserve_one`](Interner::reserve_one) ran first.
327    fn insert_slot(&mut self, slot: Slot) {
328        let mut idx = (slot.hash as usize) & self.mask;
329        while !self.table[idx].is_empty() {
330            idx = (idx + 1) & self.mask;
331        }
332        self.table[idx] = slot;
333    }
334
335    /// Ensures the dedup index has room for one more entry under a 0.75 load
336    /// factor, allocating or doubling the table as needed.
337    fn reserve_one(&mut self) {
338        let occupied_after = self.spans.len() + 1;
339        if self.table.is_empty() {
340            self.resize_table(INITIAL_CAPACITY);
341        } else if occupied_after * 4 > self.table.len() * 3 {
342            self.resize_table(self.table.len() * 2);
343        }
344    }
345
346    /// Reallocates the dedup index to `new_cap` slots (a power of two) and
347    /// re-inserts every existing symbol. The backing buffer and span table are
348    /// untouched, so no symbol changes identity.
349    fn resize_table(&mut self, new_cap: usize) {
350        let mut table = Vec::new();
351        table.resize(new_cap, Slot::EMPTY);
352        let mask = new_cap - 1;
353
354        for (i, span) in self.spans.iter().enumerate() {
355            let s = &self.buf[span.start..span.start + span.len];
356            let hash = hash_bytes(s.as_bytes());
357            let id = id_for(i + 1);
358            let mut idx = (hash as usize) & mask;
359            while !table[idx].is_empty() {
360                idx = (idx + 1) & mask;
361            }
362            table[idx] = Slot {
363                hash: hash as u32,
364                id,
365            };
366        }
367
368        self.table = table;
369        self.mask = mask;
370    }
371
372    /// Returns the string for a 1-based symbol id. Only called with ids the
373    /// interner issued, so the span always exists.
374    #[inline]
375    fn span_str(&self, id: u32) -> &str {
376        let span = self.spans[id as usize - 1];
377        &self.buf[span.start..span.start + span.len]
378    }
379}
380
381impl Default for Interner {
382    #[inline]
383    fn default() -> Self {
384        Self::new()
385    }
386}
387
388impl fmt::Debug for Interner {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        f.debug_struct("Interner")
391            .field("strings", &self.spans.len())
392            .field("bytes", &self.buf.len())
393            .finish_non_exhaustive()
394    }
395}
396
397/// Converts a span-table length into a 1-based symbol id.
398///
399/// `len` is bounded by available memory — each interned string costs a span, a
400/// table slot, and at least one byte — so it stays within `u32` long before the
401/// cast could lose information. The saturating fallback keeps the conversion free
402/// of `unwrap`/`expect` and panics, and is unreachable for any in-memory input.
403#[inline]
404fn id_for(len: usize) -> u32 {
405    u32::try_from(len).unwrap_or(u32::MAX)
406}
407
408/// Rounds a desired distinct-string count up to a power-of-two table capacity that
409/// holds it under a 0.75 load factor, never below [`INITIAL_CAPACITY`].
410#[inline]
411fn table_capacity_for(strings: usize) -> usize {
412    let target = strings.saturating_mul(4) / 3 + 1;
413    target.max(INITIAL_CAPACITY).next_power_of_two()
414}
415
416/// Hashes `bytes` with an FxHash-style multiply-rotate over 64-bit words.
417///
418/// The string length seeds the state so that strings differing only in trailing
419/// content within a word boundary (for example `"ab"` versus `"ab\0"`) do not
420/// collide on the fast fingerprint. This is a non-cryptographic hash chosen for
421/// throughput on short identifiers; correctness never depends on it, since the
422/// dedup index always confirms a candidate with a full byte comparison.
423#[inline]
424fn hash_bytes(bytes: &[u8]) -> u64 {
425    const K: u64 = 0x517c_c1b7_2722_0a95;
426
427    let mut hash = bytes.len() as u64;
428    let mut chunks = bytes.chunks_exact(8);
429    for chunk in chunks.by_ref() {
430        // `chunks_exact(8)` always yields eight bytes, so the conversion holds;
431        // the fallback is dead and only keeps this free of `unwrap`.
432        let word = u64::from_le_bytes(<[u8; 8]>::try_from(chunk).unwrap_or([0; 8]));
433        hash = (hash.rotate_left(5) ^ word).wrapping_mul(K);
434    }
435
436    let remainder = chunks.remainder();
437    if !remainder.is_empty() {
438        let mut tail = [0u8; 8];
439        tail[..remainder.len()].copy_from_slice(remainder);
440        let word = u64::from_le_bytes(tail);
441        hash = (hash.rotate_left(5) ^ word).wrapping_mul(K);
442    }
443
444    hash
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    #[test]
452    fn test_intern_same_string_returns_same_symbol() {
453        let mut interner = Interner::new();
454        let a = interner.intern("name");
455        let b = interner.intern("name");
456        assert_eq!(a, b);
457        assert_eq!(interner.len(), 1);
458    }
459
460    #[test]
461    fn test_intern_distinct_strings_return_distinct_symbols() {
462        let mut interner = Interner::new();
463        let a = interner.intern("one");
464        let b = interner.intern("two");
465        assert_ne!(a, b);
466        assert_eq!(interner.len(), 2);
467    }
468
469    #[test]
470    fn test_resolve_roundtrips() {
471        let mut interner = Interner::new();
472        for s in ["", "a", "alpha", "a longer identifier with spaces"] {
473            let sym = interner.intern(s);
474            assert_eq!(interner.resolve(sym), Some(s));
475        }
476    }
477
478    #[test]
479    fn test_resolve_out_of_range_symbol_is_none() {
480        let mut issuer = Interner::new();
481        let _ = issuer.intern("a");
482        let high = issuer.intern("b");
483
484        let empty = Interner::new();
485        assert_eq!(empty.resolve(high), None);
486    }
487
488    #[test]
489    fn test_get_does_not_intern() {
490        let mut interner = Interner::new();
491        assert_eq!(interner.get("absent"), None);
492        assert_eq!(interner.len(), 0);
493        let sym = interner.intern("absent");
494        assert_eq!(interner.get("absent"), Some(sym));
495    }
496
497    #[test]
498    fn test_ids_are_sequential_from_one() {
499        let mut interner = Interner::new();
500        assert_eq!(interner.intern("a").as_u32(), 1);
501        assert_eq!(interner.intern("b").as_u32(), 2);
502        assert_eq!(interner.intern("a").as_u32(), 1);
503        assert_eq!(interner.intern("c").as_u32(), 3);
504    }
505
506    #[test]
507    fn test_growth_preserves_earlier_symbols() {
508        let mut interner = Interner::new();
509        let mut remembered = alloc::vec::Vec::new();
510        // Enough distinct strings to force several table resizes and buffer
511        // reallocations.
512        for i in 0..10_000 {
513            let s = alloc::format!("symbol_{i}");
514            remembered.push((interner.intern(&s), s));
515        }
516        for (sym, s) in &remembered {
517            assert_eq!(interner.resolve(*sym), Some(s.as_str()));
518        }
519    }
520
521    #[test]
522    fn test_empty_string_is_interned() {
523        let mut interner = Interner::new();
524        let empty = interner.intern("");
525        assert_eq!(interner.resolve(empty), Some(""));
526        assert_eq!(interner.intern(""), empty);
527    }
528
529    #[test]
530    fn test_unicode_roundtrips() {
531        let mut interner = Interner::new();
532        for s in ["café", "naïve", "日本語", "emoji 🦀", "Ωμέγα"] {
533            let sym = interner.intern(s);
534            assert_eq!(interner.resolve(sym), Some(s));
535        }
536    }
537
538    #[test]
539    fn test_with_capacity_behaves_like_new() {
540        let mut interner = Interner::with_capacity(64);
541        let sym = interner.intern("preallocated");
542        assert_eq!(interner.resolve(sym), Some("preallocated"));
543        assert_eq!(interner.len(), 1);
544    }
545
546    #[test]
547    fn test_strings_differing_only_in_trailing_byte_are_distinct() {
548        let mut interner = Interner::new();
549        let a = interner.intern("ab");
550        let b = interner.intern("ab\0");
551        assert_ne!(a, b);
552        assert_eq!(interner.resolve(a), Some("ab"));
553        assert_eq!(interner.resolve(b), Some("ab\0"));
554    }
555
556    #[test]
557    fn test_default_is_empty() {
558        let interner = Interner::default();
559        assert!(interner.is_empty());
560    }
561
562    #[test]
563    fn test_table_capacity_for_is_power_of_two_and_fits() {
564        for n in [0usize, 1, 12, 13, 100, 1000] {
565            let cap = table_capacity_for(n);
566            assert!(cap.is_power_of_two());
567            assert!(cap >= INITIAL_CAPACITY);
568            assert!(cap * 3 >= n.saturating_mul(4));
569        }
570    }
571}