Skip to main content

sqry_core/graph/unified/string/
id.rs

1//! `StringId` opaque handle for the unified graph architecture.
2//!
3//! This module implements `StringId`, an opaque handle type for interned strings.
4//! Strings are interned to reduce memory usage and enable O(1) equality comparison.
5//!
6//! # Design
7//!
8//! - **Opaque handle**: 32-bit index into string interner
9//! - **Memory efficient**: 4 bytes per ID, shared storage for duplicate strings
10//! - **Fast comparison**: O(1) equality via index comparison
11
12use std::fmt;
13use std::hash::Hash;
14
15use serde::{Deserialize, Serialize};
16
17/// Opaque string identifier for interned strings.
18///
19/// `StringId` provides a type-safe index into the `StringInterner`.
20/// All symbol names, file paths, and other strings are interned to reduce
21/// memory usage and enable fast equality comparison.
22///
23/// # Thread Safety
24///
25/// `StringId` is `Copy` and `Send + Sync`. The actual string data lives
26/// in the `StringInterner` which handles thread safety.
27///
28/// # Example
29///
30/// ```rust,ignore
31/// let interner = StringInterner::new();
32/// let id1 = interner.intern("hello");
33/// let id2 = interner.intern("hello");
34/// assert_eq!(id1, id2);  // Same string = same ID
35/// ```
36#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
37pub struct StringId(u32);
38
39impl StringId {
40    /// Bit used to tag staging-local `StringId`s.
41    ///
42    /// Staging-local `StringIds` are allocated by `GraphBuildHelper` and must be remapped
43    /// via `StagingGraph::commit_strings()` + `StagingGraph::apply_string_remap()`
44    /// before being committed to the main graph.
45    ///
46    /// Global (interner) `StringIds` MUST NEVER have this bit set.
47    pub const LOCAL_TAG_BIT: u32 = 1 << 31;
48
49    /// Invalid sentinel value used to represent "no string" or empty.
50    pub const INVALID: StringId = StringId(u32::MAX);
51
52    /// Creates a new `StringId` from a raw index.
53    ///
54    /// # Arguments
55    ///
56    /// * `index` - The interner index for this string
57    ///
58    /// # Safety Note
59    ///
60    /// This should only be called by the `StringInterner`. Using an index
61    /// that doesn't correspond to an interned string will cause panics
62    /// when resolving.
63    #[inline]
64    #[must_use]
65    pub const fn new(index: u32) -> Self {
66        Self(index)
67    }
68
69    /// Creates a new staging-local `StringId`.
70    ///
71    /// The returned `StringId` is guaranteed to be distinguishable from any
72    /// global (interner) `StringId` by having [`Self::LOCAL_TAG_BIT`] set.
73    #[inline]
74    #[must_use]
75    pub const fn new_local(local_index: u32) -> Self {
76        Self(local_index | Self::LOCAL_TAG_BIT)
77    }
78
79    /// Returns `true` if this is a staging-local `StringId`.
80    #[inline]
81    #[must_use]
82    pub const fn is_local(self) -> bool {
83        !self.is_invalid() && (self.0 & Self::LOCAL_TAG_BIT) != 0
84    }
85
86    /// If this is a staging-local `StringId`, returns its local index.
87    #[inline]
88    #[must_use]
89    pub const fn local_index(self) -> Option<u32> {
90        if self.is_local() {
91            Some(self.0 & !Self::LOCAL_TAG_BIT)
92        } else {
93            None
94        }
95    }
96
97    /// Returns the raw index value.
98    #[inline]
99    #[must_use]
100    pub const fn index(self) -> u32 {
101        self.0
102    }
103
104    /// Returns the index as `usize` for array indexing.
105    #[inline]
106    #[must_use]
107    pub const fn as_usize(self) -> usize {
108        self.0 as usize
109    }
110
111    /// Checks if this is the invalid sentinel value.
112    #[inline]
113    #[must_use]
114    pub const fn is_invalid(self) -> bool {
115        self.0 == u32::MAX
116    }
117
118    /// Checks if this is a valid (non-sentinel) ID.
119    #[inline]
120    #[must_use]
121    pub const fn is_valid(self) -> bool {
122        self.0 != u32::MAX
123    }
124}
125
126impl fmt::Debug for StringId {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        if self.is_invalid() {
129            write!(f, "StringId(INVALID)")
130        } else if self.is_local() {
131            write!(f, "StringId(local:{})", self.local_index().unwrap_or(0))
132        } else {
133            write!(f, "StringId({})", self.0)
134        }
135    }
136}
137
138impl fmt::Display for StringId {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        if self.is_invalid() {
141            write!(f, "INVALID")
142        } else if self.is_local() {
143            write!(f, "local:{}", self.local_index().unwrap_or(0))
144        } else {
145            write!(f, "str:{}", self.0)
146        }
147    }
148}
149
150impl Default for StringId {
151    /// Returns `StringId::INVALID` as the default value.
152    #[inline]
153    fn default() -> Self {
154        Self::INVALID
155    }
156}
157
158impl From<u32> for StringId {
159    #[inline]
160    fn from(index: u32) -> Self {
161        Self(index)
162    }
163}
164
165impl From<usize> for StringId {
166    #[inline]
167    fn from(index: usize) -> Self {
168        Self(u32::try_from(index).unwrap_or(u32::MAX))
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_string_id_creation() {
178        let id = StringId::new(42);
179        assert_eq!(id.index(), 42);
180        assert_eq!(id.as_usize(), 42);
181        assert!(!id.is_invalid());
182        assert!(id.is_valid());
183    }
184
185    #[test]
186    fn test_string_id_invalid_sentinel() {
187        assert!(StringId::INVALID.is_invalid());
188        assert!(!StringId::INVALID.is_valid());
189        assert_eq!(StringId::INVALID.index(), u32::MAX);
190    }
191
192    #[test]
193    fn test_string_id_default() {
194        let default_id: StringId = StringId::default();
195        assert_eq!(default_id, StringId::INVALID);
196    }
197
198    #[test]
199    fn test_string_id_equality() {
200        let id1 = StringId::new(5);
201        let id2 = StringId::new(5);
202        let id3 = StringId::new(6);
203
204        assert_eq!(id1, id2);
205        assert_ne!(id1, id3);
206    }
207
208    #[test]
209    fn test_string_id_hash() {
210        use std::collections::HashSet;
211
212        let mut set = HashSet::new();
213        set.insert(StringId::new(1));
214        set.insert(StringId::new(2));
215        set.insert(StringId::new(3));
216
217        assert!(set.contains(&StringId::new(1)));
218        assert!(!set.contains(&StringId::new(4)));
219        assert_eq!(set.len(), 3);
220    }
221
222    #[test]
223    fn test_string_id_from() {
224        let from_u32: StringId = 42u32.into();
225        assert_eq!(from_u32.index(), 42);
226
227        let from_usize: StringId = 42usize.into();
228        assert_eq!(from_usize.index(), 42);
229    }
230
231    #[test]
232    fn test_debug_display_format() {
233        let id = StringId::new(42);
234        assert_eq!(format!("{id:?}"), "StringId(42)");
235        assert_eq!(format!("{id}"), "str:42");
236
237        assert_eq!(format!("{:?}", StringId::INVALID), "StringId(INVALID)");
238        assert_eq!(format!("{}", StringId::INVALID), "INVALID");
239    }
240
241    #[test]
242    fn test_serde_roundtrip() {
243        let original = StringId::new(123);
244
245        // JSON roundtrip
246        let json = serde_json::to_string(&original).unwrap();
247        let deserialized: StringId = serde_json::from_str(&json).unwrap();
248        assert_eq!(original, deserialized);
249
250        // Postcard roundtrip
251        let bytes = postcard::to_allocvec(&original).unwrap();
252        let deserialized: StringId = postcard::from_bytes(&bytes).unwrap();
253        assert_eq!(original, deserialized);
254    }
255
256    #[test]
257    fn test_size_of_string_id() {
258        // Verify memory layout: u32 = 4 bytes
259        assert_eq!(std::mem::size_of::<StringId>(), 4);
260    }
261
262    #[test]
263    #[allow(clippy::clone_on_copy)] // Intentionally testing Clone trait
264    fn test_copy_clone() {
265        let id = StringId::new(10);
266        let copied = id;
267        let cloned = id.clone();
268
269        assert_eq!(id, copied);
270        assert_eq!(id, cloned);
271    }
272}