ryo-executor 0.1.0

[experimental] Mutation execution engine for RYO - parallel execution, conflict detection, workspace management
Documentation
//! MutationConverter trait for MutationSpec → Mutation conversion
//!
//! Each Converter handles one or more MutationSpec variants.
//!
//! # Architecture
//!
//! ## Layered Design
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────────┐
//! │  Path Resolution Layer (SymbolPath-based)                       │
//! │  - MutationSpec uses SymbolPath for targeting                   │
//! │  - resolve() uses SymbolRegistry to find target file            │
//! │  - Unified path handling via AnalysisContext                    │
//! ├─────────────────────────────────────────────────────────────────┤
//! │  Fragment Layer (PureFile-based)                                │
//! │  - Mutation::apply() operates on PureFile AST                   │
//! │  - Name-based search within file (pragmatic choice)             │
//! │  - Fine-grained AST manipulation (statements, expressions)      │
//! └─────────────────────────────────────────────────────────────────┘
//! ```
//!
//! ## Design Decisions
//!
//! **Why SymbolPath for resolution, PureFile for application?**
//!
//! 1. **Path Resolution**: SymbolPath provides semantic addressing
//!    (`crate::module::Item`) that's independent of file layout.
//!    The SymbolRegistry resolves this to actual file locations.
//!
//! 2. **Fragment Operations**: PureFile remains the working unit for
//!    AST manipulation. Many mutations (ReplaceExpr, InsertStatement)
//!    operate at a granularity finer than Symbol-level.
//!
//! 3. **Pragmatic Trade-off**: Full Symbol-based mutations would require
//!    modeling all AST details in SymbolRegistry. Instead, we use Symbol
//!    for targeting and PureFile for manipulation.
//!
//! ## Execution Flow
//!
//! ```text
//! Wave 0: [Spec_A, Spec_B]
//!//!     ▼ resolve() - parallel OK (Registry lookup only)
//! [ResolvedMutation { mutation, target_file }, ...]
//!//!     ▼ apply_batch() - sequential (file operations)
//! PureFile modifications
//!//!     ▼ barrier
//! Wave 1: [Spec_C]  // Can see Wave 0 changes
//! ```
//!
//! ## Conflict Detection (ItemRef)
//!
//! - Same Item mutations → different Waves (serialized)
//! - Different Item mutations → same Wave (parallel resolve, sequential apply)
//! - Parallelism benefit is in resolve/convert phase (CPU-intensive)
//! - Apply phase is fast (AST manipulation) and always sequential

use crate::engine::ASTRegApply;
use crate::executor::spec::MutationSpec;
use ryo_analysis::{AnalysisContext, CheckError, SymbolPath};
use ryo_mutations::Mutation;
use ryo_symbol::{SymbolId, WorkspaceFilePath};
use thiserror::Error;

/// Error type for conversion operations
#[derive(Debug, Error)]
pub enum ConvertError {
    #[error("Unknown spec kind: {0}")]
    UnknownSpec(String),

    #[error("Type mismatch in converter: expected {expected}, got {actual}")]
    TypeMismatch {
        expected: &'static str,
        actual: String,
    },

    #[error("Parse error: {0}")]
    Parse(String),

    #[error("Target not found: {0}")]
    TargetNotFound(String),

    #[error("Symbol not found in registry: {0:?}")]
    SymbolNotFound(SymbolId),

    #[error("Apply error: {0}")]
    Apply(String),

    #[error("Missing required field '{field}' in {spec_type}")]
    MissingField {
        field: &'static str,
        spec_type: &'static str,
    },

    #[error("Pre-check failed: {0}")]
    PreCheck(#[from] CheckError),

    #[error("V2 conversion not supported for this spec kind")]
    V2NotSupported,
}

/// Result of applying a mutation
#[derive(Debug, Clone)]
pub struct ApplyResult {
    /// Number of changes made
    pub changes: usize,
    /// Files that were modified
    pub affected_files: Vec<WorkspaceFilePath>,
}

impl ApplyResult {
    pub fn new(changes: usize, affected_files: Vec<WorkspaceFilePath>) -> Self {
        Self {
            changes,
            affected_files,
        }
    }

