Installation
[]
= "1"
Quick start
use Interner;
let mut interner = new;
// Intern returns a small Copy handle; the same string always returns the same one.
let while_kw = interner.intern;
let for_kw = interner.intern;
assert_eq!;
// Name comparisons are now integer comparisons, not byte walks.
assert_ne!;
// Resolve borrows the original bytes straight out of the store.
assert_eq!;
Folding a token stream down to symbols, so later passes compare integers and a
name in an AST node costs four bytes instead of an owned String:
use ;
let mut interner = new;
let source = ;
let tokens: = source.iter.map.collect;
// The three `x` occurrences collapsed to one symbol.
assert_eq!;
assert_eq!; // let, x, =, +
Features
- Four-byte handle. A
Symbolis aNonZeroU32newtype —Copy, integer equality / ordering / hashing, andOption<Symbol>is four bytes too. - Bytes stored once. Interned strings are appended end to end in a single contiguous buffer; the dedup index stores symbol ids, not a second copy.
- Allocation-free hits. Interning a string already seen is a hash lookup with no allocation and no copy.
- Growth-stable symbols. A symbol keeps resolving to the same string for the interner's whole lifetime, including after the backing store reallocates.
- Thread-safe variant.
ConcurrentInternerlets many threads intern into one shared symbol space; the warm read path runs concurrently and racing threads never mint a duplicate symbol. Both interners share theLookupread trait. - Fallible path.
try_internreturns a typedInternErrorat the symbol-space bound instead of panicking, for callers that must account for it explicitly. no_std+ optional serde. Relies only onalloc; the defaultstdfeature is additive. Behind theserdefeature,Symbolserializes transparently as its integer id. No runtime dependencies beyond optionalserde.#![forbid(unsafe_code)]. The contiguous store is implemented without anyunsafe.
Performance
Resolution is a side-table index plus a slice — measured at roughly 1.1 ns per
resolve (Criterion mean, Windows x86_64, Rust stable, on the development
machine). Interning an already-seen string is allocation-free: a hash over the
bytes, an open-addressing probe, and one byte comparison to confirm the hit. The
numbers below are a v0.x baseline, tracked over time rather than advertised as
final:
| Operation | Mean |
|---|---|
resolve (id → &str) |
~1.1 ns |
intern (repeat hit, no allocation) |
~0.23 µs |
intern (new string, amortised growth) |
~0.62 µs |
Run them yourself with cargo bench. The warm read path scales across threads:
at 8 threads the ConcurrentInterner sustains roughly 4× the single-thread intern
throughput, since hits are served under a shared read lock.
Concurrent interning
use Arc;
use thread;
use ConcurrentInterner;
let interner = new;
let handles: =
.map
.collect;
let symbols: = handles.into_iter.map.collect;
// Every thread agrees on one symbol; "shared" was interned exactly once.
assert!;
assert_eq!;
API overview
For the complete reference with examples, see docs/API.md.
Symbol— four-byteCopyhandle:as_u32,from_u32.Interner— single-threaded interner:intern,try_intern,get,resolve,resolve_with,len,with_capacity.ConcurrentInterner— thread-safe interner sharing one symbol space (requires thestdfeature).Lookup— read-side trait both interners implement.InternError— the typed exhaustion error.
Status
v1.0.0 — stable. The public surface — the core interner, the symbol, the
thread-safe ConcurrentInterner, the fallible try_intern/InternError
contract, and optional serde for Symbol — is frozen under SemVer until 2.0. No
breaking change will be made without a major bump; see Stability.
Contributing
See dev/DIRECTIVES.md for engineering standards and the definition of done. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.