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}