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//! # Reserved names
10//!
11//! Every interner starts with the names in [`RESERVED`] already in it, in that order, which is
12//! what makes the constants in [`sym`] the symbols they are. The reason is that a pass past the
13//! lexer holds the interner through a shared reference and cannot add to it, and a pass that
14//! builds a type of its own still has to name it: the members of the target's `va_list` are
15//! named by the ABI and never by the source, so the names have to exist before anything is read.
16//!
17//! # Determinism
18//!
19//! [`Symbol`] ordering is allocation order, which is the order the source was read in. That
20//! is deterministic for a given input, and it is the reason the compiler can sort by symbol
21//! anywhere it needs a stable order without reaching for the string. Hashing a `Symbol` must
22//! never leak into output ordering, because hash order is not stable across runs, and
23//! `spec/02-the-goal.md` makes byte-identical output a requirement rather than a nicety.
24
25use std::collections::HashMap;
26use std::fmt;
27
28use crate::index::Idx;
29
30/// Marker for the symbol table, so that `Idx<SymbolTable>` cannot be confused with any
31/// other index.
32#[derive(Debug)]
33pub struct SymbolTable;
34
35/// An interned string.
36///
37/// Four bytes, `Copy`, and equal exactly when the strings are equal. Resolving one back to
38/// text needs the [`Interner`] it came from, which is deliberate: it makes accidentally
39/// printing an identifier in a hot path visible at the call site.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub struct Symbol(Idx<SymbolTable>);
42
43impl Symbol {
44    /// The underlying index, for packing a symbol into a bitfield.
45    #[inline]
46    pub const fn raw(self) -> u32 {
47        self.0.raw()
48    }
49
50    /// The symbol a [`Symbol::raw`] came from, which is the other half of packing one away.
51    ///
52    /// # Panics
53    ///
54    /// Panics if `raw` is not an index this interner could have handed out, which catches a
55    /// field holding something other than a symbol rather than resolving to the wrong string.
56    #[inline]
57    #[must_use]
58    pub const fn from_raw(raw: u32) -> Symbol {
59        Symbol(Idx::new(raw))
60    }
61}
62
63/// The names every interner is built with, in the order they are interned.
64///
65/// The list is short on purpose. A name belongs here when the compiler has to write it down and
66/// the source is not the place it comes from, which so far is the target's type for a variable
67/// argument list and nothing else.
68pub const RESERVED: &[&str] = &[
69    "__va_list_tag",
70    "gp_offset",
71    "fp_offset",
72    "overflow_arg_area",
73    "reg_save_area",
74    "__va_list",
75    "__stack",
76    "__gr_top",
77    "__vr_top",
78    "__gr_offs",
79    "__vr_offs",
80];
81
82/// The symbols for the names in [`RESERVED`].
83///
84/// Each constant is the position of its name in that list, so the two are one table written
85/// twice and a test here holds them together.
86pub mod sym {
87    use super::Symbol;
88
89    /// `__va_list_tag`, the tag of the record a SysV x86-64 `va_list` is an array of one of.
90    pub const VA_LIST_TAG: Symbol = Symbol::from_raw(0);
91    /// `gp_offset`, how far into the saved general registers the list has read.
92    pub const GP_OFFSET: Symbol = Symbol::from_raw(1);
93    /// `fp_offset`, the same for the saved floating point registers.
94    pub const FP_OFFSET: Symbol = Symbol::from_raw(2);
95    /// `overflow_arg_area`, the arguments that were passed on the stack.
96    pub const OVERFLOW_ARG_AREA: Symbol = Symbol::from_raw(3);
97    /// `reg_save_area`, where the callee spilled the argument registers.
98    pub const REG_SAVE_AREA: Symbol = Symbol::from_raw(4);
99    /// `__va_list`, the tag of the record an AAPCS64 `va_list` is.
100    pub const VA_LIST: Symbol = Symbol::from_raw(5);
101    /// `__stack`, the arguments that were passed on the stack.
102    pub const STACK: Symbol = Symbol::from_raw(6);
103    /// `__gr_top`, the end of the saved general registers.
104    pub const GR_TOP: Symbol = Symbol::from_raw(7);
105    /// `__vr_top`, the end of the saved vector registers.
106    pub const VR_TOP: Symbol = Symbol::from_raw(8);
107    /// `__gr_offs`, how far back from `__gr_top` the list has read, in bytes and negative.
108    pub const GR_OFFS: Symbol = Symbol::from_raw(9);
109    /// `__vr_offs`, the same for `__vr_top`.
110    pub const VR_OFFS: Symbol = Symbol::from_raw(10);
111}
112
113/// An append-only set of strings, each mapped to a [`Symbol`].
114///
115/// Strings are never removed, which is what makes a `Symbol` valid for the lifetime of the
116/// compilation and what lets the storage be a plain growing buffer.
117pub struct Interner {
118    /// Every interned string, concatenated. One allocation that doubles, rather than one
119    /// allocation per identifier.
120    buf: String,
121    /// Where each symbol starts and ends in `buf`.
122    spans: Vec<(u32, u32)>,
123    /// Lookup from text to symbol. The key is a span into `buf` rather than an owned
124    /// `String`, which is why the map is keyed by the string and rebuilt through `resolve`.
125    map: HashMap<Box<str>, Symbol>,
126}
127
128impl Default for Interner {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134impl Interner {
135    /// An interner holding the reserved names and nothing else.
136    pub fn new() -> Self {
137        Self::with_capacity(RESERVED.len())
138    }
139
140    /// An interner with room for `cap` strings, to avoid regrowing on a large header set.
141    pub fn with_capacity(cap: usize) -> Self {
142        let cap = cap.max(RESERVED.len());
143        let mut interner = Self {
144            buf: String::with_capacity(cap * 8),
145            spans: Vec::with_capacity(cap),
146            map: HashMap::with_capacity(cap),
147        };
148        for name in RESERVED {
149            interner.intern(name);
150        }
151        interner
152    }
153
154    /// Interns `s`, returning the existing symbol if it has been seen.
155    ///
156    /// # Panics
157    ///
158    /// Panics if more than `Idx::MAX` distinct strings are interned.
159    pub fn intern(&mut self, s: &str) -> Symbol {
160        if let Some(&sym) = self.map.get(s) {
161            return sym;
162        }
163        let start = u32::try_from(self.buf.len()).expect("interner buffer overflow");
164        self.buf.push_str(s);
165        let end = u32::try_from(self.buf.len()).expect("interner buffer overflow");
166        let sym = Symbol(Idx::from_usize(self.spans.len()));
167        self.spans.push((start, end));
168        self.map.insert(s.into(), sym);
169        sym
170    }
171
172    /// The text behind a symbol.
173    ///
174    /// # Panics
175    ///
176    /// Panics if the symbol came from a different interner. There is one interner per
177    /// compilation, so this is a bug rather than a condition to handle.
178    pub fn resolve(&self, sym: Symbol) -> &str {
179        let (start, end) = self.spans[sym.0.index()];
180        &self.buf[start as usize..end as usize]
181    }
182
183    /// How many distinct strings have been interned, the reserved names included.
184    pub fn len(&self) -> usize {
185        self.spans.len()
186    }
187
188    /// Whether anything but the reserved names has been interned.
189    pub fn is_empty(&self) -> bool {
190        self.spans.len() <= RESERVED.len()
191    }
192
193    /// Total bytes of interned text, which is the number worth watching on a large build.
194    pub fn bytes(&self) -> usize {
195        self.buf.len()
196    }
197}
198
199impl fmt::Debug for Interner {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        // Dumping every identifier in a translation unit is never what anyone wanted from a
202        // `{:?}` on the session, so this reports the shape instead.
203        f.debug_struct("Interner")
204            .field("symbols", &self.spans.len())
205            .field("bytes", &self.buf.len())
206            .finish()
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn the_same_string_gets_the_same_symbol() {
216        let mut i = Interner::new();
217        let before = i.len();
218        let a = i.intern("static_assert");
219        let b = i.intern("static_assert");
220        assert_eq!(a, b);
221        assert_eq!(i.len() - before, 1);
222    }
223
224    #[test]
225    fn different_strings_get_different_symbols() {
226        let mut i = Interner::new();
227        let before = i.len();
228        assert_ne!(i.intern("int"), i.intern("long"));
229        assert_eq!(i.len() - before, 2);
230    }
231
232    #[test]
233    fn the_reserved_names_are_there_before_anything_is_read() {
234        let i = Interner::new();
235        assert_eq!(i.len(), RESERVED.len());
236        assert!(i.is_empty(), "the reserved names do not count as something having been read");
237        assert_eq!(i.resolve(sym::VA_LIST_TAG), "__va_list_tag");
238        assert_eq!(i.resolve(sym::GP_OFFSET), "gp_offset");
239        assert_eq!(i.resolve(sym::FP_OFFSET), "fp_offset");
240        assert_eq!(i.resolve(sym::OVERFLOW_ARG_AREA), "overflow_arg_area");
241        assert_eq!(i.resolve(sym::REG_SAVE_AREA), "reg_save_area");
242        assert_eq!(i.resolve(sym::VA_LIST), "__va_list");
243        assert_eq!(i.resolve(sym::STACK), "__stack");
244        assert_eq!(i.resolve(sym::GR_TOP), "__gr_top");
245        assert_eq!(i.resolve(sym::VR_TOP), "__vr_top");
246        assert_eq!(i.resolve(sym::GR_OFFS), "__gr_offs");
247        assert_eq!(i.resolve(sym::VR_OFFS), "__vr_offs");
248    }
249
250    #[test]
251    fn a_reserved_name_written_in_the_source_is_the_symbol_it_already_had() {
252        let mut i = Interner::new();
253        let before = i.len();
254        assert_eq!(i.intern("__va_list_tag"), sym::VA_LIST_TAG);
255        assert_eq!(i.len(), before);
256    }
257
258    #[test]
259    fn every_interner_agrees_on_where_the_reserved_names_are() {
260        let small = Interner::new();
261        let large = Interner::with_capacity(4096);
262        for (at, name) in RESERVED.iter().enumerate() {
263            let sym = Symbol::from_raw(u32::try_from(at).expect("eleven names fit in a u32"));
264            assert_eq!(small.resolve(sym), *name);
265            assert_eq!(large.resolve(sym), *name);
266        }
267    }
268
269    #[test]
270    fn resolves_back_to_the_text() {
271        let mut i = Interner::new();
272        let s = i.intern("__builtin_constant_p");
273        assert_eq!(i.resolve(s), "__builtin_constant_p");
274    }
275
276    #[test]
277    fn symbols_are_numbered_in_allocation_order() {
278        let mut i = Interner::new();
279        let first = i.intern("a");
280        let second = i.intern("b");
281        assert!(first < second, "symbol order must be allocation order, not hash order");
282    }
283
284    #[test]
285    fn the_empty_string_is_internable() {
286        let mut i = Interner::new();
287        let before = i.bytes();
288        let s = i.intern("");
289        assert_eq!(i.resolve(s), "");
290        assert_eq!(i.bytes(), before);
291    }
292
293    #[test]
294    fn a_symbol_is_four_bytes() {
295        assert_eq!(size_of::<Symbol>(), 4);
296        assert_eq!(size_of::<Option<Symbol>>(), 4);
297    }
298}