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//!
25//! # Spellings that are not text
26//!
27//! A source file is UTF-8 and an identifier in it is text, but the body of a string literal is
28//! bytes and does not have to be text at all: `"\xff"` may be written as the byte itself, and
29//! the object it initialises is one byte long whatever that byte is. So [`Interner::intern_bytes`]
30//! takes a spelling that is not UTF-8 and [`Interner::resolve_bytes`] gives it back exactly,
31//! while [`Interner::resolve`] still hands back a `&str`, because almost everything that holds a
32//! symbol wants to print it. What it hands back for such a symbol is the lossy reading, with the
33//! bytes that are not characters replaced, which is right for a message and wrong for an object,
34//! and the object is what `resolve_bytes` is for.
35
36use std::fmt;
37
38use crate::hash::Map;
39use crate::index::Idx;
40
41/// Marker for the symbol table, so that `Idx<SymbolTable>` cannot be confused with any
42/// other index.
43#[derive(Debug)]
44pub struct SymbolTable;
45
46/// An interned string.
47///
48/// Four bytes, `Copy`, and equal exactly when the strings are equal. Resolving one back to
49/// text needs the [`Interner`] it came from, which is deliberate: it makes accidentally
50/// printing an identifier in a hot path visible at the call site.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
52pub struct Symbol(Idx<SymbolTable>);
53
54impl Symbol {
55    /// The underlying index, for packing a symbol into a bitfield.
56    #[inline]
57    pub const fn raw(self) -> u32 {
58        self.0.raw()
59    }
60
61    /// The symbol a [`Symbol::raw`] came from, which is the other half of packing one away.
62    ///
63    /// # Panics
64    ///
65    /// Panics if `raw` is not an index this interner could have handed out, which catches a
66    /// field holding something other than a symbol rather than resolving to the wrong string.
67    #[inline]
68    #[must_use]
69    pub const fn from_raw(raw: u32) -> Symbol {
70        Symbol(Idx::new(raw))
71    }
72}
73
74/// The names every interner is built with, in the order they are interned.
75///
76/// The list is short on purpose. A name belongs here when the compiler has to write it down and
77/// the source is not the place it comes from, which so far is the target's type for a variable
78/// argument list and nothing else.
79pub const RESERVED: &[&str] = &[
80    "__va_list_tag",
81    "gp_offset",
82    "fp_offset",
83    "overflow_arg_area",
84    "reg_save_area",
85    "__va_list",
86    "__stack",
87    "__gr_top",
88    "__vr_top",
89    "__gr_offs",
90    "__vr_offs",
91];
92
93/// The symbols for the names in [`RESERVED`].
94///
95/// Each constant is the position of its name in that list, so the two are one table written
96/// twice and a test here holds them together.
97pub mod sym {
98    use super::Symbol;
99
100    /// `__va_list_tag`, the tag of the record a SysV x86-64 `va_list` is an array of one of.
101    pub const VA_LIST_TAG: Symbol = Symbol::from_raw(0);
102    /// `gp_offset`, how far into the saved general registers the list has read.
103    pub const GP_OFFSET: Symbol = Symbol::from_raw(1);
104    /// `fp_offset`, the same for the saved floating point registers.
105    pub const FP_OFFSET: Symbol = Symbol::from_raw(2);
106    /// `overflow_arg_area`, the arguments that were passed on the stack.
107    pub const OVERFLOW_ARG_AREA: Symbol = Symbol::from_raw(3);
108    /// `reg_save_area`, where the callee spilled the argument registers.
109    pub const REG_SAVE_AREA: Symbol = Symbol::from_raw(4);
110    /// `__va_list`, the tag of the record an AAPCS64 `va_list` is.
111    pub const VA_LIST: Symbol = Symbol::from_raw(5);
112    /// `__stack`, the arguments that were passed on the stack.
113    pub const STACK: Symbol = Symbol::from_raw(6);
114    /// `__gr_top`, the end of the saved general registers.
115    pub const GR_TOP: Symbol = Symbol::from_raw(7);
116    /// `__vr_top`, the end of the saved vector registers.
117    pub const VR_TOP: Symbol = Symbol::from_raw(8);
118    /// `__gr_offs`, how far back from `__gr_top` the list has read, in bytes and negative.
119    pub const GR_OFFS: Symbol = Symbol::from_raw(9);
120    /// `__vr_offs`, the same for `__vr_top`.
121    pub const VR_OFFS: Symbol = Symbol::from_raw(10);
122}
123
124/// An append-only set of strings, each mapped to a [`Symbol`].
125///
126/// Strings are never removed, which is what makes a `Symbol` valid for the lifetime of the
127/// compilation and what lets the storage be a plain growing buffer.
128pub struct Interner {
129    /// Every interned string, concatenated. One allocation that doubles, rather than one
130    /// allocation per identifier.
131    buf: String,
132    /// Where each symbol starts and ends in `buf`.
133    spans: Vec<(u32, u32)>,
134    /// Lookup from text to symbol. The key is a span into `buf` rather than an owned
135    /// `String`, which is why the map is keyed by the string and rebuilt through `resolve`.
136    map: Map<Box<str>, Symbol>,
137    /// The spelling of a symbol whose bytes are not UTF-8, which `buf` cannot hold because
138    /// `buf` is a `String`. A map rather than a column beside `spans`, because a compilation
139    /// has a handful of these at most and usually none: a raw byte in a string literal is the
140    /// only thing that puts one here.
141    raw: Map<Symbol, Box<[u8]>>,
142    /// Lookup from those bytes back to their symbol, so that interning the same spelling twice
143    /// is the same symbol. Kept apart from `map` because two spellings that are not text can
144    /// read the same lossily and still have to be told apart.
145    raw_map: Map<Box<[u8]>, Symbol>,
146    /// What [`Interner::join`] made of two spellings, keyed by where each of them is and how long
147    /// it is. Both are `'static`, so the place says what is there for as long as the program runs,
148    /// and a place is cheaper to hash than the text it holds.
149    joined: Map<[usize; 4], Symbol>,
150}
151
152impl Default for Interner {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158impl Interner {
159    /// An interner holding the reserved names and nothing else.
160    pub fn new() -> Self {
161        Self::with_capacity(RESERVED.len())
162    }
163
164    /// An interner with room for `cap` strings, to avoid regrowing on a large header set.
165    pub fn with_capacity(cap: usize) -> Self {
166        let cap = cap.max(RESERVED.len());
167        let mut interner = Self {
168            buf: String::with_capacity(cap * 8),
169            spans: Vec::with_capacity(cap),
170            map: Map::with_capacity_and_hasher(cap, Default::default()),
171            raw: Map::default(),
172            raw_map: Map::default(),
173            joined: Map::default(),
174        };
175        for name in RESERVED {
176            interner.intern(name);
177        }
178        interner
179    }
180
181    /// Interns `s`, returning the existing symbol if it has been seen.
182    ///
183    /// # Panics
184    ///
185    /// Panics if more than `Idx::MAX` distinct strings are interned.
186    pub fn intern(&mut self, s: &str) -> Symbol {
187        if let Some(&sym) = self.map.get(s) {
188            return sym;
189        }
190        let start = u32::try_from(self.buf.len()).expect("interner buffer overflow");
191        self.buf.push_str(s);
192        let end = u32::try_from(self.buf.len()).expect("interner buffer overflow");
193        let sym = Symbol(Idx::from_usize(self.spans.len()));
194        self.spans.push((start, end));
195        self.map.insert(s.into(), sym);
196        sym
197    }
198
199    /// Interns `prefix` followed by `name`, returning the existing symbol if it has been seen.
200    ///
201    /// The same answer as interning the two written together, without writing them together
202    /// after the first time. The back end asks this for each machine opcode it puts in each
203    /// function, which is the target's prefix in front of a name out of one of its tables, and
204    /// both halves are the same few hundred static strings every time.
205    pub fn join(&mut self, prefix: &'static str, name: &'static str) -> Symbol {
206        let key = [prefix.as_ptr().addr(), prefix.len(), name.as_ptr().addr(), name.len()];
207        if let Some(&sym) = self.joined.get(&key) {
208            return sym;
209        }
210        let mut spelling = String::with_capacity(prefix.len() + name.len());
211        spelling.push_str(prefix);
212        spelling.push_str(name);
213        let sym = self.intern(&spelling);
214        self.joined.insert(key, sym);
215        sym
216    }
217
218    /// Interns a spelling that may not be text, returning the existing symbol if it has been seen.
219    ///
220    /// A spelling that is UTF-8 is interned as itself, so nothing changes for the common case and
221    /// a byte spelling equal to a name is the same symbol as that name. One that is not gets a
222    /// symbol of its own whose text is the lossy reading, which is what [`Interner::resolve`]
223    /// hands back, and whose bytes are kept beside it for [`Interner::resolve_bytes`].
224    ///
225    /// # Panics
226    ///
227    /// Panics if more than `Idx::MAX` distinct spellings are interned.
228    pub fn intern_bytes(&mut self, bytes: &[u8]) -> Symbol {
229        if let Ok(text) = std::str::from_utf8(bytes) {
230            return self.intern(text);
231        }
232        if let Some(&sym) = self.raw_map.get(bytes) {
233            return sym;
234        }
235        // Pushed straight into the buffer rather than through `intern`, because the lossy
236        // reading may be a string that is already in there and this spelling is not that one.
237        let lossy = String::from_utf8_lossy(bytes);
238        let start = u32::try_from(self.buf.len()).expect("interner buffer overflow");
239        self.buf.push_str(&lossy);
240        let end = u32::try_from(self.buf.len()).expect("interner buffer overflow");
241        let sym = Symbol(Idx::from_usize(self.spans.len()));
242        self.spans.push((start, end));
243        self.raw.insert(sym, bytes.into());
244        self.raw_map.insert(bytes.into(), sym);
245        sym
246    }
247
248    /// The symbol `s` was interned as, and [`None`] when nothing has interned it.
249    ///
250    /// For a name the compiler knows and a program may or may not write: one it did not write
251    /// was never interned, and asking this is how a table of such names is matched against the
252    /// source without adding any of them to it.
253    #[must_use]
254    pub fn find(&self, s: &str) -> Option<Symbol> {
255        self.map.get(s).copied()
256    }
257
258    /// The text behind a symbol.
259    ///
260    /// # Panics
261    ///
262    /// Panics if the symbol came from a different interner. There is one interner per
263    /// compilation, so this is a bug rather than a condition to handle.
264    pub fn resolve(&self, sym: Symbol) -> &str {
265        let (start, end) = self.spans[sym.0.index()];
266        &self.buf[start as usize..end as usize]
267    }
268
269    /// The bytes behind a symbol, which is the spelling exactly as it was written.
270    ///
271    /// The same as `resolve(sym).as_bytes()` for every symbol that came from text, which is all
272    /// of them but the ones [`Interner::intern_bytes`] made from bytes that are not UTF-8.
273    ///
274    /// # Panics
275    ///
276    /// Panics if the symbol came from a different interner, as [`Interner::resolve`] does.
277    pub fn resolve_bytes(&self, sym: Symbol) -> &[u8] {
278        match self.raw.get(&sym) {
279            Some(bytes) => bytes,
280            None => self.resolve(sym).as_bytes(),
281        }
282    }
283
284    /// How many distinct strings have been interned, the reserved names included.
285    pub fn len(&self) -> usize {
286        self.spans.len()
287    }
288
289    /// Whether anything but the reserved names has been interned.
290    pub fn is_empty(&self) -> bool {
291        self.spans.len() <= RESERVED.len()
292    }
293
294    /// Total bytes of interned text, which is the number worth watching on a large build.
295    pub fn bytes(&self) -> usize {
296        self.buf.len()
297    }
298}
299
300impl fmt::Debug for Interner {
301    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302        // Dumping every identifier in a translation unit is never what anyone wanted from a
303        // `{:?}` on the session, so this reports the shape instead.
304        f.debug_struct("Interner")
305            .field("symbols", &self.spans.len())
306            .field("bytes", &self.buf.len())
307            .finish()
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn the_same_string_gets_the_same_symbol() {
317        let mut i = Interner::new();
318        let before = i.len();
319        let a = i.intern("static_assert");
320        let b = i.intern("static_assert");
321        assert_eq!(a, b);
322        assert_eq!(i.len() - before, 1);
323    }
324
325    #[test]
326    fn different_strings_get_different_symbols() {
327        let mut i = Interner::new();
328        let before = i.len();
329        assert_ne!(i.intern("int"), i.intern("long"));
330        assert_eq!(i.len() - before, 2);
331    }
332
333    #[test]
334    fn the_reserved_names_are_there_before_anything_is_read() {
335        let i = Interner::new();
336        assert_eq!(i.len(), RESERVED.len());
337        assert!(i.is_empty(), "the reserved names do not count as something having been read");
338        assert_eq!(i.resolve(sym::VA_LIST_TAG), "__va_list_tag");
339        assert_eq!(i.resolve(sym::GP_OFFSET), "gp_offset");
340        assert_eq!(i.resolve(sym::FP_OFFSET), "fp_offset");
341        assert_eq!(i.resolve(sym::OVERFLOW_ARG_AREA), "overflow_arg_area");
342        assert_eq!(i.resolve(sym::REG_SAVE_AREA), "reg_save_area");
343        assert_eq!(i.resolve(sym::VA_LIST), "__va_list");
344        assert_eq!(i.resolve(sym::STACK), "__stack");
345        assert_eq!(i.resolve(sym::GR_TOP), "__gr_top");
346        assert_eq!(i.resolve(sym::VR_TOP), "__vr_top");
347        assert_eq!(i.resolve(sym::GR_OFFS), "__gr_offs");
348        assert_eq!(i.resolve(sym::VR_OFFS), "__vr_offs");
349    }
350
351    #[test]
352    fn a_reserved_name_written_in_the_source_is_the_symbol_it_already_had() {
353        let mut i = Interner::new();
354        let before = i.len();
355        assert_eq!(i.intern("__va_list_tag"), sym::VA_LIST_TAG);
356        assert_eq!(i.len(), before);
357    }
358
359    #[test]
360    fn every_interner_agrees_on_where_the_reserved_names_are() {
361        let small = Interner::new();
362        let large = Interner::with_capacity(4096);
363        for (at, name) in RESERVED.iter().enumerate() {
364            let sym = Symbol::from_raw(u32::try_from(at).expect("eleven names fit in a u32"));
365            assert_eq!(small.resolve(sym), *name);
366            assert_eq!(large.resolve(sym), *name);
367        }
368    }
369
370    #[test]
371    fn resolves_back_to_the_text() {
372        let mut i = Interner::new();
373        let s = i.intern("__builtin_constant_p");
374        assert_eq!(i.resolve(s), "__builtin_constant_p");
375    }
376
377    #[test]
378    fn symbols_are_numbered_in_allocation_order() {
379        let mut i = Interner::new();
380        let first = i.intern("a");
381        let second = i.intern("b");
382        assert!(first < second, "symbol order must be allocation order, not hash order");
383    }
384
385    #[test]
386    fn the_empty_string_is_internable() {
387        let mut i = Interner::new();
388        let before = i.bytes();
389        let s = i.intern("");
390        assert_eq!(i.resolve(s), "");
391        assert_eq!(i.bytes(), before);
392    }
393
394    #[test]
395    fn a_spelling_that_is_text_is_the_same_symbol_however_it_was_interned() {
396        let mut i = Interner::new();
397        let text = i.intern("hello");
398        assert_eq!(i.intern_bytes(b"hello"), text);
399        assert_eq!(i.resolve_bytes(text), b"hello");
400    }
401
402    #[test]
403    fn a_joined_spelling_is_the_symbol_of_the_two_written_together() {
404        let mut i = Interner::new();
405        let whole = i.intern("x64.mov_rr_64");
406        assert_eq!(i.join("x64.", "mov_rr_64"), whole);
407        assert_eq!(i.join("x64.", "mov_rr_64"), whole);
408        // The same text from two places is still the one symbol.
409        let (prefix, name) = "x64.mov_rr_64".split_at(4);
410        assert_eq!(i.join(prefix, name), whole);
411        assert_ne!(i.join("x64.", "mov_rr_32"), whole);
412    }
413
414    #[test]
415    fn a_spelling_that_is_not_text_keeps_its_bytes() {
416        let mut i = Interner::new();
417        let raw = i.intern_bytes(b"\"\xff\"");
418        assert_eq!(i.resolve_bytes(raw), b"\"\xff\"");
419        assert_eq!(i.intern_bytes(b"\"\xff\""), raw, "interning it twice is one symbol");
420        // The text is the lossy reading, which is what a message quoting it would print.
421        assert_eq!(i.resolve(raw), "\"\u{fffd}\"");
422    }
423
424    #[test]
425    fn two_spellings_that_read_the_same_lossily_are_still_two_symbols() {
426        let mut i = Interner::new();
427        let one = i.intern_bytes(b"\xff");
428        let other = i.intern_bytes(b"\xfe");
429        assert_eq!(i.resolve(one), i.resolve(other), "both read as the replacement character");
430        assert_ne!(one, other, "the bytes differ, so the spellings do");
431        assert_eq!(i.resolve_bytes(one), b"\xff");
432        assert_eq!(i.resolve_bytes(other), b"\xfe");
433        // And neither of them is the text that reads the same, which the source may also hold.
434        assert_ne!(i.intern("\u{fffd}"), one);
435    }
436
437    #[test]
438    fn a_symbol_is_four_bytes() {
439        assert_eq!(size_of::<Symbol>(), 4);
440        assert_eq!(size_of::<Option<Symbol>>(), 4);
441    }
442}