Skip to main content

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;