Skip to main content

ryo_executor/engine/
ast_mutation_engine.rs

1//! AST Mutation Engine - Core execution without file I/O
2//!
3//! Executes mutations against ASTRegistry and SymbolRegistry only.
4//! No PureFile or file system access during execution.
5
6use ryo_analysis::{
7    ASTRegistry, AnalysisContext, SymbolId, SymbolKind, SymbolPath, SymbolRegistry,
8};
9use ryo_mutations::MutationResult;
10use ryo_source::pure::PureItem;
11
12use super::ast_reg_apply::ASTRegApply;
13use super::events::{ModificationType, MutationEvent};
14
15/// Error when resolving a symbol
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ResolveError {
18    /// Symbol not found (by ID or name)
19    NotFound,
20    /// Multiple symbols match the name - use full path to disambiguate
21    /// (only occurs with name-based lookup, never with ID lookup)
22    Ambiguous,
23}
24
25/// Result of mutation execution
26#[derive(Debug, Clone)]
27pub struct ExecutionResult {
28    /// Mutation result (changes count, etc.)
29    pub result: MutationResult,
30    /// Events emitted during execution
31    pub events: Vec<MutationEvent>,
32}
33
34impl ExecutionResult {
35    /// Create a new execution result
36    pub fn new(result: MutationResult, events: Vec<MutationEvent>) -> Self {
37        Self { result, events }
38    }
39
40    /// Create a result indicating no changes
41    pub fn no_change() -> Self {
42        Self {
43            result: MutationResult {
44                mutation_type: String::new(),
45                changes: 0,
46                description: String::new(),
47            },
48            events: vec![],
49        }
50    }
51
52    /// Check if any changes were made
53    pub fn has_changes(&self) -> bool {
54        self.result.changes > 0
55    }
56}
57
58/// AST-centric mutation execution engine
59///
60/// Executes mutations against registries only, with no file I/O.
61///
62/// # Invariants
63///
64/// - NO file I/O during execution
65/// - NO PureFile access (only ASTRegistry)
66/// - ALL state changes via Registry APIs
67///
68/// # Usage
69///
70/// ```ignore
71/// let mutation = AddFieldMutation::new("MyStruct", "new_field", "i32");
72/// let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
73///
74/// for event in &result.events {
75///     println!("{}", event);
76/// }
77/// ```
78pub struct ASTMutationEngine;
79
80impl ASTMutationEngine {
81    /// Execute a mutation that implements ASTRegApply
82    ///
83    /// This is the primary execution path for registry-based mutations.
84    /// Uses `ASTRegApply::apply_to_registry` for actual execution.
85    ///
86    /// # Type Parameters
87    ///
88    /// * `M` - Mutation type that implements both `Mutation` and `ASTRegApply`
89    ///
90    /// # Example
91    ///
92    /// ```ignore
93    /// use ryo_executor::ASTMutationEngine;
94    ///
95    /// let mutation = AddFieldMutation::new("MyStruct", "field", "i32");
96    /// let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
97    /// ```
98    pub fn execute_ast_reg<M: ASTRegApply>(
99        mutation: &M,
100        ctx: &mut AnalysisContext,
101    ) -> ExecutionResult {
102        let mut mutation_ctx = ASTMutationContext::new(&mut ctx.ast_registry, &mut ctx.registry);
103
104        let result = mutation.apply_to_registry(&mut mutation_ctx);
105        let events = mutation_ctx.into_events();
106
107        ExecutionResult::new(result, events)
108    }
109
110    /// Execute multiple ASTRegApply mutations in sequence
111    pub fn execute_ast_reg_batch<M: ASTRegApply>(
112        mutations: &[&M],
113        ctx: &mut AnalysisContext,
114    ) -> ExecutionResult {
115        let mut total_changes = 0;
116        let mut all_events = Vec::new();
117
118        for mutation in mutations {
119            let result = Self::execute_ast_reg(*mutation, ctx);
120            total_changes += result.result.changes;
121            all_events.extend(result.events);
122        }
123
124        ExecutionResult::new(
125            MutationResult {
126                mutation_type: "ast_reg_batch".to_string(),
127                changes: total_changes,
128                description: format!("{} mutations executed", mutations.len()),
129            },
130            all_events,
131        )
132    }
133
134    /// Execute a boxed ASTRegApply mutation (dynamic dispatch)
135    ///
136    /// This is the primary execution path for V2 conversion.
137    /// Accepts `Box<dyn ASTRegApply>` from `MutationConverter::convert_v2()`.
138    ///
139    /// # Example
140    ///
141    /// ```ignore
142    /// let mutations = converter.convert_v2(&spec, &ctx)?;
143    /// for mutation in mutations {
144    ///     let result = ASTMutationEngine::execute_ast_reg_dyn(mutation.as_ref(), &mut ctx);
145    /// }
146    /// ```
147    pub fn execute_ast_reg_dyn(
148        mutation: &dyn ASTRegApply,
149        ctx: &mut AnalysisContext,
150    ) -> ExecutionResult {
151        let mut mutation_ctx = ASTMutationContext::new(&mut ctx.ast_registry, &mut ctx.registry);
152
153        let result = mutation.apply_to_registry(&mut mutation_ctx);
154        let events = mutation_ctx.into_events();
155
156        ExecutionResult::new(result, events)
157    }
158
159    /// Execute multiple boxed ASTRegApply mutations in sequence (dynamic dispatch)
160    ///
161    /// This is the batch execution path for V2 conversion.
162    /// Accepts `Vec<Box<dyn ASTRegApply>>` from `MutationConverter::convert_v2()`.
163    ///
164    /// # Example
165    ///
166    /// ```ignore
167    /// let mutations = converter.convert_v2(&spec, &ctx)?;
168    /// let result = ASTMutationEngine::execute_ast_reg_batch_dyn(mutations, &mut ctx);
169    /// ```
170    pub fn execute_ast_reg_batch_dyn(
171        mutations: Vec<Box<dyn ASTRegApply>>,
172        ctx: &mut AnalysisContext,
173    ) -> ExecutionResult {
174        let mutation_count = mutations.len();
175        let mut total_changes = 0;
176        let mut all_events = Vec::new();
177
178        for mutation in mutations {
179            let result = Self::execute_ast_reg_dyn(mutation.as_ref(), ctx);
180            total_changes += result.result.changes;
181            all_events.extend(result.events);
182        }
183
184        ExecutionResult::new(
185            MutationResult {
186                mutation_type: "ast_reg_batch_v2".to_string(),
187                changes: total_changes,
188                description: format!("{} V2 mutations executed", mutation_count),
189            },
190            all_events,
191        )
192    }
193}
194
195/// Context passed to `ASTRegApply::apply_to_registry`
196///
197/// Provides access to ASTRegistry, SymbolRegistry, and event emission.
198///
199/// # Binary Entry (main.rs) Support
200///
201/// Binary entry symbols (main.rs, src/bin/*.rs) are registered in the unified
202/// `items` map like library symbols. Use `WorkspaceFilePath::is_binary_entry()`
203/// to identify binary files when needed for file output.
204pub struct ASTMutationContext<'a> {
205    /// AST storage (complete PureItem per symbol)
206    pub ast_registry: &'a mut ASTRegistry,
207    /// Symbol metadata (path ↔ id, kind, span, etc.)
208    pub symbol_registry: &'a mut SymbolRegistry,
209    /// Events collected during execution
210    events: Vec<MutationEvent>,
211}
212
213impl<'a> ASTMutationContext<'a> {
214    /// Create a new mutation context
215    pub fn new(ast_registry: &'a mut ASTRegistry, symbol_registry: &'a mut SymbolRegistry) -> Self {
216        Self {
217            ast_registry,
218            symbol_registry,
219            events: Vec::new(),
220        }
221    }
222
223    /// Consume context and return collected events
224    pub fn into_events(self) -> Vec<MutationEvent> {
225        self.events
226    }
227
228    // ========================================================================
229    // Event emission
230    // ========================================================================
231
232    /// Emit an event (for logging/tracking)
233    pub fn emit(&mut self, event: MutationEvent) {
234        self.events.push(event);
235    }
236
237    /// Emit symbol added event
238    pub fn emit_added(&mut self, path: SymbolPath, kind: SymbolKind) {
239        self.emit(MutationEvent::SymbolAdded { path, kind });
240    }
241
242    /// Emit symbol removed event
243    pub fn emit_removed(&mut self, path: SymbolPath) {
244        self.emit(MutationEvent::SymbolRemoved { path });
245    }
246
247    /// Emit symbol modified event
248    pub fn emit_modified(&mut self, id: SymbolId, modification: ModificationType) {
249        // CRITICAL FIX: Sync Impl blocks to module_items
250        // ImplBlocks are stored in both `items` (for direct access via get_mut)
251        // and `module_items` (for file generation). When get_mut modifies an Impl,
252        // we must sync the changes to module_items.
253        let item_clone = self.ast_registry.get(id).cloned();
254        if let Some(item) = item_clone {
255            if matches!(item, ryo_source::pure::PureItem::Impl(_)) {
256                // Find parent module
257                if let Some(path) = self.symbol_registry.path(id) {
258                    if let Some(parent_path) = path.parent() {
259                        if let Some(parent_id) = self.symbol_registry.lookup(&parent_path) {
260                            // Get or create module_items
261                            if let Some(module_items) =
262                                self.ast_registry.get_module_items_mut(parent_id)
263                            {
264                                // Find and replace the Impl block in module_items
265                                if let Some(pos) = module_items.iter().position(|i| {
266                                    if let ryo_source::pure::PureItem::Impl(impl_block) = i {
267                                        if let ryo_source::pure::PureItem::Impl(ref modified_impl) =
268                                            item
269                                        {
270                                            impl_block.self_ty == modified_impl.self_ty
271                                                && impl_block.trait_ == modified_impl.trait_
272                                        } else {
273                                            false
274                                        }
275                                    } else {
276                                        false
277                                    }
278                                }) {
279                                    module_items[pos] = item;
280                                }
281                            }
282                        }
283                    }
284                }
285            }
286        }
287
288        self.emit(MutationEvent::SymbolModified { id, modification });
289    }
290
291    /// Emit symbol renamed event
292    pub fn emit_renamed(&mut self, old_path: SymbolPath, new_path: SymbolPath) {
293        self.emit(MutationEvent::SymbolRenamed {
294            old_path,
295            new_path: Box::new(new_path),
296        });
297    }
298
299    // ========================================================================
300    // AST access
301    // ========================================================================
302
303    /// Get AST for a symbol (immutable)
304    pub fn get_ast(&self, id: SymbolId) -> Option<&PureItem> {
305        self.ast_registry.get(id)
306    }
307
308    /// Get AST for a symbol (mutable)
309    pub fn get_ast_mut(&mut self, id: SymbolId) -> Option<&mut PureItem> {
310        self.ast_registry.get_mut(id)
311    }
312
313    /// Set AST for a symbol
314    pub fn set_ast(&mut self, id: SymbolId, item: PureItem) {
315        self.ast_registry.set(id, item);
316    }
317
318    /// Check if symbol has AST
319    pub fn has_ast(&self, id: SymbolId) -> bool {
320        self.ast_registry.contains(id)
321    }
322
323    // ========================================================================
324    // Symbol lookup
325    // ========================================================================
326
327    /// Lookup symbol by path
328    pub fn lookup(&self, path: &SymbolPath) -> Option<SymbolId> {
329        self.symbol_registry.lookup(path)
330    }
331
332    /// Get symbol path by ID
333    pub fn path(&self, id: SymbolId) -> Option<&SymbolPath> {
334        self.symbol_registry.path(id)
335    }
336
337    /// Get symbol kind by ID
338    pub fn kind(&self, id: SymbolId) -> Option<SymbolKind> {
339        self.symbol_registry.kind(id)
340    }
341
342    /// Resolve symbol ID by name, supporting both simple name and full path.
343    ///
344    /// - If `name` contains "::", performs full path match (always unique)
345    /// - Otherwise, matches by last segment (symbol name) only
346    ///
347    /// # Errors
348    /// - `ResolveError::NotFound` - no matching symbol
349    /// - `ResolveError::Ambiguous` - multiple symbols match (use full path)
350    ///
351    /// # Example
352    /// ```ignore
353    /// // Find struct by simple name (fails if multiple "User" exist)
354    /// let id = ctx.resolve_symbol_by_name("User", &[SymbolKind::Struct])?;
355    ///
356    /// // Find by full path (always unambiguous)
357    /// let id = ctx.resolve_symbol_by_name("crate::user::UserStatus", &[SymbolKind::Enum])?;
358    /// ```
359    pub fn resolve_symbol_by_name(
360        &self,
361        name: &str,
362        kinds: &[SymbolKind],
363    ) -> Result<SymbolId, ResolveError> {
364        let mut found: Option<SymbolId> = None;
365
366        for (id, path) in self.symbol_registry.iter() {
367            // Check kind matches
368            let kind_matches = self
369                .symbol_registry
370                .kind(id)
371                .map(|k| kinds.contains(&k))
372                .unwrap_or(false);
373            if !kind_matches {
374                continue;
375            }
376
377            // Check name matches
378            let name_matches = if name.contains("::") {
379                path.to_string() == name
380            } else {
381                path.name() == name
382            };
383            if !name_matches {
384                continue;
385            }
386
387            // Found a match
388            if found.is_some() {
389                // Second match - ambiguous
390                return Err(ResolveError::Ambiguous);
391            }
392            found = Some(id);
393        }
394
395        found.ok_or(ResolveError::NotFound)
396    }
397
398    // ========================================================================
399    // Symbol mutation (with automatic event emission)
400    // ========================================================================
401
402    /// Register new symbol (emits SymbolAdded event)
403    ///
404    /// Returns None if registration fails (e.g., duplicate path).
405    pub fn register(&mut self, path: SymbolPath, kind: SymbolKind) -> Option<SymbolId> {
406        match self.symbol_registry.register(path.clone(), kind) {
407            Ok(id) => {
408                self.emit_added(path, kind);
409                Some(id)
410            }
411            Err(_) => None,
412        }
413    }
414
415    /// Register new symbol with AST (emits SymbolAdded event)
416    ///
417    /// Returns None if registration fails.
418    pub fn register_with_ast(
419        &mut self,
420        path: SymbolPath,
421        kind: SymbolKind,
422        ast: PureItem,
423    ) -> Option<SymbolId> {
424        let id = self.register(path, kind)?;
425        self.set_ast(id, ast);
426        Some(id)
427    }
428
429    /// Remove symbol (emits SymbolRemoved event)
430    ///
431    /// This removes the symbol from:
432    /// 1. SymbolRegistry (path → id mapping)
433    /// 2. ASTRegistry items (id → PureItem mapping)
434    /// 3. Parent module's module_items list (for file generation)
435    pub fn remove_symbol(&mut self, id: SymbolId) -> Option<PureItem> {
436        if let Some(path) = self.symbol_registry.path(id).cloned() {
437            let name = path.name().to_string();
438
439            // Remove from parent module's module_items
440            if let Some(parent_path) = path.parent() {
441                if let Some(parent_id) = self.symbol_registry.lookup(&parent_path) {
442                    if let Some(items) = self.ast_registry.get_module_items_mut(parent_id) {
443                        items.retain(|item| !item_matches_name(item, &name));
444                    }
445                }
446            }
447
448            self.symbol_registry.remove(id);
449            let ast = self.ast_registry.remove(id);
450            self.emit_removed(path);
451            ast
452        } else {
453            None
454        }
455    }
456
457    /// Rename symbol (emits SymbolRenamed event)
458    pub fn rename_symbol(&mut self, id: SymbolId, new_path: SymbolPath) -> Result<(), String> {
459        if let Some(old_path) = self.symbol_registry.path(id).cloned() {
460            self.symbol_registry
461                .rename(id, new_path.clone())
462                .map_err(|e| format!("{:?}", e))?;
463            self.emit_renamed(old_path, new_path);
464            Ok(())
465        } else {
466            Err("Symbol not found".to_string())
467        }
468    }
469}
470
471/// Check if a PureItem matches the given name.
472///
473/// Used by `remove_symbol` to filter out items from parent's module_items.
474fn item_matches_name(item: &PureItem, name: &str) -> bool {
475    match item {
476        PureItem::Fn(f) => f.name == name,
477        PureItem::Struct(s) => s.name == name,
478        PureItem::Enum(e) => e.name == name,
479        PureItem::Const(c) => c.name == name,
480        PureItem::Static(s) => s.name == name,
481        PureItem::Type(t) => t.name == name,
482        PureItem::Trait(t) => t.name == name,
483        PureItem::Mod(m) => m.name == name,
484        PureItem::Impl(i) => {
485            // Impl blocks use special naming: "<impl Type>" or "<impl Trait for Type>"
486            let impl_name = if let Some(ref trait_name) = i.trait_ {
487                format!("<impl {} for {}>", trait_name, i.self_ty)
488            } else {
489                format!("<impl {}>", i.self_ty)
490            };
491            impl_name == name
492        }
493        // Use statements don't have names in the symbol registry sense
494        PureItem::Use(_) | PureItem::Macro(_) | PureItem::Other(_) => false,
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    // TODO: Add tests once apply_v2 is implemented on mutations
501}