Skip to main content

intern_lang/
symbol.rs

1//! The [`Symbol`] handle: a small, copyable stand-in for an interned string.
2
3use core::fmt;
4use core::num::NonZeroU32;
5
6/// A small, copyable handle to a string held by an [`Interner`](crate::Interner).
7///
8/// A `Symbol` is a newtype over a 32-bit id, so it is four bytes, `Copy`, and as
9/// cheap to pass or store as an integer. The point of interning is that a name in
10/// an AST node or a symbol-table key costs a `Symbol` instead of an owned
11/// `String`, and comparing two names becomes an integer comparison instead of a
12/// byte-by-byte walk — `==`, `Ord`, and `Hash` on a `Symbol` never touch the
13/// underlying bytes.
14///
15/// A `Symbol` is only meaningful together with the interner that issued it. Two
16/// symbols from the *same* interner are equal exactly when they name the same
17/// string; symbols from different interners share no relationship and should not
18/// be compared or resolved across interners (resolving a foreign symbol is
19/// handled safely — see [`Interner::resolve`](crate::Interner::resolve) — but the
20/// result is not meaningful).
21///
22/// # Examples
23///
24/// ```
25/// use intern_lang::Interner;
26///
27/// let mut interner = Interner::new();
28/// let a = interner.intern("loop");
29/// let b = interner.intern("loop");
30///
31/// // Same string interns to the same symbol, so equality is an integer compare.
32/// assert_eq!(a, b);
33///
34/// // A symbol is `Copy`: handing it around never moves or clones a string.
35/// let copy = a;
36/// assert_eq!(copy, a);
37/// ```
38#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub struct Symbol(NonZeroU32);
40
41impl Symbol {
42    /// Builds a symbol from a 1-based id.
43    ///
44    /// The interner only ever calls this with `id >= 1` (ids are assigned
45    /// sequentially starting at one), so the `NonZeroU32` construction always
46    /// succeeds; the saturating fallback exists solely to keep this path free of
47    /// `unwrap`/`expect` and can never be reached with a valid id.
48    #[inline]
49    pub(crate) fn from_raw(id: u32) -> Self {
50        Self(NonZeroU32::new(id).unwrap_or(NonZeroU32::MIN))
51    }
52
53    /// Returns the 0-based index this symbol occupies in the issuing interner's
54    /// storage. Used internally to look the string back up.
55    #[inline]
56    pub(crate) fn index(self) -> usize {
57        // `get()` is in `1..=u32::MAX`, so the subtraction never underflows.
58        self.0.get() as usize - 1
59    }
60
61    /// Returns the raw 1-based integer id backing this symbol.
62    ///
63    /// The id is stable for the lifetime of the issuing interner and is assigned
64    /// in interning order: the first distinct string interned gets `1`, the
65    /// second `2`, and so on. It is exposed for diagnostics, stable ordering, and
66    /// building external side tables keyed by symbol; it is not a string and
67    /// carries no meaning outside the interner that produced it.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// use intern_lang::Interner;
73    ///
74    /// let mut interner = Interner::new();
75    /// let first = interner.intern("alpha");
76    /// let second = interner.intern("beta");
77    ///
78    /// assert_eq!(first.as_u32(), 1);
79    /// assert_eq!(second.as_u32(), 2);
80    /// // Re-interning an existing string returns the same id, never a new one.
81    /// assert_eq!(interner.intern("alpha").as_u32(), 1);
82    /// ```
83    #[inline]
84    #[must_use]
85    pub fn as_u32(self) -> u32 {
86        self.0.get()
87    }
88}
89
90impl fmt::Debug for Symbol {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        write!(f, "Symbol({})", self.0.get())
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_from_raw_roundtrips_through_as_u32() {
102        for id in [1u32, 2, 7, 1000, u32::MAX] {
103            assert_eq!(Symbol::from_raw(id).as_u32(), id);
104        }
105    }
106
107    #[test]
108    fn test_index_is_zero_based() {
109        assert_eq!(Symbol::from_raw(1).index(), 0);
110        assert_eq!(Symbol::from_raw(42).index(), 41);
111    }
112
113    #[test]
114    fn test_symbol_is_four_bytes() {
115        assert_eq!(core::mem::size_of::<Symbol>(), 4);
116    }
117
118    #[test]
119    fn test_option_symbol_is_niche_optimized() {
120        // `NonZeroU32` lets `Option<Symbol>` reuse the zero bit pattern, so it
121        // stays four bytes — a property serde and external side tables rely on.
122        assert_eq!(core::mem::size_of::<Option<Symbol>>(), 4);
123    }
124
125    #[test]
126    fn test_equal_ids_compare_equal() {
127        assert_eq!(Symbol::from_raw(5), Symbol::from_raw(5));
128        assert_ne!(Symbol::from_raw(5), Symbol::from_raw(6));
129    }
130
131    #[test]
132    fn test_debug_shows_id() {
133        extern crate alloc;
134        assert_eq!(alloc::format!("{:?}", Symbol::from_raw(9)), "Symbol(9)");
135    }
136}