molframe_core/symbol/interner.rs
1//! The identifier dictionary itself.
2//!
3//! Local strings live in one arena rather than in one allocation each. A file
4//! with a hundred thousand atoms has a few hundred distinct identifiers, so
5//! interning them costs a few hundred bytes copied once, not an allocation per
6//! atom.
7
8use super::canonical;
9use hashbrown::HashTable;
10use hashbrown::hash_table::Entry;
11use std::convert::TryFrom;
12use std::fmt;
13use std::hash::{BuildHasher, RandomState};
14use std::sync::Arc;
15
16/// An interned identifier.
17///
18/// Identifiers below the canonical count mean the same string in every
19/// structure. Above it they are meaningful only against the interner that
20/// issued them.
21///
22/// # Examples
23///
24/// ```
25/// use molframe_core::{Interner, SymbolId};
26///
27/// let mut interner = Interner::new();
28/// let alanine = interner.intern("ALA")?;
29/// assert!(alanine.is_canonical());
30///
31/// let ligand = interner.intern("MY_LIGAND")?;
32/// assert!(!ligand.is_canonical());
33/// assert_eq!(interner.resolve(ligand), Some("MY_LIGAND"));
34/// # Ok::<(), molframe_core::DictionaryFull>(())
35/// ```
36#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
37#[repr(transparent)]
38pub struct SymbolId(u32);
39
40impl SymbolId {
41 /// Largest identifier emitted by an [`Interner`].
42 ///
43 /// The final `u32` value is reserved because [`crate::AltId`] encodes a
44 /// labelled symbol by adding one while retaining zero for the blank label.
45 const MAX_VALID_RAW: u32 = u32::MAX - 1;
46
47 /// Returns true when this identifier has the same meaning in every
48 /// structure, so comparing it across structures is sound.
49 ///
50 /// Runs in `O(1)` time and allocates no memory.
51 #[must_use]
52 #[inline]
53 pub fn is_canonical(self) -> bool {
54 canonical::text_of(self.0).is_some()
55 }
56
57 /// The raw identifier, for storage in a column.
58 ///
59 /// Runs in `O(1)` time and allocates no memory.
60 #[must_use]
61 #[inline]
62 pub const fn get(self) -> u32 {
63 self.0
64 }
65
66 /// Rebuilds an identifier from a stored value.
67 ///
68 /// `u32::MAX` is reserved and is not emitted by [`Interner`]. Callers
69 /// should pass values previously produced by this crate.
70 ///
71 /// Runs in `O(1)` time and allocates no memory.
72 #[must_use]
73 #[inline]
74 pub const fn from_raw(raw: u32) -> Self {
75 Self(raw)
76 }
77}
78
79impl fmt::Debug for SymbolId {
80 /// Formats canonical identifiers by text and local identifiers by raw value.
81 ///
82 /// Runs in `O(L)` formatting time for canonical text of length `L` and
83 /// allocates no intermediate heap storage.
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 match canonical::text_of(self.0) {
86 Some(text) => write!(f, "SymbolId({text:?})"),
87 None => write!(f, "SymbolId(local {})", self.0),
88 }
89 }
90}
91
92/// The identifier dictionary grew past the limit that guards against a hostile
93/// input inventing identifiers without bound.
94#[derive(Clone, Copy, PartialEq, Eq, Debug, thiserror::Error)]
95#[error("identifier dictionary is full")]
96pub struct DictionaryFull;
97
98/// Where a local string sits in the arena.
99///
100/// Storing the exclusive end offset avoids repeated addition during resolution
101/// without increasing the eight-byte extent representation.
102#[derive(Clone, Copy)]
103struct Extent {
104 start: u32,
105 end: u32,
106}
107
108/// A type-safe index into the local extent array.
109///
110/// The transparent representation keeps each hash-table entry at four bytes
111/// while preventing local ordinals from being confused with global IDs.
112#[derive(Clone, Copy)]
113#[repr(transparent)]
114struct LocalOrdinal(u32);
115
116/// Copy-on-write storage for the local dictionary.
117#[derive(Clone)]
118struct Storage {
119 arena: String,
120 extents: Vec<Extent>,
121 table: HashTable<LocalOrdinal>,
122}
123
124/// A structure's identifier dictionary.
125///
126/// Interning is idempotent: the same string always yields the same identifier
127/// within one interner, and identifiers are issued in first-seen order, so two
128/// runs over the same file produce the same numbering.
129#[derive(Clone)]
130pub struct Interner {
131 storage: Arc<Storage>,
132 hasher: RandomState,
133 limit: u32,
134}
135
136impl Interner {
137 // Retains allocations so the next batch can reuse the dictionary.
138 pub(crate) fn clear(&mut self) {
139 let storage = Arc::make_mut(&mut self.storage);
140 storage.arena.clear();
141 storage.extents.clear();
142 storage.table.clear();
143 }
144
145 /// Bytes retained by local arena, extent and lookup-table capacities.
146 #[must_use]
147 pub fn retained_bytes(&self) -> usize {
148 let storage = &self.storage;
149 let extents = storage
150 .extents
151 .capacity()
152 .saturating_mul(std::mem::size_of::<Extent>());
153 let table = storage
154 .table
155 .capacity()
156 .saturating_mul(std::mem::size_of::<LocalOrdinal>());
157 storage
158 .arena
159 .capacity()
160 .saturating_add(extents)
161 .saturating_add(table)
162 }
163 /// The largest number of local identifiers one structure may issue.
164 ///
165 /// This is a guard, not a design target. Reaching it means input-generated identifiers
166 /// rather than naming things.
167 pub const DEFAULT_LIMIT: u32 = 1_000_000;
168 /// Creates an empty dictionary over the canonical one.
169 ///
170 /// Runs in `O(1)` time and creates no local-string allocation.
171 #[must_use]
172 pub fn new() -> Self {
173 Self {
174 storage: Arc::new(Storage {
175 arena: String::new(),
176 extents: Vec::new(),
177 table: HashTable::new(),
178 }),
179 hasher: RandomState::new(),
180 limit: Self::DEFAULT_LIMIT,
181 }
182 }
183
184 /// Creates a dictionary with room reserved for `identifiers` local strings.
185 ///
186 /// The extent vector and hash table reserve capacity up front. The arena is
187 /// left empty because the total byte length cannot be inferred from the
188 /// number of identifiers.
189 ///
190 /// Uses `O(identifiers)` reserved memory.
191 #[must_use]
192 pub fn with_capacity(identifiers: usize) -> Self {
193 let mut interner = Self::new();
194 let storage = Arc::make_mut(&mut interner.storage);
195 storage.extents.reserve(identifiers);
196 storage.table.reserve(identifiers, |_| 0u64);
197 interner
198 }
199
200 /// Sets the limit on local identifiers.
201 ///
202 /// Runs in `O(1)` time and allocates no memory.
203 #[must_use]
204 pub const fn with_limit(mut self, limit: u32) -> Self {
205 self.limit = limit;
206 self
207 }
208
209 /// Returns the identifier for `text`, interning it if it is new.
210 ///
211 /// Existing strings require no allocation. A new local string is copied
212 /// exactly once into the arena and adds one extent and one table entry.
213 ///
214 /// Expected running time is `O(L)`, where `L` is `text.len()`. Insertion is
215 /// amortized over hash-table and arena growth.
216 ///
217 /// # Errors
218 ///
219 /// Returns [`DictionaryFull`] when the local dictionary has reached its
220 /// limit and `text` is not already in it.
221 pub fn intern(&mut self, text: &str) -> Result<SymbolId, DictionaryFull> {
222 if let Some(ordinal) = canonical::ordinal_of(text) {
223 return Ok(SymbolId(ordinal));
224 }
225
226 // Resolve existing local symbols before copy-on-write detaches a clone.
227 if Arc::strong_count(&self.storage) > 1
228 && let Some(id) = self.get(text)
229 {
230 return Ok(id);
231 }
232
233 let Self {
234 storage,
235 hasher,
236 limit,
237 } = self;
238 let Storage {
239 arena,
240 extents,
241 table,
242 } = Arc::make_mut(storage);
243 let hash = hasher.hash_one(text);
244
245 let pending = pending_local(extents.len(), *limit, arena.len(), text.len());
246
247 let (local, id, extent) = match pending {
248 Ok(pending) => pending,
249 Err(error) => {
250 return match find_local(table, hash, arena, extents, text) {
251 Some(local) => local_to_id(local).ok_or(error),
252 None => Err(error),
253 };
254 }
255 };
256
257 match table.entry(
258 hash,
259 |&other| extent_text(arena, extents, other) == Some(text),
260 |&other| {
261 // Rehashing during growth reads the arena, which the closure may
262 // borrow because only the table is being mutated here. An ordinal
263 // with no extent cannot occur; hashing the empty string keeps the
264 // table consistent rather than aborting if it somehow did.
265 match extent_text(arena, extents, other) {
266 Some(existing) => hasher.hash_one(existing),
267 None => hasher.hash_one(""),
268 }
269 },
270 ) {
271 Entry::Occupied(occupied) => local_to_id(*occupied.get()).ok_or(DictionaryFull),
272 Entry::Vacant(vacant) => {
273 arena.push_str(text);
274 extents.push(extent);
275 let _ = vacant.insert(local);
276 Ok(id)
277 }
278 }
279 }
280
281 /// Returns the identifier for `text` if it is already known, without
282 /// interning it.
283 ///
284 /// This is what a selection uses: a name that no structure contains should
285 /// resolve to nothing rather than growing the dictionary.
286 ///
287 /// Expected running time is `O(L)`, where `L` is `text.len()`. The operation
288 /// performs no allocation and does not mutate the dictionary.
289 #[must_use]
290 pub fn get(&self, text: &str) -> Option<SymbolId> {
291 if let Some(ordinal) = canonical::ordinal_of(text) {
292 return Some(SymbolId(ordinal));
293 }
294
295 let hash = self.hasher.hash_one(text);
296 find_local(
297 &self.storage.table,
298 hash,
299 &self.storage.arena,
300 &self.storage.extents,
301 text,
302 )
303 .and_then(local_to_id)
304 }
305
306 /// Returns the string an identifier names.
307 ///
308 /// Runs in `O(1)` time and allocates no memory. The returned slice borrows
309 /// either static canonical storage or this interner's arena.
310 #[must_use]
311 pub fn resolve(&self, id: SymbolId) -> Option<&str> {
312 if let Some(text) = canonical::text_of(id.0) {
313 return Some(text);
314 }
315
316 let local = id_to_local(id)?;
317 extent_text(&self.storage.arena, &self.storage.extents, local)
318 }
319
320 /// The number of local identifiers issued.
321 ///
322 /// Runs in `O(1)` time and allocates no memory.
323 #[must_use]
324 #[inline]
325 pub fn len(&self) -> usize {
326 self.storage.extents.len()
327 }
328
329 /// Returns true when no local identifier has been issued.
330 ///
331 /// Runs in `O(1)` time and allocates no memory.
332 #[must_use]
333 #[inline]
334 pub fn is_empty(&self) -> bool {
335 self.storage.extents.is_empty()
336 }
337
338 /// The bytes the local strings occupy.
339 ///
340 /// Runs in `O(1)` time and allocates no memory.
341 #[must_use]
342 #[inline]
343 pub fn arena_len(&self) -> usize {
344 self.storage.arena.len()
345 }
346
347 /// Walks canonical and structure-local identifiers in stable id order.
348 ///
349 /// This is the dictionary-side half of compiling glob predicates: pattern
350 /// work is proportional to distinct identifiers and the atom loop remains
351 /// integer membership. Iteration allocates nothing.
352 pub fn iter(&self) -> impl Iterator<Item = (SymbolId, &str)> {
353 let canonical = canonical::CANONICAL
354 .iter()
355 .enumerate()
356 .filter_map(|(position, text)| {
357 u32::try_from(position)
358 .ok()
359 .map(|position| (SymbolId(position), *text))
360 });
361 let local = self
362 .storage
363 .extents
364 .iter()
365 .enumerate()
366 .filter_map(|(position, _extent)| {
367 let local = u32::try_from(position).ok().map(LocalOrdinal)?;
368 let symbol = local_to_id(local)?;
369 extent_text(&self.storage.arena, &self.storage.extents, local)
370 .map(|text| (symbol, text))
371 });
372 canonical.chain(local)
373 }
374}
375
376impl Default for Interner {
377 /// Creates an empty interner with [`Interner::DEFAULT_LIMIT`].
378 ///
379 /// Runs in `O(1)` time and creates no local-string allocation.
380 fn default() -> Self {
381 Self::new()
382 }
383}
384
385impl fmt::Debug for Interner {
386 /// Formats the interner's local-count and arena-size metadata.
387 ///
388 /// Runs in `O(1)` time and does not traverse or copy interned strings.
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 f.debug_struct("Interner")
391 .field("local", &self.storage.extents.len())
392 .field("arena_bytes", &self.storage.arena.len())
393 .finish_non_exhaustive()
394 }
395}
396
397/// Prepares the metadata required to insert one new local string.
398///
399/// Returns the next local ordinal, its global [`SymbolId`], and its arena
400/// extent. No state is mutated, so failure cannot leave a partial insertion.
401///
402/// Runs in `O(1)` time and allocates no memory.
403#[inline]
404fn pending_local(
405 local_count: usize,
406 limit: u32,
407 arena_len: usize,
408 text_len: usize,
409) -> Result<(LocalOrdinal, SymbolId, Extent), DictionaryFull> {
410 let local = match u32::try_from(local_count) {
411 Ok(local) if local < limit => LocalOrdinal(local),
412 Ok(_) | Err(_) => return Err(DictionaryFull),
413 };
414
415 let id = local_to_id(local).ok_or(DictionaryFull)?;
416 let extent = extent_for_append(arena_len, text_len).ok_or(DictionaryFull)?;
417
418 Ok((local, id, extent))
419}
420
421/// Computes the arena extent for appending a string.
422///
423/// Returns `None` if either offset cannot be represented by the compact `u32`
424/// extent format or if the byte-length addition overflows.
425///
426/// Runs in `O(1)` time and allocates no memory.
427#[inline]
428fn extent_for_append(arena_len: usize, text_len: usize) -> Option<Extent> {
429 let end = arena_len.checked_add(text_len)?;
430
431 Some(Extent {
432 start: u32::try_from(arena_len).ok()?,
433 end: u32::try_from(end).ok()?,
434 })
435}
436
437/// Finds a local ordinal in expected `O(text.len())` time without allocation.
438#[inline]
439fn find_local(
440 table: &HashTable<LocalOrdinal>,
441 hash: u64,
442 arena: &str,
443 extents: &[Extent],
444 text: &str,
445) -> Option<LocalOrdinal> {
446 table
447 .find(hash, |&local| {
448 extent_text(arena, extents, local) == Some(text)
449 })
450 .copied()
451}
452
453/// Converts a local ordinal to an identifier, rejecting reserved overflow.
454#[inline]
455fn local_to_id(local: LocalOrdinal) -> Option<SymbolId> {
456 let canonical_count = u32::try_from(canonical::CANONICAL.len()).ok()?;
457 let raw = canonical_count.checked_add(local.0)?;
458
459 if raw > SymbolId::MAX_VALID_RAW {
460 None
461 } else {
462 Some(SymbolId(raw))
463 }
464}
465
466/// Converts a non-canonical identifier into its local ordinal in `O(1)`.
467#[inline]
468fn id_to_local(id: SymbolId) -> Option<LocalOrdinal> {
469 if id.0 > SymbolId::MAX_VALID_RAW {
470 return None;
471 }
472
473 let canonical_count = u32::try_from(canonical::CANONICAL.len()).ok()?;
474 id.0.checked_sub(canonical_count).map(LocalOrdinal)
475}
476
477/// Returns an arena slice, declining invalid ordinals or malformed extents.
478#[inline]
479fn extent_text<'a>(arena: &'a str, extents: &[Extent], local: LocalOrdinal) -> Option<&'a str> {
480 let local_index = usize::try_from(local.0).ok()?;
481 let extent = extents.get(local_index)?;
482 let start = usize::try_from(extent.start).ok()?;
483 let end = usize::try_from(extent.end).ok()?;
484
485 arena.get(start..end)
486}
487
488#[cfg(test)]
489#[path = "interner_tests.rs"]
490mod tests;