Skip to main content

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}