ryo_executor/executor/registry/converter.rs
1//! MutationConverter trait for MutationSpec → Mutation conversion
2//!
3//! Each Converter handles one or more MutationSpec variants.
4//!
5//! # Architecture
6//!
7//! ## Layered Design
8//!
9//! ```text
10//! ┌─────────────────────────────────────────────────────────────────┐
11//! │ Path Resolution Layer (SymbolPath-based) │
12//! │ - MutationSpec uses SymbolPath for targeting │
13//! │ - resolve() uses SymbolRegistry to find target file │
14//! │ - Unified path handling via AnalysisContext │
15//! ├─────────────────────────────────────────────────────────────────┤
16//! │ Fragment Layer (PureFile-based) │
17//! │ - Mutation::apply() operates on PureFile AST │
18//! │ - Name-based search within file (pragmatic choice) │
19//! │ - Fine-grained AST manipulation (statements, expressions) │
20//! └─────────────────────────────────────────────────────────────────┘
21//! ```
22//!
23//! ## Design Decisions
24//!
25//! **Why SymbolPath for resolution, PureFile for application?**
26//!
27//! 1. **Path Resolution**: SymbolPath provides semantic addressing
28//! (`crate::module::Item`) that's independent of file layout.
29//! The SymbolRegistry resolves this to actual file locations.
30//!
31//! 2. **Fragment Operations**: PureFile remains the working unit for
32//! AST manipulation. Many mutations (ReplaceExpr, InsertStatement)
33//! operate at a granularity finer than Symbol-level.
34//!
35//! 3. **Pragmatic Trade-off**: Full Symbol-based mutations would require
36//! modeling all AST details in SymbolRegistry. Instead, we use Symbol
37//! for targeting and PureFile for manipulation.
38//!
39//! ## Execution Flow
40//!
41//! ```text
42//! Wave 0: [Spec_A, Spec_B]
43//! │
44//! ▼ resolve() - parallel OK (Registry lookup only)
45//! [ResolvedMutation { mutation, target_file }, ...]
46//! │
47//! ▼ apply_batch() - sequential (file operations)
48//! PureFile modifications
49//! │
50//! ▼ barrier
51//! Wave 1: [Spec_C] // Can see Wave 0 changes
52//! ```
53//!
54//! ## Conflict Detection (ItemRef)
55//!
56//! - Same Item mutations → different Waves (serialized)
57//! - Different Item mutations → same Wave (parallel resolve, sequential apply)
58//! - Parallelism benefit is in resolve/convert phase (CPU-intensive)
59//! - Apply phase is fast (AST manipulation) and always sequential
60
61use crate::engine::ASTRegApply;
62use crate::executor::spec::MutationSpec;
63use ryo_analysis::{AnalysisContext, CheckError, SymbolPath};
64use ryo_mutations::Mutation;
65use ryo_symbol::{SymbolId, WorkspaceFilePath};
66use thiserror::Error;
67
68/// Error type for conversion operations
69#[derive(Debug, Error)]
70pub enum ConvertError {
71 #[error("Unknown spec kind: {0}")]
72 UnknownSpec(String),
73
74 #[error("Type mismatch in converter: expected {expected}, got {actual}")]
75 TypeMismatch {
76 expected: &'static str,
77 actual: String,
78 },
79
80 #[error("Parse error: {0}")]
81 Parse(String),
82
83 #[error("Target not found: {0}")]
84 TargetNotFound(String),
85
86 #[error("Symbol not found in registry: {0:?}")]
87 SymbolNotFound(SymbolId),
88
89 #[error("Apply error: {0}")]
90 Apply(String),
91
92 #[error("Missing required field '{field}' in {spec_type}")]
93 MissingField {
94 field: &'static str,
95 spec_type: &'static str,
96 },
97
98 #[error("Pre-check failed: {0}")]
99 PreCheck(#[from] CheckError),
100}
101
102/// Result of applying a mutation
103#[derive(Debug, Clone)]
104pub struct ApplyResult {
105 /// Number of changes made
106 pub changes: usize,
107 /// Files that were modified
108 pub affected_files: Vec<WorkspaceFilePath>,
109}
110
111impl ApplyResult {
112 pub fn new(changes: usize, affected_files: Vec<WorkspaceFilePath>) -> Self {
113 Self {
114 changes,
115 affected_files,
116 }
117 }
118
119 pub fn empty() -> Self {
120 Self {
121 changes: 0,
122 affected_files: vec![],
123 }
124 }
125
126 pub fn merge(&mut self, other: ApplyResult) {
127 self.changes += other.changes;
128 self.affected_files.extend(other.affected_files);
129 }
130}
131
132/// A mutation resolved with its target file information.
133///
134/// Created by `MutationConverter::resolve()`. Contains:
135/// - The mutation to apply
136/// - The specific file to apply it to (if known via SymbolId resolution)
137///
138/// When `target_file` is `None`, the mutation will be applied to all files.
139#[derive(Debug)]
140pub struct ResolvedMutation {
141 /// The mutation to apply
142 pub mutation: Box<dyn Mutation>,
143 /// Target file (resolved via SymbolId). None means apply to all files.
144 pub target_file: Option<WorkspaceFilePath>,
145}
146
147impl ResolvedMutation {
148 /// Create a resolved mutation targeting a specific file
149 pub fn with_target(mutation: Box<dyn Mutation>, target_file: WorkspaceFilePath) -> Self {
150 Self {
151 mutation,
152 target_file: Some(target_file),
153 }
154 }
155
156 /// Create a resolved mutation targeting all files
157 pub fn all_files(mutation: Box<dyn Mutation>) -> Self {
158 Self {
159 mutation,
160 target_file: None,
161 }
162 }
163}
164
165/// Trait for converting MutationSpec to Mutation and applying it
166///
167/// Each implementation handles specific MutationSpec variants.
168/// The Registry routes specs to appropriate converters.
169///
170/// # Resolution vs Application
171///
172/// - `resolve()`: Converts spec to mutation and resolves target file via SymbolId
173/// - `convert_and_apply()`: Legacy method that both converts and applies (deprecated pattern)
174///
175/// New code should use `resolve()` + batch application via BlueprintExecutor.
176pub trait MutationConverter: Send + Sync {
177 /// Returns the spec kind(s) this converter handles
178 /// e.g., &["Rename"] or &["AddField", "RemoveField"]
179 fn spec_kinds(&self) -> &'static [&'static str];
180
181 /// Check if this converter can handle the given spec
182 fn can_handle(&self, spec: &MutationSpec) -> bool {
183 self.spec_kinds().contains(&spec.kind_name())
184 }
185
186 /// Convert MutationSpec to execution units (V2 API)
187 ///
188 /// Returns a vector of ASTRegApply mutations that implement the spec.
189 /// One spec may expand to multiple execution units (e.g., AddVariant
190 /// may also add match arms, AddField may update constructors).
191 ///
192 /// # Design
193 ///
194 /// The 1:N mapping (one spec → multiple mutations) enables:
195 /// - Compound operations (AddVariant + AddMatchArm)
196 /// - Cascading updates (field addition → constructor updates)
197 /// - Atomic multi-location changes
198 fn convert_v2(
199 &self,
200 spec: &MutationSpec,
201 ctx: &AnalysisContext,
202 ) -> Result<Vec<Box<dyn ASTRegApply>>, ConvertError>;
203}
204
205/// Resolve a SymbolPath to WorkspaceFilePath via Registry.
206///
207/// Uses FilePathResolver from ryo-symbol for path resolution.
208/// Falls back to file inference if symbol is not in Registry
209/// (e.g., for newly created modules not yet registered).
210///
211/// # Main Symbol Support
212///
213/// Paths prefixed with "main::" (e.g., "main::my_crate::Config") target
214/// binary entry points (main.rs, src/bin/*.rs). The "main::" prefix is
215/// stripped and the corresponding main.rs file is located.
216///
217/// TODO(refactor): Main symbol resolution is a workaround for the current
218/// binary/library separation. Consider:
219/// - Unified symbol registry with binary/lib distinction
220/// - Separate BinarySymbolRegistry
221/// - EntryPoint-aware SymbolPath
222pub fn resolve_file_path_from_symbol(
223 ctx: &AnalysisContext,
224 path: &SymbolPath,
225) -> Result<WorkspaceFilePath, ConvertError> {
226 use ryo_symbol::FilePathResolver;
227
228 // Handle main:: prefixed paths (binary entry points)
229 // TODO(refactor): This is a workaround. Ideally, binary symbols would be
230 // in a separate registry or have a unified symbol system with entry point info.
231 if path.is_main_symbol() {
232 return resolve_main_symbol_file(ctx, path);
233 }
234
235 let symbol_registry = ctx.registry();
236 let resolver = FilePathResolver::new(ctx.workspace_root.to_path_buf());
237
238 // Try Registry lookup first (for existing symbols)
239 if let Some(symbol_id) = symbol_registry.lookup(path) {
240 if let Some(span) = symbol_registry.span(symbol_id) {
241 if ctx.files().contains_key(&span.file) {
242 return Ok(span.file.clone());
243 }
244 }
245 }
246
247 // Fallback: use FilePathResolver inference
248 // This handles newly created files not yet in the Registry
249 // Note: SymbolPath types are unified (ryo_analysis re-exports ryo_symbol::SymbolPath)
250 let candidates = resolver.resolve_candidates(path);
251 for candidate in candidates {
252 if ctx.files().contains_key(&candidate) {
253 return Ok(candidate);
254 }
255 }
256
257 Err(ConvertError::TargetNotFound(format!(
258 "File not found for symbol: {}",
259 path
260 )))
261}
262
263/// Resolve a main:: prefixed symbol path to its binary entry file.
264///
265/// Converts "main::my_crate::Config" to the corresponding main.rs file.
266///
267/// TODO(refactor): This function is a temporary workaround for binary/library
268/// separation. Future improvements could include:
269/// - BinarySymbolRegistry for explicit binary symbol management
270/// - EntryPoint-aware SymbolPath resolution
271/// - Unified registry with binary/lib metadata
272fn resolve_main_symbol_file(
273 ctx: &AnalysisContext,
274 path: &SymbolPath,
275) -> Result<WorkspaceFilePath, ConvertError> {
276 // Get the target crate name from "main::crate_name::..."
277 let target_crate = path.main_target_crate().ok_or_else(|| {
278 ConvertError::TargetNotFound(format!(
279 "Invalid main symbol path (missing crate name): {}",
280 path
281 ))
282 })?;
283
284 // Find main.rs file for this crate
285 for file_path in ctx.files().keys() {
286 if file_path.is_binary_entry() && file_path.crate_name().as_str() == target_crate {
287 return Ok(file_path.clone());
288 }
289 }
290
291 Err(ConvertError::TargetNotFound(format!(
292 "Binary entry file not found for main symbol: {} (crate: {})",
293 path, target_crate
294 )))
295}
296
297/// Resolve an optional SymbolPath to optional WorkspaceFilePath via Registry.
298///
299/// Returns Ok(None) if path is None.
300/// Returns Err if path is Some but resolution fails.
301pub fn opt_resolve_file_path_from_symbol(
302 ctx: &AnalysisContext,
303 path: &Option<SymbolPath>,
304) -> Result<Option<WorkspaceFilePath>, ConvertError> {
305 match path {
306 Some(p) => resolve_file_path_from_symbol(ctx, p).map(Some),
307 None => Ok(None),
308 }
309}