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}