Skip to main content

rucc_base/
intern.rs

1//! String interning.
2//!
3//! Identifiers are compared constantly: on every macro lookup, every scope lookup, every
4//! typedef disambiguation. Interning turns those comparisons into an integer compare and
5//! turns the storage into one arena instead of a `String` per occurrence. The lexer interns
6//! during the scan rather than after it, per `spec/06-lexer-and-parser.md`, so an identifier
7//! is never materialised as a `String` at all.
8//!
9//! # Determinism
10//!
11//! [`Symbol`] ordering is allocation order, which is the order the source was read in. That
12//! is deterministic for a given input, and it is the reason the compiler can sort by symbol
13//! anywhere it needs a stable order without reaching for the string. Hashing a `Symbol` must
14//! never leak into output ordering, because hash order is not stable across runs, and
15//! `spec/02-the-goal.md` makes byte-identical output a requirement rather than a nicety.
16
17use std::collections::HashMap;
18use std::fmt;
19
20use crate::index::Idx;
21
22/// Marker for the symbol table, so that `Idx<SymbolTable>` cannot be confused with any
23/// other index.
24#[derive(Debug)]
25pub struct SymbolTable;
26
27/// An interned string.
28///
29/// Four bytes, `Copy`, and equal exactly when the strings are equal. Resolving one back to
30/// text needs the [`Interner`] it came from, which is deliberate: it makes accidentally
31/// printing an identifier in a hot path visible at the call site.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct Symbol(Idx<SymbolTable>);
34
35impl Symbol {
36    /// The underlying index, for packing a symbol into a bitfield.
37    #[inline]
38    pub const fn raw(self) -> u32 {
39        self.0.raw()
40    }
41
42    /// The symbol a [`Symbol::raw`] came from, which is the other half of packing one away.
43    ///
44    /// # Panics
45    ///
46    /// Panics if `raw` is not an index this interner could have handed out, which catches a
47    /// field holding something other than a symbol rather than resolving to the wrong string.
48    #[inline]
49    #[must_use]
50    pub const fn from_raw(raw: u32) -> Symbol {
51        Symbol(Idx::new(raw))
52    }
53}
54
55/// An append-only set of strings, each mapped to a [`Symbol`].
56///
57/// Strings are never removed, which is what makes a `Symbol` valid for the lifetime of the
58/// compilation and what lets the storage be a plain growing buffer.
59#[derive(Default)]
60pub struct Interner {
61    /// Every interned string, concatenated. One allocation that doubles, rather than one
62    /// allocation per identifier.
63    buf: String,
64    /// Where each symbol starts and ends in `buf`.
65    spans: Vec<(u32, u32)>,
66    /// Lookup from text to symbol. The key is a span into `buf` rather than an owned
67    /// `String`, which is why the map is keyed by the string and rebuilt through `resolve`.
68    map: HashMap<Box<str>, Symbol>,
69}
70
71impl Interner {
72    /// An empty interner.
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// An interner with room for `cap` strings, to avoid regrowing on a large header set.
78    pub fn with_capacity(cap: usize) -> Self {
79        Self {
80            buf: String::with_capacity(cap * 8),
81            spans: Vec::with_capacity(cap),
82            map: HashMap::with_capacity(cap),
83        }
84    }
85
86    /// Interns `s`, returning the existing symbol if it has been seen.
87    ///
88    /// # Panics
89    ///
90    /// Panics if more than `Idx::MAX` distinct strings are interned.
91    pub fn intern(&mut self, s: &str) -> Symbol {
92        if let Some(&sym) = self.map.get(s) {
93            return sym;
94        }
95        let start = u32::try_from(self.buf.len()).expect("interner buffer overflow");
96        self.buf.push_str(s);
97        let end = u32::try_from(self.buf.len()).expect("interner buffer overflow");
98        let sym = Symbol(Idx::from_usize(self.spans.len()));
99        self.spans.push((start, end));
100        self.map.insert(s.into(), sym);
101        sym
102    }
103
104    /// The text behind a symbol.
105    ///
106    /// # Panics
107    ///
108    /// Panics if the symbol came from a different interner. There is one interner per
109    /// compilation, so this is a bug rather than a condition to handle.
110    pub fn resolve(&self, sym: Symbol) -> &str {
111        let (start, end) = self.spans[sym.0.index()];
112        &self.buf[start as usize..end as usize]
113    }
114
115    /// How many distinct strings have been interned.
116    pub fn len(&self) -> usize {
117        self.spans.len()
118    }
119
120    /// Whether anything has been interned.
121    pub fn is_empty(&self) -> bool {
122        self.spans.is_empty()
123    }
124
125    /// Total bytes of interned text, which is the number worth watching on a large build.
126    pub fn bytes(&self) -> usize {
127        self.buf.len()
128    }
129}
130
131impl fmt::Debug for Interner {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        // Dumping every identifier in a translation unit is never what anyone wanted from a
134        // `{:?}` on the session, so this reports the shape instead.
135        f.debug_struct("Interner")
136            .field("symbols", &self.spans.len())
137            .field("bytes", &self.buf.len())
138            .finish()
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn the_same_string_gets_the_same_symbol() {
148        let mut i = Interner::new();
149        let a = i.intern("static_assert");
150        let b = i.intern("static_assert");
151        assert_eq!(a, b);
152        assert_eq!(i.len(), 1);
153    }
154
155    #[test]
156    fn different_strings_get_different_symbols() {
157        let mut i = Interner::new();
158        assert_ne!(i.intern("int"), i.intern("long"));
159        assert_eq!(i.len(), 2);
160    }
161
162    #[test]
163    fn resolves_back_to_the_text() {
164        let mut i = Interner::new();
165        let s = i.intern("__builtin_constant_p");
166        assert_eq!(i.resolve(s), "__builtin_constant_p");
167    }
168
169    #[test]
170    fn symbols_are_numbered_in_allocation_order() {
171        let mut i = Interner::new();
172        let first = i.intern("a");
173        let second = i.intern("b");
174        assert!(first < second, "symbol order must be allocation order, not hash order");
175    }
176
177    #[test]
178    fn the_empty_string_is_internable() {
179        let mut i = Interner::new();
180        let s = i.intern("");
181        assert_eq!(i.resolve(s), "");
182        assert_eq!(i.bytes(), 0);
183    }
184
185    #[test]
186    fn a_symbol_is_four_bytes() {
187        assert_eq!(size_of::<Symbol>(), 4);
188        assert_eq!(size_of::<Option<Symbol>>(), 4);
189    }
190}