Skip to main content

rustyfi_lang/
symbol.rs

1//! Interned identifiers: [`SymbolStore`] (an append-only unique-string
2//! registry) and [`Symbol`] (a `Copy`, `u32`-sized handle into one).
3//!
4//! The brand covers the *compile side* only (`Ast` → elaborate →
5//! typecheck); the runtime side never sees a `Symbol`, because names are
6//! resolved away at the compile membrane (`compile.rs`).
7//!
8//! # Why a lifetime brand
9//!
10//! A `Symbol<'s>` is just an index, so on its own it would be meaninglessly
11//! interchangeable between stores and would outlive the store it names. The
12//! `PhantomData<&'s SymbolStore>` ties every symbol to the borrow of the store
13//! that minted it, which is what lets the AST hold bare `u32`s while the
14//! compiler still guarantees a live store is available wherever a symbol is
15//! read back as text. The brand costs nothing at run time — and it is
16//! *deliberately* confined to the front half of the pipeline: letting it reach
17//! `Value` would cascade a lifetime through all 172 `prim_*` functions for
18//! zero speed.
19//!
20//! # Interning is not enough — `resolve` must be cheap
21//!
22//! The port models namespacing with flat mangled string keys (`"M.x"`,
23//! `"\cmd"`, `"$M.atan2"`, `"%cmd_arg0"`), and several consumers *inspect that
24//! text*: the command-sigil test, `Scope::names_with_prefix`, `open_module`'s
25//! prefix scan, and — critically for byte-identity — the **lexicographic
26//! sorting** of optional-argument label rows and record kinds. A `Symbol`'s
27//! index order is *insertion* order, never lexicographic, so all of those must
28//! keep working on resolved text. [`SymbolStore::resolve`] therefore returns a
29//! borrowed `&str` (no allocation, no refcount bump) rather than an owned or
30//! reference-counted string.
31
32use std::cell::RefCell;
33use std::collections::HashMap;
34use std::marker::PhantomData;
35
36/// An interned identifier: an index into the [`SymbolStore`] that minted it,
37/// branded with that store's borrow lifetime.
38///
39/// `Copy` + `Eq` + `Hash` in one `u32`, so scope stacks, type environments and
40/// the compiler's lexical frames compare and hash identifiers by integer
41/// instead of by string content.
42///
43/// Equality is index equality, which is *exactly* string equality for symbols
44/// from the same store (the store deduplicates on intern). Comparing symbols
45/// from two different stores is meaningless but not unsound — see
46/// [`SymbolStore::resolve`].
47///
48/// [`Ord`] is **index order (insertion order), not lexicographic order.** It
49/// exists only so symbols can key a `BTreeMap`/`BTreeSet` for deterministic
50/// iteration within one run. Anywhere the *output* depends on ordering (type
51/// error text, record kinds, optional-label rows), sort by
52/// [`SymbolStore::resolve`] text instead — see this module's header.
53#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub struct Symbol<'s>(u32, PhantomData<&'s SymbolStore>);
55
56impl Symbol<'_> {
57    /// The raw index. Useful for dense side-tables keyed by symbol; carries no
58    /// meaning without the store that minted it.
59    #[inline]
60    pub fn index(self) -> u32 {
61        self.0
62    }
63}
64
65/// Prints the index, not the text — a `Symbol` is a bare `u32` plus a
66/// zero-sized brand, so it has no way to reach its store from here.
67///
68/// This is a deliberate choice: golden tests must diff
69/// *resolved* strings produced at their format site, never `Debug`-of-AST.
70impl std::fmt::Debug for Symbol<'_> {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(f, "Symbol({})", self.0)
73    }
74}
75
76/// An append-only, deduplicating registry of identifier strings.
77///
78/// Interning takes `&self`, not `&mut self`: elaboration and typechecking both
79/// *mint new derived names mid-pass* (`qualify_key`'s `"M.x"`, `$`-mangled
80/// module keys, `"%patbind…"`, fresh desugar names like `"%cmd_arg0"`,
81/// qualified constructor lookup keys), so the store must stay interned-into
82/// while the tree it brands is being walked. Hence the interior mutability.
83///
84/// Entries are never removed or mutated, which is what makes [`resolve`]
85/// able to hand out `&str`s that live as long as the store borrow.
86///
87/// [`resolve`]: SymbolStore::resolve
88#[derive(Default)]
89pub struct SymbolStore {
90    inner: RefCell<StoreInner>,
91}
92
93#[derive(Default)]
94struct StoreInner {
95    /// Index → text. `Box<str>` (not `String`) because the pointed-to bytes
96    /// must never move once pushed: growing this `Vec` relocates the *boxes*,
97    /// not the string data they own, which is the invariant `resolve`'s
98    /// lifetime extension rests on.
99    texts: Vec<Box<str>>,
100    /// Text → index, for dedup. Keys alias `texts`' contents conceptually but
101    /// are stored separately (a second `Box<str>`) to keep this safe code.
102    index: HashMap<Box<str>, u32>,
103}
104
105impl SymbolStore {
106    pub fn new() -> SymbolStore {
107        SymbolStore::default()
108    }
109
110    /// Intern `text`, returning its symbol. Interning the same text twice
111    /// returns the same symbol.
112    pub fn intern<'s>(&'s self, text: &str) -> Symbol<'s> {
113        let mut inner = self.inner.borrow_mut();
114        if let Some(&i) = inner.index.get(text) {
115            return Symbol(i, PhantomData);
116        }
117        let i = u32::try_from(inner.texts.len())
118            .expect("SymbolStore overflow: more than u32::MAX distinct identifiers");
119        let boxed: Box<str> = text.into();
120        inner.index.insert(boxed.clone(), i);
121        inner.texts.push(boxed);
122        Symbol(i, PhantomData)
123    }
124
125    /// The text `sym` was interned from.
126    ///
127    /// # Panics
128    ///
129    /// If `sym` was minted by a *different* store that happened to share this
130    /// one's borrow lifetime and holds more entries than this one. The brand
131    /// makes that hard to write by accident and it is not unsound — the bounds
132    /// check below turns it into a panic rather than a bogus read — but a
133    /// program should still keep one store per pipeline run.
134    pub fn resolve<'s>(&'s self, sym: Symbol<'s>) -> &'s str {
135        let inner = self.inner.borrow();
136        let s: &str = inner
137            .texts
138            .get(sym.0 as usize)
139            .unwrap_or_else(|| panic!("Symbol({}) does not belong to this SymbolStore", sym.0));
140        // SAFETY: `s` points into the heap allocation owned by a `Box<str>`
141        // that lives in `inner.texts`. That allocation's address is fixed for
142        // as long as the box exists, and:
143        //   * entries are only ever *pushed* — never removed, replaced, or
144        //     mutated (the only writer is `intern`, which pushes);
145        //   * growing `texts` relocates the `Box` pointers, not the `str`
146        //     bytes they point at;
147        //   * the data outlives the `RefCell` borrow guard and is dropped only
148        //     with `self`,
149        // so widening the guard-scoped borrow to `&'s self`'s lifetime does
150        // not create a dangling or aliasing-mutable reference. This is the
151        // standard interner pattern; it is needed because a `RefCell` cannot
152        // otherwise lend out data past the guard.
153        unsafe { &*(s as *const str) }
154    }
155
156    /// How many distinct strings have been interned. (Also the index the next
157    /// new symbol will get — useful for sizing dense side-tables.)
158    pub fn len(&self) -> usize {
159        self.inner.borrow().texts.len()
160    }
161
162    pub fn is_empty(&self) -> bool {
163        self.len() == 0
164    }
165}
166
167impl std::fmt::Debug for SymbolStore {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        write!(f, "SymbolStore({} symbols)", self.len())
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use std::collections::HashSet;
177
178    #[test]
179    fn intern_dedups_and_resolves() {
180        let store = SymbolStore::new();
181        let a = store.intern("foo");
182        let b = store.intern("bar");
183        let a2 = store.intern("foo");
184        assert_eq!(a, a2);
185        assert_ne!(a, b);
186        assert_eq!(store.resolve(a), "foo");
187        assert_eq!(store.resolve(b), "bar");
188        assert_eq!(store.len(), 2);
189    }
190
191    #[test]
192    fn indices_are_dense_and_insertion_ordered() {
193        let store = SymbolStore::new();
194        assert!(store.is_empty());
195        let syms: Vec<_> = ["z", "y", "x"].iter().map(|s| store.intern(s)).collect();
196        assert_eq!(
197            syms.iter().map(|s| s.index()).collect::<Vec<_>>(),
198            vec![0, 1, 2]
199        );
200        let mut sorted = syms.clone();
201        sorted.sort();
202        assert_eq!(sorted, syms);
203        let mut by_text: Vec<&str> = syms.iter().map(|&s| store.resolve(s)).collect();
204        by_text.sort_unstable();
205        assert_eq!(by_text, vec!["x", "y", "z"]);
206    }
207
208    #[test]
209    fn resolved_borrows_survive_further_interning() {
210        // resolve()'s borrow must survive further interning (elaborate/
211        // typecheck mint derived names mid-walk).
212        let store = SymbolStore::new();
213        let first = store.resolve(store.intern("first"));
214        for i in 0..10_000 {
215            store.intern(&format!("derived%{i}"));
216        }
217        assert_eq!(first, "first");
218        assert_eq!(store.len(), 10_001);
219    }
220
221    #[test]
222    fn mangled_keys_round_trip_verbatim() {
223        // The port's identifiers are mangled composites; nothing about them is
224        // normalized on the way through.
225        let store = SymbolStore::new();
226        for key in ["M.x", "\\cmd", "+p", "M.\\cmd", "$M.atan2", "%context"] {
227            assert_eq!(store.resolve(store.intern(key)), key);
228        }
229        assert_eq!(store.len(), 6);
230    }
231
232    #[test]
233    fn symbols_are_hashable_keys() {
234        let store = SymbolStore::new();
235        let set: HashSet<Symbol<'_>> = ["a", "b", "a", "c"]
236            .iter()
237            .map(|s| store.intern(s))
238            .collect();
239        assert_eq!(set.len(), 3);
240        assert_eq!(std::mem::size_of::<Symbol<'_>>(), 4);
241    }
242}