Skip to main content

ryo_symbol/
registry.rs

1//! SymbolRegistry - Central registry for symbol management
2//!
3//! # Persistent Identity (UUID)
4//!
5//! Every symbol is automatically assigned a UUID at registration time.
6//! This UUID serves as the **persistent identity** that survives:
7//! - Server restarts (SymbolId is session-volatile)
8//! - Symbol renames (SymbolPath changes, UUID stays)
9//! - Code refactoring (same entity, different location)
10//!
11//! ## Why UUID, not SymbolPath?
12//!
13//! SymbolPath is like Active Directory's Distinguished Name (DN) - it describes
14//! **where** something is, not **what** it is. When you rename `foo::Bar` to
15//! `foo::Baz`, the SymbolPath changes but the entity remains the same.
16//!
17//! ## External Output Requirements
18//!
19//! **All external APIs that expose symbol references MUST use UUID.**
20//!
21//! ### TODO: Migrate to UUID-based output
22//!
23//! The following locations currently use SymbolPath or SymbolId strings
24//! and should be migrated to include UUID:
25//!
26//! - `ryo-app/src/api/types.rs`:
27//!   - `Suggestion.symbol_path` → add `uuid: Uuid`
28//!   - `TypeAnalysisResponse.symbol_id` → add `uuid: Uuid`
29//!   - `TypeAnalysisResponse.supertraits` → `Vec<SymbolRef>` with uuid
30//!   - `TypeAnalysisResponse.implementors` → `Vec<SymbolRef>` with uuid
31//!   - `TypeImpactInfo.containing_types` → `Vec<SymbolRef>` with uuid
32//!   - `VarInfo.symbol_path` → add `uuid: Uuid`
33//!   - `ChainNodeInfo.id` → add `uuid: Uuid`
34//!   - `CascadeResponse.callers/users` → `Vec<SymbolRef>` with uuid
35//!
36//! - `ryo-storage/src/txlog/entry.rs`:
37//!   - `TxAction::MutationApplied.affected_symbols` → `Vec<Uuid>`
38//!   - `MutationRecord.affected_symbols` → `Vec<Uuid>`
39//!
40//! ## Performance Note
41//!
42//! Current implementation assigns UUID to ALL symbols at registration.
43//! If memory becomes an issue (~80 bytes per symbol), consider lazy allocation
44//! only when symbols are exposed via external APIs.
45
46use std::collections::HashMap;
47
48use slotmap::{SecondaryMap, SlotMap};
49use uuid::Uuid;
50
51use crate::error::{InvalidSymbolId, RegistrationError, RenameError, UnregisterReexportError};
52use crate::file_path::WorkspaceFilePath;
53use crate::id::SymbolId;
54use crate::kind::SymbolKind;
55use crate::path::SymbolPath;
56use crate::span::{FileSpan, Visibility};
57use crate::var_scope::VarScope;
58
59/// Re-export information
60#[derive(Debug, Clone)]
61pub struct ReExportInfo {
62    /// Re-export path (alias)
63    pub alias_path: SymbolPath,
64    /// File where the re-export is defined
65    pub origin_file: WorkspaceFilePath,
66}
67
68/// Central registry for symbol management
69///
70/// # Single Point of Access
71///
72/// **All symbol operations must go through SymbolRegistry.**
73///
74/// - Get SymbolId: `register()` or `lookup()`
75/// - Get/update metadata: `kind()`, `span()`, `set_span()`, etc.
76/// - Graph operations also use SymbolId
77///
78/// # Responsibilities
79/// - Bidirectional SymbolPath ↔ SymbolId conversion
80/// - Metadata management (Kind, Span, Visibility, etc.)
81/// - Symbol lifecycle management
82///
83/// # Thread Safety
84/// - Read operations are thread-safe
85/// - Write operations require exclusive access
86///   (Executor controls this at Tick boundaries)
87#[derive(Clone)]
88pub struct SymbolRegistry {
89    // === Core Mappings ===
90    /// SymbolId → SymbolPath (reverse lookup)
91    id_to_path: SlotMap<SymbolId, SymbolPath>,
92    /// SymbolPath → SymbolId (forward lookup)
93    path_to_id: HashMap<SymbolPath, SymbolId>,
94
95    // === Metadata (SecondaryMap) ===
96    /// Symbol kind
97    kinds: SecondaryMap<SymbolId, SymbolKind>,
98    /// File location
99    spans: SecondaryMap<SymbolId, FileSpan>,
100    /// Visibility
101    visibility: SecondaryMap<SymbolId, Visibility>,
102    /// Parent symbol (for InSymbol)
103    parents: SecondaryMap<SymbolId, SymbolId>,
104
105    // === Re-export Management ===
106    /// Re-export info (canonical → aliases)
107    re_exports: SecondaryMap<SymbolId, Vec<ReExportInfo>>,
108    /// Re-export reverse lookup (alias path → canonical SymbolId)
109    alias_to_canonical: HashMap<SymbolPath, SymbolId>,
110
111    // === Persistent Identity (UUID) ===
112    /// SymbolId → Uuid (O(1) lookup for serialization)
113    id_to_uuid: SecondaryMap<SymbolId, Uuid>,
114    /// Uuid → SymbolId (reverse lookup for deserialization)
115    uuid_to_id: HashMap<Uuid, SymbolId>,
116    /// Preloaded UUID mappings from previous session (SymbolPath → Uuid)
117    /// Used during register() to restore persistent identity
118    preloaded_uuids: HashMap<SymbolPath, Uuid>,
119}
120
121impl SymbolRegistry {
122    /// Create a new empty registry
123    pub fn new() -> Self {
124        Self {
125            id_to_path: SlotMap::with_key(),
126            path_to_id: HashMap::new(),
127            kinds: SecondaryMap::new(),
128            spans: SecondaryMap::new(),
129            visibility: SecondaryMap::new(),
130            parents: SecondaryMap::new(),
131            re_exports: SecondaryMap::new(),
132            alias_to_canonical: HashMap::new(),
133            id_to_uuid: SecondaryMap::new(),
134            uuid_to_id: HashMap::new(),
135            preloaded_uuids: HashMap::new(),
136        }
137    }
138
139    /// Create with pre-allocated capacity
140    pub fn with_capacity(capacity: usize) -> Self {
141        Self {
142            id_to_path: SlotMap::with_capacity_and_key(capacity),
143            path_to_id: HashMap::with_capacity(capacity),
144            kinds: SecondaryMap::new(),
145            spans: SecondaryMap::new(),
146            visibility: SecondaryMap::new(),
147            parents: SecondaryMap::new(),
148            re_exports: SecondaryMap::new(),
149            alias_to_canonical: HashMap::new(),
150            id_to_uuid: SecondaryMap::new(),
151            uuid_to_id: HashMap::with_capacity(capacity),
152            preloaded_uuids: HashMap::new(),
153        }
154    }
155
156    // ========== Registration ==========
157
158    /// Register a symbol (returns existing ID if already registered)
159    ///
160    /// # Returns
161    /// - `Ok(SymbolId)`: Registration successful (new or existing)
162    /// - `Err(RegistrationError)`: Registration failed
163    pub fn register(
164        &mut self,
165        path: SymbolPath,
166        kind: SymbolKind,
167    ) -> Result<SymbolId, RegistrationError> {
168        // Check if already registered
169        if let Some(&existing_id) = self.path_to_id.get(&path) {
170            // Verify kind matches
171            if let Some(&existing_kind) = self.kinds.get(existing_id) {
172                if existing_kind != kind {
173                    return Err(RegistrationError::ConflictingKind {
174                        path: Box::new(path),
175                        existing: existing_kind,
176                        new: kind,
177                    });
178                }
179            }
180            return Ok(existing_id);
181        }
182
183        // Register new symbol
184        let id = self.id_to_path.insert(path.clone());
185        self.path_to_id.insert(path.clone(), id);
186        self.kinds.insert(id, kind);
187
188        // Use preloaded UUID if available (from previous session), otherwise generate new
189        let uuid = self
190            .preloaded_uuids
191            .remove(&path)
192            .unwrap_or_else(Uuid::new_v4);
193        self.id_to_uuid.insert(id, uuid);
194        self.uuid_to_id.insert(uuid, id);
195
196        Ok(id)
197    }
198
199    /// Register with full metadata
200    pub fn register_with_metadata(
201        &mut self,
202        path: SymbolPath,
203        kind: SymbolKind,
204        span: Option<FileSpan>,
205        vis: Option<Visibility>,
206    ) -> Result<SymbolId, RegistrationError> {
207        let id = self.register(path, kind)?;
208
209        if let Some(span) = span {
210            self.spans.insert(id, span);
211        }
212        if let Some(vis) = vis {
213            self.visibility.insert(id, vis);
214        }
215
216        Ok(id)
217    }
218
219    /// Register a variable (InSymbol)
220    ///
221    /// Creates a path like `parent::$scope::name` and registers it.
222    ///
223    /// # Arguments
224    /// - `containing_symbol`: Parent symbol (function/method/struct)
225    /// - `scope`: Variable scope type
226    /// - `name`: Variable name
227    /// - `kind`: Symbol kind (Variable, Parameter, or Field)
228    pub fn register_var(
229        &mut self,
230        containing_symbol: SymbolId,
231        scope: VarScope,
232        name: &str,
233        kind: SymbolKind,
234    ) -> Result<SymbolId, RegistrationError> {
235        // Get parent path
236        let parent_path = self
237            .path(containing_symbol)
238            .ok_or(RegistrationError::InvalidParent)?
239            .clone();
240
241        // Create variable path
242        let var_path = parent_path
243            .with_var_scope(scope, name)
244            .map_err(RegistrationError::InvalidPath)?;
245
246        // Register
247        let id = self.register(var_path, kind)?;
248
249        // Record parent relationship
250        self.parents.insert(id, containing_symbol);
251
252        Ok(id)
253    }
254
255    // ========== Lookup ==========
256
257    /// SymbolPath → SymbolId (O(1) hash lookup)
258    ///
259    /// Also resolves re-export aliases to their canonical ID.
260    #[inline]
261    pub fn lookup(&self, path: &SymbolPath) -> Option<SymbolId> {
262        // Try canonical path first
263        if let Some(&id) = self.path_to_id.get(path) {
264            return Some(id);
265        }
266        // Try as alias
267        self.alias_to_canonical.get(path).copied()
268    }
269
270    /// SymbolId → SymbolPath (O(1) array access)
271    #[inline]
272    pub fn resolve(&self, id: SymbolId) -> Option<&SymbolPath> {
273        self.id_to_path.get(id)
274    }
275
276    /// Alias for resolve() - get path from ID
277    #[inline]
278    pub fn path(&self, id: SymbolId) -> Option<&SymbolPath> {
279        self.resolve(id)
280    }
281
282    /// Get SymbolRef (ID + Path) for unified display
283    ///
284    /// # Format
285    /// Returns `SymbolRef` which displays as: `SymbolId(2v1)@path::to::symbol`
286    ///
287    /// # Example
288    /// ```ignore
289    /// let sym_ref = registry.get_ref(id)?;
290    /// println!("{}", sym_ref);  // SymbolId(2v1)@my_crate::MyStruct
291    /// ```
292    #[inline]
293    pub fn get_ref(&self, id: SymbolId) -> Option<crate::SymbolRef> {
294        self.resolve(id)
295            .map(|path| crate::SymbolRef::new(id, path.clone()))
296    }
297
298    /// Check if SymbolId is valid (with generation check)
299    #[inline]
300    pub fn contains(&self, id: SymbolId) -> bool {
301        self.id_to_path.contains_key(id)
302    }
303
304    // ========== Metadata Access ==========
305
306    /// Get kind
307    #[inline]
308    pub fn kind(&self, id: SymbolId) -> Option<SymbolKind> {
309        self.kinds.get(id).copied()
310    }
311
312    /// Get span
313    #[inline]
314    pub fn span(&self, id: SymbolId) -> Option<&FileSpan> {
315        self.spans.get(id)
316    }
317
318    /// Get visibility
319    #[inline]
320    pub fn visibility(&self, id: SymbolId) -> Option<&Visibility> {
321        self.visibility.get(id)
322    }
323
324    /// Get parent symbol (for InSymbol)
325    #[inline]
326    pub fn parent(&self, id: SymbolId) -> Option<SymbolId> {
327        self.parents.get(id).copied()
328    }
329
330    // ========== Metadata Mutation ==========
331
332    /// Set/update span
333    pub fn set_span(&mut self, id: SymbolId, span: FileSpan) -> Result<(), InvalidSymbolId> {
334        if !self.contains(id) {
335            return Err(InvalidSymbolId(id));
336        }
337        self.spans.insert(id, span);
338        Ok(())
339    }
340
341    /// Set/update visibility
342    pub fn set_visibility(&mut self, id: SymbolId, vis: Visibility) -> Result<(), InvalidSymbolId> {
343        if !self.contains(id) {
344            return Err(InvalidSymbolId(id));
345        }
346        self.visibility.insert(id, vis);
347        Ok(())
348    }
349
350    /// Set/update kind
351    pub fn set_kind(&mut self, id: SymbolId, kind: SymbolKind) -> Result<(), InvalidSymbolId> {
352        if !self.contains(id) {
353            return Err(InvalidSymbolId(id));
354        }
355        self.kinds.insert(id, kind);
356        Ok(())
357    }
358
359    /// Remove a symbol from the registry
360    ///
361    /// Returns the path of the removed symbol, or None if the ID was invalid.
362    /// Also removes the persistent UUID mapping if present.
363    pub fn remove(&mut self, id: SymbolId) -> Option<SymbolPath> {
364        let path = self.id_to_path.remove(id)?;
365        self.path_to_id.remove(&path);
366        self.kinds.remove(id);
367        self.spans.remove(id);
368        self.visibility.remove(id);
369        self.parents.remove(id);
370
371        // Clean up re-exports pointing to this symbol
372        if let Some(aliases) = self.re_exports.remove(id) {
373            for info in aliases {
374                self.alias_to_canonical.remove(&info.alias_path);
375            }
376        }
377
378        // Clean up UUID mapping
379        if let Some(uuid) = self.id_to_uuid.remove(id) {
380            self.uuid_to_id.remove(&uuid);
381        }
382
383        Some(path)
384    }
385
386    /// Rename a symbol
387    ///
388    /// Returns the old path on success.
389    pub fn rename(
390        &mut self,
391        id: SymbolId,
392        new_path: SymbolPath,
393    ) -> Result<SymbolPath, RenameError> {
394        // Validate ID exists
395        let old_path = self
396            .id_to_path
397            .get(id)
398            .ok_or(RenameError::InvalidId(id))?
399            .clone();
400
401        // Check new path doesn't conflict
402        if self.path_to_id.contains_key(&new_path) {
403            return Err(RenameError::PathExists(Box::new(new_path)));
404        }
405
406        // Update mappings
407        self.path_to_id.remove(&old_path);
408        self.path_to_id.insert(new_path.clone(), id);
409        self.id_to_path[id] = new_path;
410
411        Ok(old_path)
412    }
413
414    // ========== Name-based Lookup ==========
415
416    /// Find all symbols with the given name (last segment of path).
417    ///
418    /// Also includes canonical symbols that have re-export aliases matching the name.
419    pub fn find_by_name(&self, name: &str) -> Vec<SymbolId> {
420        let mut results: Vec<SymbolId> = self
421            .id_to_path
422            .iter()
423            .filter(|(_, path)| path.name() == name)
424            .map(|(id, _)| id)
425            .collect();
426
427        // Include canonical IDs reachable via alias paths matching this name
428        for (alias_path, &canonical_id) in &self.alias_to_canonical {
429            if alias_path.name() == name && !results.contains(&canonical_id) {
430                results.push(canonical_id);
431            }
432        }
433
434        results
435    }
436
437    /// Find a single symbol by name (returns first match)
438    pub fn lookup_by_name(&self, name: &str) -> Option<SymbolId> {
439        self.id_to_path
440            .iter()
441            .find(|(_, path)| path.name() == name)
442            .map(|(id, _)| id)
443    }
444
445    // ========== Re-export Management ==========
446
447    /// Register a re-export
448    pub fn register_reexport(
449        &mut self,
450        canonical_id: SymbolId,
451        alias_path: SymbolPath,
452        origin_file: WorkspaceFilePath,
453    ) -> Result<(), InvalidSymbolId> {
454        if !self.contains(canonical_id) {
455            return Err(InvalidSymbolId(canonical_id));
456        }
457
458        // Add alias → canonical mapping
459        self.alias_to_canonical
460            .insert(alias_path.clone(), canonical_id);
461
462        // Add canonical → aliases mapping
463        let info = ReExportInfo {
464            alias_path,
465            origin_file,
466        };
467        self.re_exports
468            .entry(canonical_id)
469            .expect("canonical_id was validated by self.contains() above")
470            .or_default()
471            .push(info);
472
473        Ok(())
474    }
475
476    /// Unregister a re-export
477    pub fn unregister_reexport(
478        &mut self,
479        alias_path: &SymbolPath,
480    ) -> Result<(), UnregisterReexportError> {
481        let canonical_id = self
482            .alias_to_canonical
483            .remove(alias_path)
484            .ok_or(UnregisterReexportError::NotFound)?;
485
486        if let Some(aliases) = self.re_exports.get_mut(canonical_id) {
487            aliases.retain(|info| &info.alias_path != alias_path);
488            if aliases.is_empty() {
489                self.re_exports.remove(canonical_id);
490            }
491        }
492
493        Ok(())
494    }
495
496    /// Get re-exports for a symbol
497    pub fn re_exports(&self, id: SymbolId) -> Option<&[ReExportInfo]> {
498        self.re_exports.get(id).map(|v| v.as_slice())
499    }
500
501    // ========== Persistent Identity (UUID) ==========
502
503    /// Register a symbol with persistent UUID
504    ///
505    /// This method assigns a stable UUID to the symbol that survives across sessions.
506    /// Use this for symbols that need to be tracked through renames or serialized.
507    ///
508    /// # Arguments
509    /// - `path`: Symbol path
510    /// - `kind`: Symbol kind
511    /// - `uuid`: `Some(uuid)` when restoring from serialized data, `None` to generate new
512    ///
513    /// # Returns
514    /// - `Ok((SymbolId, Uuid))`: The runtime ID and persistent UUID
515    /// - `Err(RegistrationError)`: Registration failed
516    ///
517    /// # Example
518    /// ```ignore
519    /// // New symbol (generates UUID)
520    /// let (id, uuid) = registry.register_persistent(path, kind, None)?;
521    ///
522    /// // Restore from serialized data
523    /// let (id, uuid) = registry.register_persistent(path, kind, Some(saved_uuid))?;
524    /// ```
525    pub fn register_persistent(
526        &mut self,
527        path: SymbolPath,
528        kind: SymbolKind,
529        uuid: Option<Uuid>,
530    ) -> Result<(SymbolId, Uuid), RegistrationError> {
531        // Standard registration
532        let id = self.register(path, kind)?;
533
534        // Check if UUID already assigned (idempotent for existing symbols)
535        if let Some(&existing_uuid) = self.id_to_uuid.get(id) {
536            // If caller provided a UUID, verify it matches
537            if let Some(provided) = uuid {
538                if provided != existing_uuid {
539                    // UUID conflict - this is a programming error
540                    // The same symbol shouldn't have different UUIDs
541                    return Err(RegistrationError::UuidConflict {
542                        id,
543                        existing: existing_uuid,
544                        provided,
545                    });
546                }
547            }
548            return Ok((id, existing_uuid));
549        }
550
551        // Assign UUID (use provided or generate new)
552        let final_uuid = uuid.unwrap_or_else(Uuid::new_v4);
553        self.id_to_uuid.insert(id, final_uuid);
554        self.uuid_to_id.insert(final_uuid, id);
555
556        Ok((id, final_uuid))
557    }
558
559    /// Assign a persistent UUID to an existing symbol
560    ///
561    /// Use this to add persistence to a symbol that was registered without UUID.
562    ///
563    /// # Returns
564    /// - `Ok(Uuid)`: The assigned UUID (existing or new)
565    /// - `Err(InvalidSymbolId)`: Symbol doesn't exist
566    pub fn assign_uuid(
567        &mut self,
568        id: SymbolId,
569        uuid: Option<Uuid>,
570    ) -> Result<Uuid, InvalidSymbolId> {
571        if !self.contains(id) {
572            return Err(InvalidSymbolId(id));
573        }
574
575        // Return existing UUID if already assigned
576        if let Some(&existing) = self.id_to_uuid.get(id) {
577            return Ok(existing);
578        }
579
580        let final_uuid = uuid.unwrap_or_else(Uuid::new_v4);
581        self.id_to_uuid.insert(id, final_uuid);
582        self.uuid_to_id.insert(final_uuid, id);
583
584        Ok(final_uuid)
585    }
586
587    /// Get persistent UUID for a symbol (O(1))
588    ///
589    /// Returns `None` if the symbol was not registered with persistence.
590    #[inline]
591    pub fn uuid(&self, id: SymbolId) -> Option<Uuid> {
592        self.id_to_uuid.get(id).copied()
593    }
594
595    /// Lookup symbol by persistent UUID (O(1))
596    ///
597    /// Use this when deserializing references from saved data.
598    #[inline]
599    pub fn lookup_by_uuid(&self, uuid: Uuid) -> Option<SymbolId> {
600        self.uuid_to_id.get(&uuid).copied()
601    }
602
603    /// Check if a symbol has a persistent UUID
604    #[inline]
605    pub fn has_uuid(&self, id: SymbolId) -> bool {
606        self.id_to_uuid.contains_key(id)
607    }
608
609    /// Get all symbols with persistent UUIDs
610    pub fn iter_persistent(&self) -> impl Iterator<Item = (SymbolId, Uuid)> + '_ {
611        self.id_to_uuid.iter().map(|(id, &uuid)| (id, uuid))
612    }
613
614    /// Get count of symbols with persistent UUIDs
615    pub fn persistent_count(&self) -> usize {
616        self.id_to_uuid.len()
617    }
618
619    /// Preload UUID mappings from a previous session.
620    ///
621    /// Call this before registering symbols to restore persistent UUIDs.
622    /// When `register()` is called, it will use preloaded UUIDs instead of
623    /// generating new ones.
624    ///
625    /// # Arguments
626    /// * `mappings` - HashMap of SymbolPath → UUID from previous session
627    ///
628    /// # Example
629    /// ```ignore
630    /// // Load from file
631    /// let mappings: HashMap<SymbolPath, Uuid> = load_from_file(path)?;
632    /// registry.preload_uuid_mapping(mappings);
633    ///
634    /// // Now register symbols - they'll get their previous UUIDs
635    /// registry.register(path, kind)?;
636    /// ```
637    pub fn preload_uuid_mapping(&mut self, mappings: HashMap<SymbolPath, Uuid>) {
638        self.preloaded_uuids = mappings;
639    }
640
641    /// Export current UUID mappings for persistence.
642    ///
643    /// Returns a HashMap of SymbolPath → UUID that can be serialized
644    /// and loaded in a future session via `preload_uuid_mapping()`.
645    ///
646    /// # Example
647    /// ```ignore
648    /// // Save to file
649    /// let mappings = registry.export_uuid_mapping();
650    /// save_to_file(path, &mappings)?;
651    /// ```
652    pub fn export_uuid_mapping(&self) -> HashMap<SymbolPath, Uuid> {
653        self.id_to_path
654            .iter()
655            .filter_map(|(id, path)| self.id_to_uuid.get(id).map(|&uuid| (path.clone(), uuid)))
656            .collect()
657    }
658
659    /// Export UUID mappings as strings for JSON serialization.
660    ///
661    /// Converts SymbolPath and Uuid to String for easy JSON storage.
662    pub fn export_uuid_mapping_strings(&self) -> HashMap<String, String> {
663        self.export_uuid_mapping()
664            .into_iter()
665            .map(|(path, uuid)| (path.to_string(), uuid.to_string()))
666            .collect()
667    }
668
669    /// Preload UUID mappings from string format (for JSON deserialization).
670    ///
671    /// Parses string keys/values back to SymbolPath/Uuid.
672    /// Invalid entries are silently skipped.
673    pub fn preload_uuid_mapping_strings(&mut self, mappings: HashMap<String, String>) {
674        let parsed: HashMap<SymbolPath, Uuid> = mappings
675            .into_iter()
676            .filter_map(|(path_str, uuid_str)| {
677                let path = SymbolPath::parse(&path_str).ok()?;
678                let uuid = Uuid::parse_str(&uuid_str).ok()?;
679                Some((path, uuid))
680            })
681            .collect();
682        self.preloaded_uuids = parsed;
683    }
684
685    // ========== Iteration ==========
686
687    /// Iterate over all symbols
688    pub fn iter(&self) -> impl Iterator<Item = (SymbolId, &SymbolPath)> {
689        self.id_to_path.iter()
690    }
691
692    /// Iterate over symbols of a specific kind
693    pub fn iter_by_kind(&self, kind: SymbolKind) -> impl Iterator<Item = SymbolId> + '_ {
694        self.kinds
695            .iter()
696            .filter(move |(_, &k)| k == kind)
697            .map(|(id, _)| id)
698    }
699
700    /// Iterate over symbols in a specific crate
701    pub fn iter_in_crate<'a>(&'a self, crate_name: &'a str) -> impl Iterator<Item = SymbolId> + 'a {
702        self.id_to_path
703            .iter()
704            .filter(move |(_, path)| path.crate_name() == crate_name)
705            .map(|(id, _)| id)
706    }
707
708    // ========== Statistics ==========
709
710    /// Get number of registered symbols
711    pub fn len(&self) -> usize {
712        self.id_to_path.len()
713    }
714
715    /// Check if registry is empty
716    pub fn is_empty(&self) -> bool {
717        self.id_to_path.is_empty()
718    }
719
720    /// Get memory usage statistics
721    pub fn memory_stats(&self) -> MemoryStats {
722        MemoryStats {
723            symbol_count: self.id_to_path.len(),
724            // Rough estimates
725            estimated_bytes: self.id_to_path.len() * 64 // Very rough estimate
726                + self.path_to_id.len() * 80
727                + self.kinds.len() * 8
728                + self.spans.len() * 48
729                + self.visibility.len() * 16,
730        }
731    }
732}
733
734impl Default for SymbolRegistry {
735    fn default() -> Self {
736        Self::new()
737    }
738}
739
740/// Memory usage statistics
741#[derive(Debug, Clone)]
742pub struct MemoryStats {
743    /// Number of symbols currently held in the registry.
744    pub symbol_count: usize,
745    /// Rough byte estimate of the registry's in-memory footprint
746    /// (path strings + slot map overhead); not a precise allocator total.
747    pub estimated_bytes: usize,
748}
749
750#[cfg(test)]
751mod tests {
752    use super::*;
753
754    fn make_path(s: &str) -> SymbolPath {
755        SymbolPath::parse(s).unwrap()
756    }
757
758    #[test]
759    fn test_register_and_lookup() {
760        let mut registry = SymbolRegistry::new();
761
762        let path = make_path("my_crate::MyStruct");
763        let id = registry.register(path.clone(), SymbolKind::Struct).unwrap();
764
765        assert!(registry.contains(id));
766        assert_eq!(registry.lookup(&path), Some(id));
767        assert_eq!(registry.resolve(id), Some(&path));
768        assert_eq!(registry.kind(id), Some(SymbolKind::Struct));
769    }
770
771    #[test]
772    fn test_register_duplicate() {
773        let mut registry = SymbolRegistry::new();
774
775        let path = make_path("my_crate::MyStruct");
776        let id1 = registry.register(path.clone(), SymbolKind::Struct).unwrap();
777        let id2 = registry.register(path.clone(), SymbolKind::Struct).unwrap();
778
779        // Same ID returned
780        assert_eq!(id1, id2);
781    }
782
783    #[test]
784    fn test_register_conflicting_kind() {
785        let mut registry = SymbolRegistry::new();
786
787        let path = make_path("my_crate::MyStruct");
788        registry.register(path.clone(), SymbolKind::Struct).unwrap();
789
790        // Attempt to register with different kind
791        let result = registry.register(path, SymbolKind::Enum);
792        assert!(matches!(
793            result,
794            Err(RegistrationError::ConflictingKind { .. })
795        ));
796    }
797
798    #[test]
799    fn test_register_var() {
800        let mut registry = SymbolRegistry::new();
801
802        // Register parent
803        let fn_path = make_path("my_crate::my_fn");
804        let fn_id = registry.register(fn_path, SymbolKind::Function).unwrap();
805
806        // Register variable
807        let var_id = registry
808            .register_var(fn_id, VarScope::Local, "result", SymbolKind::Variable)
809            .unwrap();
810
811        // Verify
812        let var_path = registry.resolve(var_id).unwrap();
813        assert_eq!(var_path.to_string(), "my_crate::my_fn::$var::result");
814        assert_eq!(registry.parent(var_id), Some(fn_id));
815    }
816
817    #[test]
818    fn test_reexport() {
819        let mut registry = SymbolRegistry::new();
820
821        // Register canonical
822        let canonical_path = make_path("std::collections::hash_map::HashMap");
823        let canonical_id = registry
824            .register(canonical_path, SymbolKind::Struct)
825            .unwrap();
826
827        // Register re-export
828        let alias_path = make_path("std::collections::HashMap");
829        let origin = WorkspaceFilePath::new_for_test("src/collections/mod.rs", "/std", "std");
830
831        registry
832            .register_reexport(canonical_id, alias_path.clone(), origin)
833            .unwrap();
834
835        // Both paths resolve to same ID
836        assert_eq!(registry.lookup(&alias_path), Some(canonical_id));
837    }
838
839    #[test]
840    fn test_iter_by_kind() {
841        let mut registry = SymbolRegistry::new();
842
843        registry
844            .register(make_path("my_crate::Struct1"), SymbolKind::Struct)
845            .unwrap();
846        registry
847            .register(make_path("my_crate::Struct2"), SymbolKind::Struct)
848            .unwrap();
849        registry
850            .register(make_path("my_crate::func"), SymbolKind::Function)
851            .unwrap();
852
853        let structs: Vec<_> = registry.iter_by_kind(SymbolKind::Struct).collect();
854        assert_eq!(structs.len(), 2);
855
856        let funcs: Vec<_> = registry.iter_by_kind(SymbolKind::Function).collect();
857        assert_eq!(funcs.len(), 1);
858    }
859
860    // ========== UUID Persistence Tests ==========
861
862    #[test]
863    fn test_register_persistent_new() {
864        let mut registry = SymbolRegistry::new();
865
866        let path = make_path("my_crate::MyStruct");
867        let (id, uuid) = registry
868            .register_persistent(path.clone(), SymbolKind::Struct, None)
869            .unwrap();
870
871        // Verify bidirectional mapping
872        assert_eq!(registry.uuid(id), Some(uuid));
873        assert_eq!(registry.lookup_by_uuid(uuid), Some(id));
874        assert!(registry.has_uuid(id));
875    }
876
877    #[test]
878    fn test_register_persistent_returns_auto_uuid() {
879        let mut registry = SymbolRegistry::new();
880
881        // With auto-UUID on register(), register_persistent() without
882        // explicit UUID just returns the auto-generated one
883        let path = make_path("my_crate::MyStruct");
884        let (id, uuid) = registry
885            .register_persistent(path, SymbolKind::Struct, None)
886            .unwrap();
887
888        // UUID was auto-assigned by register()
889        assert_eq!(registry.uuid(id), Some(uuid));
890        assert_eq!(registry.lookup_by_uuid(uuid), Some(id));
891    }
892
893    #[test]
894    fn test_register_persistent_idempotent() {
895        let mut registry = SymbolRegistry::new();
896
897        let path = make_path("my_crate::MyStruct");
898
899        // First registration
900        let (id1, uuid1) = registry
901            .register_persistent(path.clone(), SymbolKind::Struct, None)
902            .unwrap();
903
904        // Second registration (same path)
905        let (id2, uuid2) = registry
906            .register_persistent(path, SymbolKind::Struct, None)
907            .unwrap();
908
909        // Should return same ID and UUID
910        assert_eq!(id1, id2);
911        assert_eq!(uuid1, uuid2);
912    }
913
914    #[test]
915    fn test_uuid_survives_rename() {
916        let mut registry = SymbolRegistry::new();
917
918        // Register with UUID
919        let old_path = make_path("my_crate::OldName");
920        let (id, uuid) = registry
921            .register_persistent(old_path, SymbolKind::Struct, None)
922            .unwrap();
923
924        // Rename
925        let new_path = make_path("my_crate::NewName");
926        registry.rename(id, new_path.clone()).unwrap();
927
928        // UUID should be preserved (same entity, different name)
929        assert_eq!(registry.uuid(id), Some(uuid));
930        assert_eq!(registry.lookup_by_uuid(uuid), Some(id));
931
932        // Path should be updated
933        assert_eq!(registry.resolve(id), Some(&new_path));
934    }
935
936    #[test]
937    fn test_uuid_removed_on_delete() {
938        let mut registry = SymbolRegistry::new();
939
940        let path = make_path("my_crate::MyStruct");
941        let (id, uuid) = registry
942            .register_persistent(path, SymbolKind::Struct, None)
943            .unwrap();
944
945        // Remove symbol
946        registry.remove(id);
947
948        // UUID mapping should be cleaned up
949        assert!(registry.uuid(id).is_none());
950        assert!(registry.lookup_by_uuid(uuid).is_none());
951    }
952
953    #[test]
954    fn test_auto_uuid_on_register() {
955        let mut registry = SymbolRegistry::new();
956
957        // All symbols now get UUID automatically on register
958        let path = make_path("my_crate::MyStruct");
959        let id = registry.register(path, SymbolKind::Struct).unwrap();
960
961        // UUID is assigned automatically
962        assert!(registry.has_uuid(id));
963        let uuid = registry.uuid(id).unwrap();
964        assert_eq!(registry.lookup_by_uuid(uuid), Some(id));
965
966        // assign_uuid returns existing UUID (idempotent)
967        let uuid2 = registry.assign_uuid(id, None).unwrap();
968        assert_eq!(uuid, uuid2);
969    }
970
971    #[test]
972    fn test_iter_persistent() {
973        let mut registry = SymbolRegistry::new();
974
975        // All symbols now get UUID automatically
976        let id0 = registry
977            .register(make_path("my_crate::Symbol0"), SymbolKind::Struct)
978            .unwrap();
979        let id1 = registry
980            .register(make_path("my_crate::Symbol1"), SymbolKind::Struct)
981            .unwrap();
982        let id2 = registry
983            .register(make_path("my_crate::Symbol2"), SymbolKind::Enum)
984            .unwrap();
985
986        // All 3 symbols should have UUIDs
987        let persistent: Vec<_> = registry.iter_persistent().collect();
988        assert_eq!(persistent.len(), 3);
989        assert_eq!(registry.persistent_count(), 3);
990
991        // Verify each symbol has a UUID
992        assert!(registry.has_uuid(id0));
993        assert!(registry.has_uuid(id1));
994        assert!(registry.has_uuid(id2));
995    }
996
997    #[test]
998    fn test_uuid_conflict_error() {
999        let mut registry = SymbolRegistry::new();
1000
1001        let path = make_path("my_crate::MyStruct");
1002        let different_uuid = Uuid::new_v4();
1003
1004        // First, register normally (auto-assigns UUID)
1005        let id = registry.register(path.clone(), SymbolKind::Struct).unwrap();
1006        let auto_uuid = registry.uuid(id).unwrap();
1007
1008        // Try to register_persistent with a different UUID - should conflict
1009        let result = registry.register_persistent(path, SymbolKind::Struct, Some(different_uuid));
1010
1011        assert!(matches!(
1012            result,
1013            Err(RegistrationError::UuidConflict { .. })
1014        ));
1015
1016        // Verify original UUID is unchanged
1017        assert_eq!(registry.uuid(id), Some(auto_uuid));
1018    }
1019
1020    // ========== Re-export + find_by_name Tests ==========
1021
1022    #[test]
1023    fn test_find_by_name_includes_aliases() {
1024        let mut registry = SymbolRegistry::new();
1025
1026        // Register canonical symbol: parking_lot::Mutex
1027        let canonical_path = make_path("parking_lot::Mutex");
1028        let canonical_id = registry
1029            .register(canonical_path, SymbolKind::Struct)
1030            .unwrap();
1031
1032        // Register re-export alias: tokio::sync::Mutex → parking_lot::Mutex
1033        let alias_path = make_path("tokio::sync::Mutex");
1034        let file_path = WorkspaceFilePath::new_for_test("src/sync/mod.rs", "/tmp/tokio", "tokio");
1035        registry
1036            .register_reexport(canonical_id, alias_path, file_path)
1037            .unwrap();
1038
1039        // find_by_name("Mutex") should find the canonical via alias
1040        let results = registry.find_by_name("Mutex");
1041        assert_eq!(results.len(), 1);
1042        assert_eq!(results[0], canonical_id);
1043    }
1044
1045    #[test]
1046    fn test_find_by_name_no_duplicate_with_alias() {
1047        let mut registry = SymbolRegistry::new();
1048
1049        // Canonical symbol has name "Mutex" in its path
1050        let canonical_path = make_path("parking_lot::Mutex");
1051        let canonical_id = registry
1052            .register(canonical_path, SymbolKind::Struct)
1053            .unwrap();
1054
1055        // Alias also ends in "Mutex"
1056        let alias_path = make_path("tokio::sync::Mutex");
1057        let file_path = WorkspaceFilePath::new_for_test("src/sync/mod.rs", "/tmp/tokio", "tokio");
1058        registry
1059            .register_reexport(canonical_id, alias_path, file_path)
1060            .unwrap();
1061
1062        // Should not duplicate: canonical already found via id_to_path
1063        let results = registry.find_by_name("Mutex");
1064        assert_eq!(results.len(), 1);
1065    }
1066
1067    #[test]
1068    fn test_lookup_resolves_alias() {
1069        let mut registry = SymbolRegistry::new();
1070
1071        let canonical_path = make_path("my_crate::inner::Config");
1072        let canonical_id = registry
1073            .register(canonical_path, SymbolKind::Struct)
1074            .unwrap();
1075
1076        let alias_path = make_path("my_crate::Config");
1077        let file_path = WorkspaceFilePath::new_for_test("src/lib.rs", "/tmp/my_crate", "my_crate");
1078        registry
1079            .register_reexport(canonical_id, alias_path.clone(), file_path)
1080            .unwrap();
1081
1082        // lookup with alias path should return canonical ID
1083        assert_eq!(registry.lookup(&alias_path), Some(canonical_id));
1084    }
1085}