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}