Skip to main content

ryo_analysis/symbol/
update.rs

1//! Registry update operations for delta-based mutation execution.
2//!
3//! # Overview
4//!
5//! During parallel Mutation execution, SymbolRegistry changes are managed as deltas.
6//! Each Mutation shares Registry as read-only and returns changes as `RegistryUpdate`.
7//! All deltas are applied at Tick end.
8//!
9//! # Design
10//!
11//! ```text
12//! 1 Tick flow:
13//! ┌─────────────────────────────────────────────────────┐
14//! │  Mutation A (modify X)  ──┐                         │
15//! │  Mutation B (modify Y)  ──┼─→ Collect deltas → Apply to Registry
16//! │  Mutation C (modify Z)  ──┘                         │
17//! └─────────────────────────────────────────────────────┘
18//! ```
19//!
20//! See `docs/parallel-execution-design.md` for full design details.
21
22use super::{
23    FileSpan, InvalidSymbolId, RegistrationError, RenameError, SymbolId, SymbolKind, SymbolPath,
24    SymbolRegistry, Visibility,
25};
26use serde::Serialize;
27
28/// Registry change request (delta).
29///
30/// Represents a single atomic change to the SymbolRegistry.
31/// Collected during Mutation execution and applied at Tick end.
32///
33/// NOTE: Deserialize is NOT derived because FileSpan contains WorkspaceFilePath
34/// which requires context during deserialization.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
36pub enum RegistryUpdate {
37    /// Add a new symbol.
38    Add {
39        /// Canonical SymbolPath of the new symbol.
40        path: SymbolPath,
41        /// Kind classification (struct / fn / trait / ...).
42        kind: SymbolKind,
43        /// Source-file span where the symbol is declared.
44        span: FileSpan,
45    },
46
47    /// Remove an existing symbol.
48    Remove {
49        /// Target symbol to remove.
50        id: SymbolId,
51    },
52
53    /// Rename a symbol (change its path).
54    Rename {
55        /// Target symbol to rename.
56        id: SymbolId,
57        /// New canonical SymbolPath to assign.
58        new_path: SymbolPath,
59    },
60
61    /// Update symbol's file span (position changed).
62    UpdateSpan {
63        /// Target symbol whose span is being updated.
64        id: SymbolId,
65        /// Replacement file span.
66        new_span: FileSpan,
67    },
68
69    /// Update symbol's visibility.
70    UpdateVisibility {
71        /// Target symbol whose visibility is being updated.
72        id: SymbolId,
73        /// Replacement visibility value.
74        new_visibility: Visibility,
75    },
76
77    /// Update symbol's kind.
78    UpdateKind {
79        /// Target symbol whose kind is being updated.
80        id: SymbolId,
81        /// Replacement kind classification.
82        new_kind: SymbolKind,
83    },
84}
85
86impl RegistryUpdate {
87    /// Get the target SymbolId if this update modifies an existing symbol.
88    ///
89    /// Returns `None` for `Add` operations (no existing symbol).
90    pub fn target_id(&self) -> Option<SymbolId> {
91        match self {
92            RegistryUpdate::Add { .. } => None,
93            RegistryUpdate::Remove { id }
94            | RegistryUpdate::Rename { id, .. }
95            | RegistryUpdate::UpdateSpan { id, .. }
96            | RegistryUpdate::UpdateVisibility { id, .. }
97            | RegistryUpdate::UpdateKind { id, .. } => Some(*id),
98        }
99    }
100
101    /// Check if this update is a destructive operation.
102    ///
103    /// Destructive operations (Remove, Rename) require special handling
104    /// for conflict detection.
105    pub fn is_destructive(&self) -> bool {
106        matches!(
107            self,
108            RegistryUpdate::Remove { .. } | RegistryUpdate::Rename { .. }
109        )
110    }
111}
112
113/// Batch of registry updates from a single Mutation.
114#[derive(Debug, Clone, Default)]
115pub struct RegistryUpdateBatch {
116    updates: Vec<RegistryUpdate>,
117}
118
119impl RegistryUpdateBatch {
120    /// Create an empty batch.
121    pub fn new() -> Self {
122        Self::default()
123    }
124
125    /// Create a batch with pre-allocated capacity.
126    pub fn with_capacity(capacity: usize) -> Self {
127        Self {
128            updates: Vec::with_capacity(capacity),
129        }
130    }
131
132    /// Add an update to the batch.
133    pub fn push(&mut self, update: RegistryUpdate) {
134        self.updates.push(update);
135    }
136
137    /// Add a symbol addition.
138    pub fn add_symbol(&mut self, path: SymbolPath, kind: SymbolKind, span: FileSpan) {
139        self.push(RegistryUpdate::Add { path, kind, span });
140    }
141
142    /// Add a symbol removal.
143    pub fn remove_symbol(&mut self, id: SymbolId) {
144        self.push(RegistryUpdate::Remove { id });
145    }
146
147    /// Add a symbol rename.
148    pub fn rename_symbol(&mut self, id: SymbolId, new_path: SymbolPath) {
149        self.push(RegistryUpdate::Rename { id, new_path });
150    }
151
152    /// Add a span update.
153    pub fn update_span(&mut self, id: SymbolId, new_span: FileSpan) {
154        self.push(RegistryUpdate::UpdateSpan { id, new_span });
155    }
156
157    /// Get the updates.
158    pub fn updates(&self) -> &[RegistryUpdate] {
159        &self.updates
160    }
161
162    /// Consume and return the updates.
163    pub fn into_updates(self) -> Vec<RegistryUpdate> {
164        self.updates
165    }
166
167    /// Check if the batch is empty.
168    pub fn is_empty(&self) -> bool {
169        self.updates.is_empty()
170    }
171
172    /// Get the number of updates.
173    pub fn len(&self) -> usize {
174        self.updates.len()
175    }
176
177    /// Apply all updates to the registry.
178    ///
179    /// Updates are applied in order. If any update fails, the operation
180    /// stops and returns the error. Previously applied updates are NOT
181    /// rolled back (partial application).
182    ///
183    /// # Returns
184    /// - `Ok(applied_count)`: Number of successfully applied updates
185    /// - `Err(ApplyError)`: First error encountered
186    pub fn apply(self, registry: &mut SymbolRegistry) -> Result<usize, ApplyError> {
187        let mut applied = 0;
188
189        for update in self.updates {
190            update.apply(registry)?;
191            applied += 1;
192        }
193
194        Ok(applied)
195    }
196}
197
198/// Error during registry update application.
199#[derive(Debug, thiserror::Error)]
200pub enum ApplyError {
201    /// Symbol registration failed.
202    #[error("registration failed: {0}")]
203    Registration(#[from] RegistrationError),
204
205    /// Invalid symbol ID.
206    #[error("invalid symbol id: {0}")]
207    InvalidId(#[from] InvalidSymbolId),
208
209    /// Rename failed.
210    #[error("rename failed: {0}")]
211    Rename(#[from] RenameError),
212}
213
214impl RegistryUpdate {
215    /// Apply this update to the registry.
216    pub fn apply(self, registry: &mut SymbolRegistry) -> Result<(), ApplyError> {
217        match self {
218            RegistryUpdate::Add { path, kind, span } => {
219                let id = registry.register(path, kind)?;
220                registry.set_span(id, span)?;
221            }
222            RegistryUpdate::Remove { id } => {
223                // remove() returns None if not found, but we treat it as success
224                // (idempotent deletion)
225                let _ = registry.remove(id);
226            }
227            RegistryUpdate::Rename { id, new_path } => {
228                registry.rename(id, new_path)?;
229            }
230            RegistryUpdate::UpdateSpan { id, new_span } => {
231                registry.set_span(id, new_span)?;
232            }
233            RegistryUpdate::UpdateVisibility { id, new_visibility } => {
234                registry.set_visibility(id, new_visibility)?;
235            }
236            RegistryUpdate::UpdateKind { id, new_kind } => {
237                registry.set_kind(id, new_kind)?;
238            }
239        }
240        Ok(())
241    }
242}
243
244impl IntoIterator for RegistryUpdateBatch {
245    type Item = RegistryUpdate;
246    type IntoIter = std::vec::IntoIter<RegistryUpdate>;
247
248    fn into_iter(self) -> Self::IntoIter {
249        self.updates.into_iter()
250    }
251}
252
253impl<'a> IntoIterator for &'a RegistryUpdateBatch {
254    type Item = &'a RegistryUpdate;
255    type IntoIter = std::slice::Iter<'a, RegistryUpdate>;
256
257    fn into_iter(self) -> Self::IntoIter {
258        self.updates.iter()
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use ryo_symbol::WorkspaceFilePath;
266
267    /// Create a test FileSpan.
268    fn test_span() -> FileSpan {
269        FileSpan::new(
270            WorkspaceFilePath::new_for_test("test/file.rs", "/", "test"),
271            0,
272            10,
273        )
274    }
275
276    #[test]
277    fn test_target_id() {
278        use slotmap::KeyData;
279
280        let id = SymbolId::from(KeyData::from_ffi(1));
281        let path = SymbolPath::parse("foo::bar").unwrap();
282
283        // Add has no target
284        let add = RegistryUpdate::Add {
285            path: path.clone(),
286            kind: SymbolKind::Function,
287            span: test_span(),
288        };
289        assert!(add.target_id().is_none());
290
291        // Remove has target
292        let remove = RegistryUpdate::Remove { id };
293        assert_eq!(remove.target_id(), Some(id));
294
295        // Rename has target
296        let rename = RegistryUpdate::Rename { id, new_path: path };
297        assert_eq!(rename.target_id(), Some(id));
298    }
299
300    #[test]
301    fn test_is_destructive() {
302        use slotmap::KeyData;
303
304        let id = SymbolId::from(KeyData::from_ffi(1));
305        let path = SymbolPath::parse("foo::bar").unwrap();
306
307        assert!(!RegistryUpdate::Add {
308            path: path.clone(),
309            kind: SymbolKind::Function,
310            span: test_span(),
311        }
312        .is_destructive());
313
314        assert!(RegistryUpdate::Remove { id }.is_destructive());
315        assert!(RegistryUpdate::Rename { id, new_path: path }.is_destructive());
316
317        assert!(!RegistryUpdate::UpdateSpan {
318            id,
319            new_span: test_span()
320        }
321        .is_destructive());
322    }
323
324    #[test]
325    fn test_batch_builder() {
326        use slotmap::KeyData;
327
328        let id = SymbolId::from(KeyData::from_ffi(1));
329        let path = SymbolPath::parse("foo::bar").unwrap();
330
331        let mut batch = RegistryUpdateBatch::new();
332        assert!(batch.is_empty());
333
334        batch.add_symbol(path.clone(), SymbolKind::Function, test_span());
335        batch.remove_symbol(id);
336
337        assert_eq!(batch.len(), 2);
338        assert!(!batch.is_empty());
339    }
340
341    #[test]
342    fn test_batch_apply_add() {
343        let mut registry = SymbolRegistry::new();
344        let path = SymbolPath::parse("test::NewSymbol").unwrap();
345
346        let mut batch = RegistryUpdateBatch::new();
347        batch.add_symbol(path.clone(), SymbolKind::Function, test_span());
348
349        let applied = batch.apply(&mut registry).unwrap();
350        assert_eq!(applied, 1);
351
352        // Verify symbol was added
353        let id = registry.lookup(&path).expect("symbol should exist");
354        assert_eq!(registry.kind(id), Some(SymbolKind::Function));
355        assert!(registry.span(id).is_some());
356    }
357
358    #[test]
359    fn test_batch_apply_remove() {
360        let mut registry = SymbolRegistry::new();
361        let path = SymbolPath::parse("test::ToRemove").unwrap();
362        let id = registry.register(path.clone(), SymbolKind::Struct).unwrap();
363
364        let mut batch = RegistryUpdateBatch::new();
365        batch.remove_symbol(id);
366
367        let applied = batch.apply(&mut registry).unwrap();
368        assert_eq!(applied, 1);
369
370        // Verify symbol was removed
371        assert!(registry.lookup(&path).is_none());
372        assert!(!registry.contains(id));
373    }
374
375    #[test]
376    fn test_batch_apply_rename() {
377        let mut registry = SymbolRegistry::new();
378        let old_path = SymbolPath::parse("test::OldName").unwrap();
379        let new_path = SymbolPath::parse("test::NewName").unwrap();
380        let id = registry
381            .register(old_path.clone(), SymbolKind::Struct)
382            .unwrap();
383
384        let mut batch = RegistryUpdateBatch::new();
385        batch.rename_symbol(id, new_path.clone());
386
387        let applied = batch.apply(&mut registry).unwrap();
388        assert_eq!(applied, 1);
389
390        // Verify rename
391        assert!(registry.lookup(&old_path).is_none());
392        assert_eq!(registry.lookup(&new_path), Some(id));
393    }
394
395    #[test]
396    fn test_batch_apply_multiple() {
397        use super::Visibility;
398
399        let mut registry = SymbolRegistry::new();
400        let path1 = SymbolPath::parse("test::First").unwrap();
401        let path2 = SymbolPath::parse("test::Second").unwrap();
402
403        let id1 = registry
404            .register(path1.clone(), SymbolKind::Struct)
405            .unwrap();
406
407        let mut batch = RegistryUpdateBatch::new();
408        batch.add_symbol(path2.clone(), SymbolKind::Function, test_span());
409        batch.push(RegistryUpdate::UpdateVisibility {
410            id: id1,
411            new_visibility: Visibility::Public,
412        });
413
414        let applied = batch.apply(&mut registry).unwrap();
415        assert_eq!(applied, 2);
416
417        // Verify both operations
418        assert!(registry.lookup(&path2).is_some());
419        assert_eq!(registry.visibility(id1), Some(&Visibility::Public));
420    }
421
422    #[test]
423    fn test_batch_apply_empty() {
424        let mut registry = SymbolRegistry::new();
425        let batch = RegistryUpdateBatch::new();
426
427        let applied = batch.apply(&mut registry).unwrap();
428        assert_eq!(applied, 0);
429    }
430}