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}