Skip to main content

ryo_symbol/
id.rs

1//! SymbolId - High-performance internal symbol identifier
2//!
3//! Uses SlotMap for O(1) operations with generation counting for dangling detection.
4
5use slotmap::KeyData;
6use std::fmt;
7
8/// High-performance internal symbol ID
9///
10/// # Properties
11/// - O(1) comparison and hashing
12/// - Generation counter for dangling reference detection
13/// - 8 bytes fixed size
14///
15/// # Important: Always obtain via SymbolRegistry
16///
17/// SymbolId must be obtained through `SymbolRegistry::register()` or
18/// `SymbolRegistry::lookup()`. Direct construction is prohibited.
19///
20/// ```ignore
21/// // Correct usage
22/// let id = registry.register(path, kind)?;
23/// let id = registry.lookup(&path)?;
24///
25/// // Prohibited: direct construction
26/// // let id = SymbolId::default();  // Compiles but forbidden
27/// ```
28///
29/// # Stability
30/// SymbolId はセッション内でのみ安定。サーバー再起動で値が変わるため、
31/// セッション跨ぎでのキャッシュや永続化には使用不可。
32/// 永続的な参照が必要な場合は UUID (`MatchResult.uuid`) を使用すること。
33///
34/// # Thread Safety
35/// SymbolId is Copy and can be safely shared across threads.
36/// However, the SymbolRegistry that owns the symbol data
37/// must be properly synchronized.
38#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
39#[repr(transparent)]
40#[derive(Default)]
41pub struct SymbolId(KeyData);
42
43// SlotMap Key trait implementation
44// SAFETY: SymbolId is a newtype wrapper around KeyData with #[repr(transparent)]
45unsafe impl slotmap::Key for SymbolId {
46    fn data(&self) -> KeyData {
47        self.0
48    }
49}
50
51impl From<KeyData> for SymbolId {
52    fn from(k: KeyData) -> Self {
53        Self(k)
54    }
55}
56
57#[cfg(feature = "schemars")]
58impl schemars::JsonSchema for SymbolId {
59    fn schema_name() -> std::borrow::Cow<'static, str> {
60        std::borrow::Cow::Borrowed("SymbolId")
61    }
62
63    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
64        // SymbolId is represented as a string in JSON (e.g., "165v1")
65        generator.subschema_for::<String>()
66    }
67}
68
69impl SymbolId {
70    /// Parse SymbolId from string representation.
71    ///
72    /// Accepts formats:
73    /// - `"165v1"` - compact format (index `v` version)
74    /// - `"SymbolId(165v1)"` - debug format
75    ///
76    /// # Examples
77    /// ```
78    /// # use ryo_symbol::SymbolId;
79    /// let id = SymbolId::parse("165v1");
80    /// let id = SymbolId::parse("SymbolId(165v1)");
81    /// ```
82    pub fn parse(s: &str) -> Option<Self> {
83        // Strip "SymbolId(" prefix and ")" suffix if present
84        let inner = s
85            .strip_prefix("SymbolId(")
86            .and_then(|s| s.strip_suffix(')'))
87            .unwrap_or(s);
88
89        // Parse "indexVversion" format (e.g., "165v1")
90        let (idx_str, ver_str) = inner.split_once('v')?;
91        let idx: u32 = idx_str.parse().ok()?;
92        let ver: u32 = ver_str.parse().ok()?;
93
94        // SlotMap KeyData uses ffi format: (version << 32) | index
95        // But version must be non-zero
96        if ver == 0 {
97            return None;
98        }
99
100        let ffi = ((ver as u64) << 32) | (idx as u64);
101        Some(KeyData::from_ffi(ffi).into())
102    }
103
104    /// Get the index component of this SymbolId.
105    fn index(&self) -> u32 {
106        self.0.as_ffi() as u32
107    }
108
109    /// Get the version component of this SymbolId.
110    fn version(&self) -> u32 {
111        (self.0.as_ffi() >> 32) as u32
112    }
113}
114
115// ============================================================================
116// Display/Debug implementations
117// ============================================================================
118
119/// Display: compact format "165v1"
120///
121/// Use this for user-facing output and serialization.
122impl fmt::Display for SymbolId {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(f, "{}v{}", self.index(), self.version())
125    }
126}
127
128/// Debug: wrapped format "SymbolId(165v1)"
129///
130/// Overrides the default SlotMap KeyData debug output for cleaner logs.
131impl fmt::Debug for SymbolId {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(f, "SymbolId({}v{})", self.index(), self.version())
134    }
135}
136
137// ============================================================================
138// Serialize/Deserialize - TODO: Integrate with persistent UUID keys
139// ============================================================================
140
141// TODO: Proper serialization design
142//
143// ## Problem
144// SymbolId is a SlotMap key and is **session-local** (volatile).
145// Serializing "165v1" and deserializing in another process is meaningless
146// because SlotMap generates new indices/versions per session.
147//
148// ## Design Requirements
149// 1. **Persistent Key**: Assign UUID or stable hash to each symbol
150// 2. **Bidirectional Mapping**: SymbolId ↔ UUID in SymbolRegistry
151// 3. **Contextual Deserialization**: Deserialize with SymbolRegistry context
152//    - Serialize: SymbolId → UUID (via registry.uuid(id))
153//    - Deserialize: UUID → SymbolId (via registry.lookup_by_uuid(uuid))
154//
155// ## Temporary Implementation
156// The following impls serialize to "165v1" format for **debugging only**.
157// They will fail silently when used across sessions/processes.
158// Do NOT rely on these for production persistence.
159
160use serde::{Deserialize, Deserializer, Serialize, Serializer};
161
162impl Serialize for SymbolId {
163    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
164    where
165        S: Serializer,
166    {
167        // WARNING: Session-local representation only
168        // TODO: Replace with UUID serialization
169        serializer.serialize_str(&self.to_string())
170    }
171}
172
173impl<'de> Deserialize<'de> for SymbolId {
174    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
175    where
176        D: Deserializer<'de>,
177    {
178        // WARNING: Session-local representation only
179        // TODO: Replace with UUID → SymbolId lookup via registry
180        let s = String::deserialize(deserializer)?;
181        Self::parse(&s).ok_or_else(|| serde::de::Error::custom(format!("Invalid SymbolId: {}", s)))
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use slotmap::SlotMap;
189
190    #[test]
191    fn test_symbol_id_basic() {
192        let mut map: SlotMap<SymbolId, &str> = SlotMap::with_key();
193
194        let id1 = map.insert("foo");
195        let id2 = map.insert("bar");
196
197        assert_ne!(id1, id2);
198        assert_eq!(map.get(id1), Some(&"foo"));
199        assert_eq!(map.get(id2), Some(&"bar"));
200    }
201
202    #[test]
203    fn test_symbol_id_generation() {
204        let mut map: SlotMap<SymbolId, &str> = SlotMap::with_key();
205
206        let id1 = map.insert("foo");
207        map.remove(id1);
208
209        // New insert gets a different ID (different generation)
210        let id2 = map.insert("bar");
211        assert_ne!(id1, id2);
212
213        // Old ID no longer valid
214        assert!(map.get(id1).is_none());
215    }
216
217    #[test]
218    fn test_parse_compact() {
219        let id = SymbolId::parse("165v1");
220        assert!(id.is_some());
221    }
222
223    #[test]
224    fn test_parse_debug_format() {
225        let id = SymbolId::parse("SymbolId(165v1)");
226        assert!(id.is_some());
227    }
228
229    #[test]
230    fn test_parse_invalid() {
231        assert!(SymbolId::parse("").is_none());
232        assert!(SymbolId::parse("invalid").is_none());
233        assert!(SymbolId::parse("165v0").is_none()); // version 0 is invalid
234    }
235
236    #[test]
237    fn test_display_format() {
238        let mut map: SlotMap<SymbolId, &str> = SlotMap::with_key();
239        let id = map.insert("test");
240        let display = format!("{}", id);
241        // Should be "indexVversion" format
242        assert!(display.contains('v'), "Display format should contain 'v'");
243        assert!(
244            !display.contains("SymbolId"),
245            "Display should not contain 'SymbolId'"
246        );
247    }
248
249    #[test]
250    fn test_debug_format() {
251        let mut map: SlotMap<SymbolId, &str> = SlotMap::with_key();
252        let id = map.insert("test");
253        let debug = format!("{:?}", id);
254        // Should be "SymbolId(indexVversion)" format
255        assert!(
256            debug.starts_with("SymbolId("),
257            "Debug format should start with 'SymbolId('"
258        );
259        assert!(debug.ends_with(')'), "Debug format should end with ')'");
260    }
261
262    #[test]
263    fn test_display_debug_roundtrip() {
264        let mut map: SlotMap<SymbolId, &str> = SlotMap::with_key();
265        let id = map.insert("test");
266
267        // Display format should be parseable
268        let display = format!("{}", id);
269        let parsed_from_display = SymbolId::parse(&display);
270        assert_eq!(
271            Some(id),
272            parsed_from_display,
273            "Should parse from display format"
274        );
275
276        // Debug format should be parseable
277        let debug = format!("{:?}", id);
278        let parsed_from_debug = SymbolId::parse(&debug);
279        assert_eq!(
280            Some(id),
281            parsed_from_debug,
282            "Should parse from debug format"
283        );
284    }
285
286    #[test]
287    fn test_serde_roundtrip() {
288        let mut map: SlotMap<SymbolId, &str> = SlotMap::with_key();
289        let id = map.insert("test");
290
291        // Serialize
292        let json = serde_json::to_string(&id).unwrap();
293
294        // Deserialize
295        let deserialized: SymbolId = serde_json::from_str(&json).unwrap();
296
297        // WARNING: This test only works within the same session
298        // because SymbolId is session-local (SlotMap key)
299        assert_eq!(id, deserialized);
300    }
301}