    pub fn empty() -> Self {
        Self {
            changes: 0,
            affected_files: vec![],
        }
    }

    pub fn merge(&mut self, other: ApplyResult) {
        self.changes += other.changes;
        self.affected_files.extend(other.affected_files);
    }
}

/// A mutation resolved with its target file information.
///
/// Created by `MutationConverter::resolve()`. Contains:
/// - The mutation to apply
/// - The specific file to apply it to (if known via SymbolId resolution)
///
/// When `target_file` is `None`, the mutation will be applied to all files.
#[derive(Debug)]
pub struct ResolvedMutation {
    /// The mutation to apply
    pub mutation: Box<dyn Mutation>,
    /// Target file (resolved via SymbolId). None means apply to all files.
    pub target_file: Option<WorkspaceFilePath>,
}

impl ResolvedMutation {
    /// Create a resolved mutation targeting a specific file
    pub fn with_target(mutation: Box<dyn Mutation>, target_file: WorkspaceFilePath) -> Self {
        Self {
            mutation,
            target_file: Some(target_file),
        }
    }

    /// Create a resolved mutation targeting all files
    pub fn all_files(mutation: Box<dyn Mutation>) -> Self {
        Self {
            mutation,
            target_file: None,
        }
    }
}

/// Trait for converting MutationSpec to Mutation and applying it
///
/// Each implementation handles specific MutationSpec variants.
/// The Registry routes specs to appropriate converters.
///
/// # Resolution vs Application
///
/// - `resolve()`: Converts spec to mutation and resolves target file via SymbolId
/// - `convert_and_apply()`: Legacy method that both converts and applies (deprecated pattern)
///
/// New code should use `resolve()` + batch application via BlueprintExecutor.
pub trait MutationConverter: Send + Sync {
    /// Returns the spec kind(s) this converter handles
    /// e.g., &["Rename"] or &["AddField", "RemoveField"]
    fn spec_kinds(&self) -> &'static [&'static str];

    /// Check if this converter can handle the given spec
    fn can_handle(&self, spec: &MutationSpec) -> bool {
        self.spec_kinds().contains(&spec.kind_name())
    }

    /// Convert a MutationSpec to a Mutation (DEPRECATED - use convert_v2 instead)
    ///
    /// This method is deprecated and will be removed in a future version.
    /// Use `convert_v2()` which returns `ASTRegApply` mutations instead.
    #[deprecated(
        since = "0.1.0",
        note = "Returns Box<dyn Mutation> for legacy apply(&mut PureFile). Use convert_v2() for ASTRegApply."
    )]
    fn convert(&self, _spec: &MutationSpec) -> Result<Box<dyn Mutation>, ConvertError> {
        Err(ConvertError::V2NotSupported)
    }

    /// Convert MutationSpec to execution units (V2 API)
    ///
    /// Returns a vector of ASTRegApply mutations that implement the spec.
    /// One spec may expand to multiple execution units (e.g., AddVariant
    /// may also add match arms, AddField may update constructors).
    ///
    /// # Design
    ///
    /// The 1:N mapping (one spec → multiple mutations) enables:
    /// - Compound operations (AddVariant + AddMatchArm)
    /// - Cascading updates (field addition → constructor updates)
    /// - Atomic multi-location changes
    ///
    /// # Default Implementation
    ///
    /// Returns `Err(ConvertError::V2NotSupported)` to allow gradual migration.
    /// Converters should override this to provide V2 support.
    fn convert_v2(
        &self,
        _spec: &MutationSpec,
        _ctx: &AnalysisContext,
    ) -> Result<Vec<Box<dyn ASTRegApply>>, ConvertError> {
        Err(ConvertError::V2NotSupported)
    }
}

