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        // SAFETY: ASTMutationContext borrows `ast_registry` and `registry`
103        // mutably. We need a *shared* borrow of `code_graph` for the same
104        // lifetime, which the borrow checker would reject because
105        // `AnalysisContext` owns all three fields. Split the borrow up front
106        // by going through raw pointers, then immediately re-create safe
107        // references from disjoint fields. This is sound: the three fields
108        // are disjoint and the resulting references do not alias.
109        let code_graph_ptr: *const ryo_analysis::CodeGraphV2 = &ctx.code_graph;
110        let mut mutation_ctx = ASTMutationContext::new(&mut ctx.ast_registry, &mut ctx.registry)
111            .with_code_graph(unsafe { &*code_graph_ptr });
112
113        let result = mutation.apply_to_registry(&mut mutation_ctx);
114        let events = mutation_ctx.into_events();
115
116        ExecutionResult::new(result, events)
117    }
118
119    /// Execute multiple ASTRegApply mutations in sequence
120    pub fn execute_ast_reg_batch<M: ASTRegApply>(
121        mutations: &[&M],
122        ctx: &mut AnalysisContext,
123    ) -> ExecutionResult {
124        let mut total_changes = 0;
125        let mut all_events = Vec::new();
126
127        for mutation in mutations {
128            let result = Self::execute_ast_reg(*mutation, ctx);
129            total_changes += result.result.changes;
130            all_events.extend(result.events);
131        }
132
133        ExecutionResult::new(
134            MutationResult {
135                mutation_type: "ast_reg_batch".to_string(),
136                changes: total_changes,
137                description: format!("{} mutations executed", mutations.len()),
138            },
139            all_events,
140        )
141    }
142
143    /// Execute a boxed ASTRegApply mutation (dynamic dispatch)
144    ///
145    /// This is the primary execution path for V2 conversion.
146    /// Accepts `Box<dyn ASTRegApply>` from `MutationConverter::convert_v2()`.
147    ///
148    /// # Example
149    ///
150    /// ```ignore
151    /// let mutations = converter.convert_v2(&spec, &ctx)?;
152    /// for mutation in mutations {
153    ///     let result = ASTMutationEngine::execute_ast_reg_dyn(mutation.as_ref(), &mut ctx);
154    /// }
155    /// ```
156    pub fn execute_ast_reg_dyn(
157        mutation: &dyn ASTRegApply,
158        ctx: &mut AnalysisContext,
159    ) -> ExecutionResult {
160        // SAFETY: ASTMutationContext borrows `ast_registry` and `registry`
161        // mutably. We need a *shared* borrow of `code_graph` for the same
162        // lifetime, which the borrow checker would reject because
163        // `AnalysisContext` owns all three fields. Split the borrow up front
164        // by going through raw pointers, then immediately re-create safe
165        // references from disjoint fields. This is sound: the three fields
166        // are disjoint and the resulting references do not alias.
167        let code_graph_ptr: *const ryo_analysis::CodeGraphV2 = &ctx.code_graph;
168        let mut mutation_ctx = ASTMutationContext::new(&mut ctx.ast_registry, &mut ctx.registry)
169            .with_code_graph(unsafe { &*code_graph_ptr });
170
171        let result = mutation.apply_to_registry(&mut mutation_ctx);
172        let events = mutation_ctx.into_events();
173
174        ExecutionResult::new(result, events)
175    }
176
177    /// Execute multiple boxed ASTRegApply mutations in sequence (dynamic dispatch)
178    ///
179    /// This is the batch execution path for V2 conversion.
180    /// Accepts `Vec<Box<dyn ASTRegApply>>` from `MutationConverter::convert_v2()`.
181    ///
182    /// # Example
183    ///
184    /// ```ignore
185    /// let mutations = converter.convert_v2(&spec, &ctx)?;
186    /// let result = ASTMutationEngine::execute_ast_reg_batch_dyn(mutations, &mut ctx);
187    /// ```
188    pub fn execute_ast_reg_batch_dyn(
189        mutations: Vec<Box<dyn ASTRegApply>>,
190        ctx: &mut AnalysisContext,
191    ) -> ExecutionResult {
192        let mutation_count = mutations.len();
193        let mut total_changes = 0;
194        let mut all_events = Vec::new();
195
196        for mutation in mutations {
197            let result = Self::execute_ast_reg_dyn(mutation.as_ref(), ctx);
198            total_changes += result.result.changes;
199            all_events.extend(result.events);
200        }
201
202        ExecutionResult::new(
203            MutationResult {
204                mutation_type: "ast_reg_batch_v2".to_string(),
205                changes: total_changes,
206                description: format!("{} V2 mutations executed", mutation_count),
207            },
208            all_events,
209        )
210    }
211}
212
213/// Context passed to `ASTRegApply::apply_to_registry`
214///
215/// Provides access to ASTRegistry, SymbolRegistry, and event emission.
216///
217/// # Binary Entry (main.rs) Support
218///
219/// Binary entry symbols (main.rs, src/bin/*.rs) are registered in the unified
220/// `items` map like library symbols. Use `WorkspaceFilePath::is_binary_entry()`
221/// to identify binary files when needed for file output.
222pub struct ASTMutationContext<'a> {
223    /// AST storage (complete PureItem per symbol)
224    pub ast_registry: &'a mut ASTRegistry,
225    /// Symbol metadata (path ↔ id, kind, span, etc.)
226    pub symbol_registry: &'a mut SymbolRegistry,
227    /// Optional read-only handle to the analysis CodeGraphV2. Mutations
228    /// that need to discover relationships beyond the (`ast`, `symbol`)
229    /// pair — most notably `ExtractTraitMutation`, which has to inject a
230    /// `use Trait;` into every file that calls one of the methods it just
231    /// moved into a trait — receive the graph through this field.
232    ///
233    /// Held as an `Option<&'a CodeGraphV2>` so legacy callers that
234    /// construct an `ASTMutationContext` without a graph keep working
235    /// (the field is `None` and graph-dependent steps gracefully skip).
236    pub code_graph: Option<&'a ryo_analysis::CodeGraphV2>,
237    /// Events collected during execution
238    events: Vec<MutationEvent>,
239}
240
241impl<'a> ASTMutationContext<'a> {
242    /// Create a new mutation context (without a CodeGraphV2 handle).
243    pub fn new(ast_registry: &'a mut ASTRegistry, symbol_registry: &'a mut SymbolRegistry) -> Self {
244        Self {
245            ast_registry,
246            symbol_registry,
247            code_graph: None,
248            events: Vec::new(),
249        }
250    }
251
252    /// Attach a read-only `CodeGraphV2` handle to the context. Required by
253    /// mutations (e.g. `ExtractTraitMutation`) that need to walk caller
254    /// relationships to update call sites at apply-time.
255    pub fn with_code_graph(mut self, graph: &'a ryo_analysis::CodeGraphV2) -> Self {
256        self.code_graph = Some(graph);
257        self
258    }
259
260    /// Consume context and return collected events
261    pub fn into_events(self) -> Vec<MutationEvent> {
262        self.events
263    }
264
265    // ========================================================================
266    // Event emission
267    // ========================================================================
268
269    /// Emit an event (for logging/tracking)
270    pub fn emit(&mut self, event: MutationEvent) {
271        self.events.push(event);
272    }
273
274    /// Emit symbol added event
275    pub fn emit_added(&mut self, path: SymbolPath, kind: SymbolKind) {
276        self.emit(MutationEvent::SymbolAdded { path, kind });
277    }
278
279    /// Emit symbol removed event
280    pub fn emit_removed(&mut self, path: SymbolPath) {
281        self.emit(MutationEvent::SymbolRemoved { path });
282    }
283
284    /// Emit symbol modified event
285    pub fn emit_modified(&mut self, id: SymbolId, modification: ModificationType) {
286        // CRITICAL FIX: Sync Impl blocks to module_items
287        // ImplBlocks are stored in both `items` (for direct access via get_mut)
288        // and `module_items` (for file generation). When get_mut modifies an Impl,
289        // we must sync the changes to module_items.
290        let item_clone = self.ast_registry.get(id).cloned();
291        if let Some(item) = item_clone {
292            if matches!(item, ryo_source::pure::PureItem::Impl(_)) {
293                // Find parent module
294                if let Some(path) = self.symbol_registry.path(id) {
295                    if let Some(parent_path) = path.parent() {
296                        if let Some(parent_id) = self.symbol_registry.lookup(&parent_path) {
297                            // Get or create module_items
298                            if let Some(module_items) =
299                                self.ast_registry.get_module_items_mut(parent_id)
300                            {
301                                // Find and replace the Impl block in module_items
302                                if let Some(pos) = module_items.iter().position(|i| {
303                                    if let ryo_source::pure::PureItem::Impl(impl_block) = i {
304                                        if let ryo_source::pure::PureItem::Impl(ref modified_impl) =
305                                            item
306                                        {
307                                            impl_block.self_ty == modified_impl.self_ty
308                                                && impl_block.trait_ == modified_impl.trait_
309                                        } else {
310                                            false
311                                        }
312                                    } else {
313                                        false
314                                    }
315                                }) {
316                                    module_items[pos] = item;
317                                }
318                            }
319                        }
320                    }
321                }
322            }
323        }
324
325        self.emit(MutationEvent::SymbolModified { id, modification });
326    }
327
328    /// Emit symbol renamed event
329    pub fn emit_renamed(&mut self, old_path: SymbolPath, new_path: SymbolPath) {
330        self.emit(MutationEvent::SymbolRenamed {
331            old_path,
332            new_path: Box::new(new_path),
333        });
334    }
335
336    // ========================================================================
337    // AST access
338    // ========================================================================
339
340    /// Get AST for a symbol (immutable)
341    pub fn get_ast(&self, id: SymbolId) -> Option<&PureItem> {
342        self.ast_registry.get(id)
343    }
344
345    /// Get AST for a symbol (mutable)
346    pub fn get_ast_mut(&mut self, id: SymbolId) -> Option<&mut PureItem> {
347        self.ast_registry.get_mut(id)
348    }
349
350    /// Set AST for a symbol
351    pub fn set_ast(&mut self, id: SymbolId, item: PureItem) {
352        self.ast_registry.set(id, item);
353    }
354
355    /// Check if symbol has AST
356    pub fn has_ast(&self, id: SymbolId) -> bool {
357        self.ast_registry.contains(id)
358    }
359
360    // ========================================================================
361    // Symbol lookup
362    // ========================================================================
363
364    /// Lookup symbol by path
365    pub fn lookup(&self, path: &SymbolPath) -> Option<SymbolId> {
366        self.symbol_registry.lookup(path)
367    }
368
369    /// Get symbol path by ID
370    pub fn path(&self, id: SymbolId) -> Option<&SymbolPath> {
371        self.symbol_registry.path(id)
372    }
373
374    /// Get symbol kind by ID
375    pub fn kind(&self, id: SymbolId) -> Option<SymbolKind> {
376        self.symbol_registry.kind(id)
377    }
378
379    /// Resolve symbol ID by name, supporting both simple name and full path.
380    ///
381    /// - If `name` contains "::", performs full path match (always unique)
382    /// - Otherwise, matches by last segment (symbol name) only
383    ///
384    /// # Errors
385    /// - `ResolveError::NotFound` - no matching symbol
386    /// - `ResolveError::Ambiguous` - multiple symbols match (use full path)
387    ///
388    /// # Example
389    /// ```ignore
390    /// // Find struct by simple name (fails if multiple "User" exist)
391    /// let id = ctx.resolve_symbol_by_name("User", &[SymbolKind::Struct])?;
392    ///
393    /// // Find by full path (always unambiguous)
394    /// let id = ctx.resolve_symbol_by_name("crate::user::UserStatus", &[SymbolKind::Enum])?;
395    /// ```
396    pub fn resolve_symbol_by_name(
397        &self,
398        name: &str,
399        kinds: &[SymbolKind],
400    ) -> Result<SymbolId, ResolveError> {
401        let mut found: Option<SymbolId> = None;
402
403        for (id, path) in self.symbol_registry.iter() {
404            // Check kind matches
405            let kind_matches = self
406                .symbol_registry
407                .kind(id)
408                .map(|k| kinds.contains(&k))
409                .unwrap_or(false);
410            if !kind_matches {
411                continue;
412            }
413
414            // Check name matches
415            let name_matches = if name.contains("::") {
416                path.to_string() == name
417            } else {
418                path.name() == name
419            };
420            if !name_matches {
421                continue;
422            }
423
424            // Found a match
425            if found.is_some() {
426                // Second match - ambiguous
427                return Err(ResolveError::Ambiguous);
428            }
429            found = Some(id);
430        }
431
432        found.ok_or(ResolveError::NotFound)
433    }
434
435    // ========================================================================
436    // Symbol mutation (with automatic event emission)
437    // ========================================================================
438
439    /// Register new symbol (emits SymbolAdded event)
440    ///
441    /// Returns None if registration fails (e.g., duplicate path).
442    pub fn register(&mut self, path: SymbolPath, kind: SymbolKind) -> Option<SymbolId> {
443        match self.symbol_registry.register(path.clone(), kind) {
444            Ok(id) => {
445                self.emit_added(path, kind);
446                Some(id)
447            }
448            Err(_) => None,
449        }
450    }
451
452    /// Register new symbol with AST (emits SymbolAdded event)
453    ///
454    /// Returns None if registration fails.
455    pub fn register_with_ast(
456        &mut self,
457        path: SymbolPath,
458        kind: SymbolKind,
459        ast: PureItem,
460    ) -> Option<SymbolId> {
461        let id = self.register(path, kind)?;
462        self.set_ast(id, ast);
463        Some(id)
464    }
465
466    /// Remove symbol (emits SymbolRemoved event)
467    ///
468    /// This removes the symbol from:
469    /// 1. SymbolRegistry (path → id mapping)
470    /// 2. ASTRegistry items (id → PureItem mapping)
471    /// 3. Parent module's module_items list (for file generation)
472    pub fn remove_symbol(&mut self, id: SymbolId) -> Option<PureItem> {
473        if let Some(path) = self.symbol_registry.path(id).cloned() {
474            let name = path.name().to_string();
475
476            // Remove from parent module's module_items
477            if let Some(parent_path) = path.parent() {
478                if let Some(parent_id) = self.symbol_registry.lookup(&parent_path) {
479                    if let Some(items) = self.ast_registry.get_module_items_mut(parent_id) {
480                        items.retain(|item| !item_matches_name(item, &name));
481                    }
482                }
483            }
484
485            self.symbol_registry.remove(id);
486            let ast = self.ast_registry.remove(id);
487            self.emit_removed(path);
488            ast
489        } else {
490            None
491        }
492    }
493
494    /// Rename symbol (emits SymbolRenamed event)
495    pub fn rename_symbol(&mut self, id: SymbolId, new_path: SymbolPath) -> Result<(), String> {
496        if let Some(old_path) = self.symbol_registry.path(id).cloned() {
497            self.symbol_registry
498                .rename(id, new_path.clone())
499                .map_err(|e| format!("{:?}", e))?;
500            self.emit_renamed(old_path, new_path);
501            Ok(())
502        } else {
503            Err("Symbol not found".to_string())
504        }
505    }
506}
507
508/// Check if a PureItem matches the given name.
509///
510/// Used by `remove_symbol` to filter out items from parent's module_items.
511fn item_matches_name(item: &PureItem, name: &str) -> bool {
512    match item {
513        PureItem::Fn(f) => f.name == name,
514        PureItem::Struct(s) => s.name == name,
515        PureItem::Enum(e) => e.name == name,
516        PureItem::Const(c) => c.name == name,
517        PureItem::Static(s) => s.name == name,
518        PureItem::Type(t) => t.name == name,
519        PureItem::Trait(t) => t.name == name,
520        PureItem::Mod(m) => m.name == name,
521        PureItem::Impl(i) => {
522            // Impl blocks use special naming: "<impl Type>" or "<impl Trait for Type>"
523            let impl_name = if let Some(ref trait_name) = i.trait_ {
524                format!("<impl {} for {}>", trait_name, i.self_ty)
525            } else {
526                format!("<impl {}>", i.self_ty)
527            };
528            impl_name == name
529        }
530        // Use statements don't have names in the symbol registry sense
531        PureItem::Use(_) | PureItem::Macro(_) | PureItem::Other(_) | PureItem::Verbatim(_) => false,
532    }
533}
534
535#[cfg(test)]
536mod tests {
537    // TODO: Add tests once apply_v2 is implemented on mutations
538}