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
43/// An append-only set of strings, each mapped to a [`Symbol`].
44///
45/// Strings are never removed, which is what makes a `Symbol` valid for the lifetime of the
46/// compilation and what lets the storage be a plain growing buffer.
47#[derive(Default)]
48pub struct Interner {
49    /// Every interned string, concatenated. One allocation that doubles, rather than one
50    /// allocation per identifier.
51    buf: String,
52    /// Where each symbol starts and ends in `buf`.
53    spans: Vec<(u32, u32)>,
54    /// Lookup from text to symbol. The key is a span into `buf` rather than an owned
55    /// `String`, which is why the map is keyed by the string and rebuilt through `resolve`.
56    map: HashMap<Box<str>, Symbol>,
57}
58
59impl Interner {
60    /// An empty interner.
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// An interner with room for `cap` strings, to avoid regrowing on a large header set.
66    pub fn with_capacity(cap: usize) -> Self {
67        Self {
68            buf: String::with_capacity(cap * 8),
69            spans: Vec::with_capacity(cap),
70            map: HashMap::with_capacity(cap),
71        }
72    }
73
74    /// Interns `s`, returning the existing symbol if it has been seen.
75    ///
76    /// # Panics
77    ///
78    /// Panics if more than `Idx::MAX` distinct strings are interned.
79    pub fn intern(&mut self, s: &str) -> Symbol {
80        if let Some(&sym) = self.map.get(s) {
81            return sym;
82        }
83        let start = u32::try_from(self.buf.len()).expect("interner buffer overflow");
84        self.buf.push_str(s);
85        let end = u32::try_from(self.buf.len()).expect("interner buffer overflow");
86        let sym = Symbol(Idx::from_usize(self.spans.len()));
87        self.spans.push((start, end));
88        self.map.insert(s.into(), sym);
89        sym
90    }
91
92    /// The text behind a symbol.
93    ///
94    /// # Panics
95    ///
96    /// Panics if the symbol came from a different interner. There is one interner per
97    /// compilation, so this is a bug rather than a condition to handle.
98    pub fn resolve(&self, sym: Symbol) -> &str {
99        let (start, end) = self.spans[sym.0.index()];
100        &self.buf[start as usize..end as usize]
101    }
102
103    /// How many distinct strings have been interned.
104    pub fn len(&self) -> usize {
105        self.spans.len()
106    }
107
108    /// Whether anything has been interned.
109    pub fn is_empty(&self) -> bool {
110        self.spans.is_empty()
111    }
112
113    /// Total bytes of interned text, which is the number worth watching on a large build.
114    pub fn bytes(&self) -> usize {
115        self.buf.len()
116    }
117}
118
119impl fmt::Debug for Interner {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        // Dumping every identifier in a translation unit is never what anyone wanted from a
122        // `{:?}` on the session, so this reports the shape instead.
123        f.debug_struct("Interner")
124            .field("symbols", &self.spans.len())
125            .field("bytes", &self.buf.len())
126            .finish()
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn the_same_string_gets_the_same_symbol() {
136        let mut i = Interner::new();
137        let a = i.intern("static_assert");
138        let b = i.intern("static_assert");
139        assert_eq!(a, b);
140        assert_eq!(i.len(), 1);
141    }
142
143    #[test]
144    fn different_strings_get_different_symbols() {
145        let mut i = Interner::new();
146        assert_ne!(i.intern("int"), i.intern("long"));
147        assert_eq!(i.len(), 2);
148    }
149
150    #[test]
151    fn resolves_back_to_the_text() {
152        let mut i = Interner::new();
153        let s = i.intern("__builtin_constant_p");
154        assert_eq!(i.resolve(s), "__builtin_constant_p");
155    }
156
157    #[test]
158    fn symbols_are_numbered_in_allocation_order() {
159        let mut i = Interner::new();
160        let first = i.intern("a");
161        let second = i.intern("b");
162        assert!(first < second, "symbol order must be allocation order, not hash order");
163    }
164
165    #[test]
166    fn the_empty_string_is_internable() {
167        let mut i = Interner::new();
168        let s = i.intern("");
169        assert_eq!(i.resolve(s), "");
170        assert_eq!(i.bytes(), 0);
171    }
172
173    #[test]
174    fn a_symbol_is_four_bytes() {
175        assert_eq!(size_of::<Symbol>(), 4);
176        assert_eq!(size_of::<Option<Symbol>>(), 4);
177    }
178}