/// Resolve a SymbolPath to WorkspaceFilePath via Registry.
///
/// Uses FilePathResolver from ryo-symbol for path resolution.
/// Falls back to file inference if symbol is not in Registry
/// (e.g., for newly created modules not yet registered).
///
/// # Main Symbol Support
///
/// Paths prefixed with "main::" (e.g., "main::my_crate::Config") target
/// binary entry points (main.rs, src/bin/*.rs). The "main::" prefix is
/// stripped and the corresponding main.rs file is located.
///
/// TODO(refactor): Main symbol resolution is a workaround for the current
/// binary/library separation. Consider:
/// - Unified symbol registry with binary/lib distinction
/// - Separate BinarySymbolRegistry
/// - EntryPoint-aware SymbolPath
pub fn resolve_file_path_from_symbol(
    ctx: &AnalysisContext,
    path: &SymbolPath,
) -> Result<WorkspaceFilePath, ConvertError> {
    use ryo_symbol::FilePathResolver;

    // Handle main:: prefixed paths (binary entry points)
    // TODO(refactor): This is a workaround. Ideally, binary symbols would be
    // in a separate registry or have a unified symbol system with entry point info.
    if path.is_main_symbol() {
        return resolve_main_symbol_file(ctx, path);
    }

    let symbol_registry = ctx.registry();
    let resolver = FilePathResolver::new(ctx.workspace_root.to_path_buf());

    // Try Registry lookup first (for existing symbols)
    if let Some(symbol_id) = symbol_registry.lookup(path) {
        if let Some(span) = symbol_registry.span(symbol_id) {
            if ctx.files().contains_key(&span.file) {
                return Ok(span.file.clone());
            }
        }
    }

    // Fallback: use FilePathResolver inference
    // This handles newly created files not yet in the Registry
    // Note: SymbolPath types are unified (ryo_analysis re-exports ryo_symbol::SymbolPath)
    let candidates = resolver.resolve_candidates(path);
    for candidate in candidates {
        if ctx.files().contains_key(&candidate) {
            return Ok(candidate);
        }
    }

    Err(ConvertError::TargetNotFound(format!(
        "File not found for symbol: {}",
        path
    )))
}

/// Resolve a main:: prefixed symbol path to its binary entry file.
///
/// Converts "main::my_crate::Config" to the corresponding main.rs file.
///
/// TODO(refactor): This function is a temporary workaround for binary/library
/// separation. Future improvements could include:
/// - BinarySymbolRegistry for explicit binary symbol management
/// - EntryPoint-aware SymbolPath resolution
/// - Unified registry with binary/lib metadata
fn resolve_main_symbol_file(
    ctx: &AnalysisContext,
    path: &SymbolPath,
) -> Result<WorkspaceFilePath, ConvertError> {
    // Get the target crate name from "main::crate_name::..."
    let target_crate = path.main_target_crate().ok_or_else(|| {
        ConvertError::TargetNotFound(format!(
            "Invalid main symbol path (missing crate name): {}",
            path
        ))
    })?;

    // Find main.rs file for this crate
    for file_path in ctx.files().keys() {
        if file_path.is_binary_entry() && file_path.crate_name().as_str() == target_crate {
            return Ok(file_path.clone());
        }
    }

    Err(ConvertError::TargetNotFound(format!(
        "Binary entry file not found for main symbol: {} (crate: {})",
        path, target_crate
    )))
}

/// Resolve an optional SymbolPath to optional WorkspaceFilePath via Registry.
///
/// Returns Ok(None) if path is None.
/// Returns Err if path is Some but resolution fails.
pub fn opt_resolve_file_path_from_symbol(
    ctx: &AnalysisContext,
    path: &Option<SymbolPath>,
) -> Result<Option<WorkspaceFilePath>, ConvertError> {
    match path {
        Some(p) => resolve_file_path_from_symbol(ctx, p).map(Some),
        None => Ok(None),
    }
}