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 #[error("V2 conversion not supported for this spec kind")]
102 V2NotSupported,
103}
104
105/// Result of applying a mutation
106#[derive(Debug, Clone)]
107pub struct ApplyResult {
108 /// Number of changes made
109 pub changes: usize,
110 /// Files that were modified
111 pub affected_files: Vec<WorkspaceFilePath>,
112}
113
114impl ApplyResult {
115 pub fn new(changes: usize, affected_files: Vec<WorkspaceFilePath>) -> Self {
116 Self {
117 changes,
118 affected_files,
119 }
120 }
121
122 pub fn empty() -> Self {
123 Self {
124 changes: 0,
125 affected_files: vec![],
126 }
127 }
128
129 pub fn merge(&mut self, other: ApplyResult) {
130 self.changes += other.changes;
131 self.affected_files.extend(other.affected_files);
132 }
133}
134
135/// A mutation resolved with its target file information.
136///
137/// Created by `MutationConverter::resolve()`. Contains:
138/// - The mutation to apply
139/// - The specific file to apply it to (if known via SymbolId resolution)
140///
141/// When `target_file` is `None`, the mutation will be applied to all files.
142#[derive(Debug)]
143pub struct ResolvedMutation {
144 /// The mutation to apply
145 pub mutation: Box<dyn Mutation>,
146 /// Target file (resolved via SymbolId). None means apply to all files.
147 pub target_file: Option<WorkspaceFilePath>,
148}
149
150impl ResolvedMutation {
151 /// Create a resolved mutation targeting a specific file
152 pub fn with_target(mutation: Box<dyn Mutation>, target_file: WorkspaceFilePath) -> Self {
153 Self {
154 mutation,
155 target_file: Some(target_file),
156 }
157 }
158
159 /// Create a resolved mutation targeting all files
160 pub fn all_files(mutation: Box<dyn Mutation>) -> Self {
161 Self {
162 mutation,
163 target_file: None,
164 }
165 }
166}
167
168/// Trait for converting MutationSpec to Mutation and applying it
169///
170/// Each implementation handles specific MutationSpec variants.
171/// The Registry routes specs to appropriate converters.
172///
173/// # Resolution vs Application
174///
175/// - `resolve()`: Converts spec to mutation and resolves target file via SymbolId
176/// - `convert_and_apply()`: Legacy method that both converts and applies (deprecated pattern)
177///
178/// New code should use `resolve()` + batch application via BlueprintExecutor.
179pub trait MutationConverter: Send + Sync {
180 /// Returns the spec kind(s) this converter handles
181 /// e.g., &["Rename"] or &["AddField", "RemoveField"]
182 fn spec_kinds(&self) -> &'static [&'static str];
183
184 /// Check if this converter can handle the given spec
185 fn can_handle(&self, spec: &MutationSpec) -> bool {
186 self.spec_kinds().contains(&spec.kind_name())
187 }
188
189 /// Convert a MutationSpec to a Mutation (DEPRECATED - use convert_v2 instead)
190 ///
191 /// This method is deprecated and will be removed in a future version.
192 /// Use `convert_v2()` which returns `ASTRegApply` mutations instead.
193 #[deprecated(
194 since = "0.1.0",
195 note = "Returns Box<dyn Mutation> for legacy apply(&mut PureFile). Use convert_v2() for ASTRegApply."
196 )]
197 fn convert(&self, _spec: &MutationSpec) -> Result<Box<dyn Mutation>, ConvertError> {
198 Err(ConvertError::V2NotSupported)
199 }
200
201 /// Convert MutationSpec to execution units (V2 API)
202 ///
203 /// Returns a vector of ASTRegApply mutations that implement the spec.
204 /// One spec may expand to multiple execution units (e.g., AddVariant
205 /// may also add match arms, AddField may update constructors).
206 ///
207 /// # Design
208 ///
209 /// The 1:N mapping (one spec → multiple mutations) enables:
210 /// - Compound operations (AddVariant + AddMatchArm)
211 /// - Cascading updates (field addition → constructor updates)
212 /// - Atomic multi-location changes
213 ///
214 /// # Default Implementation
215 ///
216 /// Returns `Err(ConvertError::V2NotSupported)` to allow gradual migration.
217 /// Converters should override this to provide V2 support.
218 fn convert_v2(
219 &self,
220 _spec: &MutationSpec,
221 _ctx: &AnalysisContext,
222 ) -> Result<Vec<Box<dyn ASTRegApply>>, ConvertError> {
223 Err(ConvertError::V2NotSupported)
224 }
225}
226
227/// Resolve a SymbolPath to WorkspaceFilePath via Registry.
228///
229/// Uses FilePathResolver from ryo-symbol for path resolution.
230/// Falls back to file inference if symbol is not in Registry
231/// (e.g., for newly created modules not yet registered).
232///
233/// # Main Symbol Support
234///
235/// Paths prefixed with "main::" (e.g., "main::my_crate::Config") target
236/// binary entry points (main.rs, src/bin/*.rs). The "main::" prefix is
237/// stripped and the corresponding main.rs file is located.
238///
239/// TODO(refactor): Main symbol resolution is a workaround for the current
240/// binary/library separation. Consider:
241/// - Unified symbol registry with binary/lib distinction
242/// - Separate BinarySymbolRegistry
243/// - EntryPoint-aware SymbolPath
244pub fn resolve_file_path_from_symbol(
245 ctx: &AnalysisContext,
246 path: &SymbolPath,
247) -> Result<WorkspaceFilePath, ConvertError> {
248 use ryo_symbol::FilePathResolver;
249
250 // Handle main:: prefixed paths (binary entry points)
251 // TODO(refactor): This is a workaround. Ideally, binary symbols would be
252 // in a separate registry or have a unified symbol system with entry point info.
253 if path.is_main_symbol() {
254 return resolve_main_symbol_file(ctx, path);
255 }
256
257 let symbol_registry = ctx.registry();
258 let resolver = FilePathResolver::new(ctx.workspace_root.to_path_buf());
259
260 // Try Registry lookup first (for existing symbols)
261 if let Some(symbol_id) = symbol_registry.lookup(path) {
262 if let Some(span) = symbol_registry.span(symbol_id) {
263 if ctx.files().contains_key(&span.file) {
264 return Ok(span.file.clone());
265 }
266 }
267 }
268
269 // Fallback: use FilePathResolver inference
270 // This handles newly created files not yet in the Registry
271 // Note: SymbolPath types are unified (ryo_analysis re-exports ryo_symbol::SymbolPath)
272 let candidates = resolver.resolve_candidates(path);
273 for candidate in candidates {
274 if ctx.files().contains_key(&candidate) {
275 return Ok(candidate);
276 }
277 }
278
279 Err(ConvertError::TargetNotFound(format!(
280 "File not found for symbol: {}",
281 path
282 )))
283}
284
285/// Resolve a main:: prefixed symbol path to its binary entry file.
286///
287/// Converts "main::my_crate::Config" to the corresponding main.rs file.
288///
289/// TODO(refactor): This function is a temporary workaround for binary/library
290/// separation. Future improvements could include:
291/// - BinarySymbolRegistry for explicit binary symbol management
292/// - EntryPoint-aware SymbolPath resolution
293/// - Unified registry with binary/lib metadata
294fn resolve_main_symbol_file(
295 ctx: &AnalysisContext,
296 path: &SymbolPath,
297) -> Result<WorkspaceFilePath, ConvertError> {
298 // Get the target crate name from "main::crate_name::..."
299 let target_crate = path.main_target_crate().ok_or_else(|| {
300 ConvertError::TargetNotFound(format!(
301 "Invalid main symbol path (missing crate name): {}",
302 path
303 ))
304 })?;
305
306 // Find main.rs file for this crate
307 for file_path in ctx.files().keys() {
308 if file_path.is_binary_entry() && file_path.crate_name().as_str() == target_crate {
309 return Ok(file_path.clone());
310 }
311 }
312
313 Err(ConvertError::TargetNotFound(format!(
314 "Binary entry file not found for main symbol: {} (crate: {})",
315 path, target_crate
316 )))
317}
318
319/// Resolve an optional SymbolPath to optional WorkspaceFilePath via Registry.
320///
321/// Returns Ok(None) if path is None.
322/// Returns Err if path is Some but resolution fails.
323pub fn opt_resolve_file_path_from_symbol(
324 ctx: &AnalysisContext,
325 path: &Option<SymbolPath>,
326) -> Result<Option<WorkspaceFilePath>, ConvertError> {
327 match path {
328 Some(p) => resolve_file_path_from_symbol(ctx, p).map(Some),
329 None => Ok(None),
330 }
331}