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}