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}