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///
39/// # Serialization
40///
41/// With the `serde` feature enabled, `Symbol` serializes transparently as its raw
42/// integer id (the same value [`as_u32`](Symbol::as_u32) returns) and
43/// deserializes back, rejecting `0`. The id is only meaningful with the interner
44/// that issued it, so persist symbols alongside the interner that produced them.
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
47pub struct Symbol(NonZeroU32);
48
49impl Symbol {
50    /// Reconstructs a symbol from a raw 1-based id, or `None` if `id` is `0`.
51    ///
52    /// This is the inverse of [`as_u32`](Symbol::as_u32), for rebuilding a symbol
53    /// from an id stored elsewhere (a file, a wire message, an external table).
54    /// It only rebuilds the handle; whether the id names anything is decided when
55    /// you [`resolve`](crate::Interner::resolve) it against an interner, which
56    /// returns `None` for an out-of-range id. A symbol is only meaningful with the
57    /// interner that issued it.
58    ///
59    /// # Examples
60    ///
61    /// ```
62    /// use intern_lang::{Interner, Symbol};
63    ///
64    /// let mut interner = Interner::new();
65    /// let sym = interner.intern("persisted");
66    ///
67    /// // Round-trip a symbol through its raw id.
68    /// let rebuilt = Symbol::from_u32(sym.as_u32()).unwrap();
69    /// assert_eq!(rebuilt, sym);
70    /// assert_eq!(interner.resolve(rebuilt), Some("persisted"));
71    ///
72    /// // Zero is never a valid symbol id.
73    /// assert_eq!(Symbol::from_u32(0), None);
74    /// ```
75    #[inline]
76    #[must_use]
77    pub fn from_u32(id: u32) -> Option<Self> {
78        NonZeroU32::new(id).map(Self)
79    }
80
81    /// Builds a symbol from a 1-based id.
82    ///
83    /// The interner only ever calls this with `id >= 1` (ids are assigned
84    /// sequentially starting at one), so the `NonZeroU32` construction always
85    /// succeeds; the saturating fallback exists solely to keep this path free of
86    /// `unwrap`/`expect` and can never be reached with a valid id.
87    #[inline]
88    pub(crate) fn from_raw(id: u32) -> Self {
89        Self(NonZeroU32::new(id).unwrap_or(NonZeroU32::MIN))
90    }
91
92    /// Returns the 0-based index this symbol occupies in the issuing interner's
93    /// storage. Used internally to look the string back up.
94    #[inline]
95    pub(crate) fn index(self) -> usize {
96        // `get()` is in `1..=u32::MAX`, so the subtraction never underflows.
97        self.0.get() as usize - 1
98    }
99
100    /// Returns the raw 1-based integer id backing this symbol.
101    ///
102    /// The id is stable for the lifetime of the issuing interner and is assigned
103    /// in interning order: the first distinct string interned gets `1`, the
104    /// second `2`, and so on. It is exposed for diagnostics, stable ordering, and
105    /// building external side tables keyed by symbol; it is not a string and
106    /// carries no meaning outside the interner that produced it.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use intern_lang::Interner;
112    ///
113    /// let mut interner = Interner::new();
114    /// let first = interner.intern("alpha");
115    /// let second = interner.intern("beta");
116    ///
117    /// assert_eq!(first.as_u32(), 1);
118    /// assert_eq!(second.as_u32(), 2);
119    /// // Re-interning an existing string returns the same id, never a new one.
120    /// assert_eq!(interner.intern("alpha").as_u32(), 1);
121    /// ```
122    #[inline]
123    #[must_use]
124    pub fn as_u32(self) -> u32 {
125        self.0.get()
126    }
127}
128
129impl fmt::Debug for Symbol {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        write!(f, "Symbol({})", self.0.get())
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_from_raw_roundtrips_through_as_u32() {
141        for id in [1u32, 2, 7, 1000, u32::MAX] {
142            assert_eq!(Symbol::from_raw(id).as_u32(), id);
143        }
144    }
145
146    #[test]
147    fn test_index_is_zero_based() {
148        assert_eq!(Symbol::from_raw(1).index(), 0);
149        assert_eq!(Symbol::from_raw(42).index(), 41);
150    }
151
152    #[test]
153    fn test_symbol_is_four_bytes() {
154        assert_eq!(core::mem::size_of::<Symbol>(), 4);
155    }
156
157    #[test]
158    fn test_option_symbol_is_niche_optimized() {
159        // `NonZeroU32` lets `Option<Symbol>` reuse the zero bit pattern, so it
160        // stays four bytes — a property serde and external side tables rely on.
161        assert_eq!(core::mem::size_of::<Option<Symbol>>(), 4);
162    }
163
164    #[test]
165    fn test_equal_ids_compare_equal() {
166        assert_eq!(Symbol::from_raw(5), Symbol::from_raw(5));
167        assert_ne!(Symbol::from_raw(5), Symbol::from_raw(6));
168    }
169
170    #[test]
171    fn test_debug_shows_id() {
172        extern crate alloc;
173        assert_eq!(alloc::format!("{:?}", Symbol::from_raw(9)), "Symbol(9)");
174    }
175}