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}