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