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    #[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}