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}