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}