ryo-executor 0.1.0

[experimental] Mutation execution engine for RYO - parallel execution, conflict detection, workspace management
Documentation
//! Source Pipeline - Type-Safe Generator → Dumper Architecture
//!
//! This module defines the type-safe pipeline for source code generation and output.
//! It ensures that `RegistryGenerator` and file dumping are always used together,
//! preventing the bug where mod declarations are missing due to bypassing the generator.
//!
//! # Architecture Overview
//!
//! ```text
//!                     ┌─────────────────────────┐
//!                     │    SourceGenerator      │
//!                     │    (trait)              │
//!                     └───────────┬─────────────┘
//!//!                     ┌───────────┴─────────────┐
//!                     │   RegistryGenerator     │
//!                     │   (impl SourceGenerator)│
//!                     │   - Module structure    │
//!                     │   - Empty mod handling  │
//!                     │   - mod decl generation │
//!                     └───────────┬─────────────┘
//!                                 │ GeneratedWorkspace
//!                                 │ (crate-relative paths)
//!//!                     ┌─────────────────────────┐
//!                     │    SourceDumper<G>      │
//!                     │    G: SourceGenerator   │
//!                     │   - Path conversion     │
//!                     │   - crate-relative →    │
//!                     │     WorkspaceFilePath   │
//!                     └───────────┬─────────────┘
//!                                 │ HashMap<WorkspaceFilePath, String>
//!//!                     ┌─────────────────────────┐
//!                     │ sync_files_and_rebuild  │
//!                     │   - ctx update          │
//!                     │   - graph rebuild       │
//!                     └─────────────────────────┘
//! ```
//!
//! # Design Rationale
//!
//! ## Problem: Decoupled Generator and Dumper
//!
//! Previously, `FileDumper` and `RegistryGenerator` were independent:
//!
//! - `FileDumper`: Dumps from `ast_registry` directly (no empty module handling)
//! - `RegistryGenerator`: Generates with empty module handling (but wasn't used!)
//!
//! This caused `CreateMod` to fail because:
//! 1. `CreateMod` registers module in `symbol_registry` but NOT in `ast_registry`
//! 2. `FileDumper` only looks at `ast_registry`, missing the new module
//! 3. Result: No `mod xxx;` declaration in parent file
//!
//! ## Solution: Type-Enforced Pipeline
//!
//! By making `SourceDumper` generic over `SourceGenerator`:
//!
//! ```rust,ignore
//! // The ONLY way to get file output is through the pipeline
//! let dumper = SourceDumper::new(RegistryGenerator::multi_file());
//! let files = dumper.dump_all(ctx);
//! ```
//!
//! This ensures:
//! 1. Generator is always invoked (empty modules handled correctly)
//! 2. Path conversion is always applied
//! 3. Cannot accidentally bypass the generator
//!
//! # Responsibilities
//!
//! | Component | Responsibility | Input | Output |
//! |-----------|----------------|-------|--------|
//! | `SourceGenerator` | Module structure, source generation | ASTRegistry + SymbolRegistry | GeneratedWorkspace |
//! | `SourceDumper<G>` | Path conversion, file mapping | GeneratedWorkspace + ctx | HashMap<WorkspaceFilePath, String> |
//!
//! # Empty Module Handling
//!
//! `RegistryGenerator` handles empty modules by:
//!
//! 1. Scanning `symbol_registry` for `SymbolKind::Mod` entries
//! 2. For modules without child items in `ast_registry`, creating `PureMod { content: None }`
//! 3. Adding these to parent module's items for `mod xxx;` declaration output
//!
//! This happens in `RegistryGenerator::generate()`, specifically in:
//! - `collect Mod symbols from SymbolRegistry` phase
//! - `add_mod_symbols_to_tree()` function

use ryo_analysis::{ASTRegistry, AnalysisContext};
use ryo_symbol::{SymbolRegistry, WorkspaceFilePath};
use std::collections::HashMap;

use super::registry_generator::{GeneratedWorkspace, RegistryGenerator};
use ryo_source::pure::ToSynError;

// ============================================================================
// SourceGenerator Trait
// ============================================================================

/// Trait for source code generators.
///
/// Implementors transform `ASTRegistry + SymbolRegistry` into `GeneratedWorkspace`.
/// The key responsibility is determining module structure and handling edge cases
/// like empty modules.
pub trait SourceGenerator {
    /// Generate source files from registries.
    ///
    /// # Returns
    ///
    /// `GeneratedWorkspace` containing crate-relative paths (e.g., `"src/lib.rs"`).
    /// The caller is responsible for converting these to workspace-relative paths.
    fn generate(
        &self,
        ast_registry: &ASTRegistry,
        symbol_registry: &SymbolRegistry,
    ) -> Result<GeneratedWorkspace, ToSynError>;
}

impl SourceGenerator for RegistryGenerator {
    fn generate(
        &self,
        ast_registry: &ASTRegistry,
        symbol_registry: &SymbolRegistry,
    ) -> Result<GeneratedWorkspace, ToSynError> {
        self.generate(ast_registry, symbol_registry)
    }
}

// ============================================================================
// SourceDumper
// ============================================================================

/// Type-safe source dumper that enforces generator usage.
///
/// This struct ensures that source generation always goes through a `SourceGenerator`,
/// preventing bugs where mod declarations are missing.
///
/// # Type Safety
///
/// By requiring a `SourceGenerator` at construction time, we guarantee that:
/// - Empty modules are handled correctly
/// - Module hierarchy is properly derived
/// - `mod xxx;` declarations are generated
///
/// # Example
///
/// ```rust,ignore
/// use ryo_executor::engine::{SourceDumper, RegistryGenerator};
///
/// // Create dumper with generator (type-enforced)
/// let dumper = SourceDumper::new(RegistryGenerator::multi_file());
///
/// // Dump all files (generator is automatically invoked)
/// let files = dumper.dump_all(&ctx);
/// ```
pub struct SourceDumper<G: SourceGenerator> {
    generator: G,
}

impl<G: SourceGenerator> SourceDumper<G> {
    /// Create a new SourceDumper with the given generator.
    pub fn new(generator: G) -> Self {
        Self { generator }
    }

    /// Generate and dump all files from the analysis context.
    ///
    /// This method:
    /// 1. Invokes the generator to create `GeneratedWorkspace`
    /// 2. Converts crate-relative paths to `WorkspaceFilePath`
    /// 3. Returns the final file map
    ///
    /// # Path Conversion
    ///
    /// Crate-relative paths (e.g., `"src/lib.rs"`) are converted to
    /// workspace-relative paths (e.g., `"crates/core/src/lib.rs"`) by:
    ///
    /// 1. Finding existing files for each crate to determine crate_root
    /// 2. Combining crate_root + crate-relative path
    pub fn dump_all(
        &self,
        ctx: &AnalysisContext,
    ) -> Result<HashMap<WorkspaceFilePath, String>, ToSynError> {
        // Step 1: Generate via the generator (handles empty modules, etc.)
        let workspace = self.generator.generate(&ctx.ast_registry, &ctx.registry)?;

        // Step 2: Convert to WorkspaceFilePath
        Ok(self.convert_to_workspace_paths(workspace, ctx))
    }

    /// Convert GeneratedWorkspace to HashMap<WorkspaceFilePath, String>.
    ///
    /// This handles the crate-relative → workspace-relative path conversion.
    fn convert_to_workspace_paths(
        &self,
        workspace: GeneratedWorkspace,
        ctx: &AnalysisContext,
    ) -> HashMap<WorkspaceFilePath, String> {
        let mut result = HashMap::new();

        // Build crate_name → template WorkspaceFilePath mapping
        let crate_templates = Self::get_crate_templates(ctx);

        for (crate_name, generated_crate) in workspace.crates {
            // Get template file for this crate (to derive workspace_root and crate_name)
            let template = crate_templates.get(&crate_name);

            for (crate_relative_path, generated_file) in generated_crate.files {
                // Create WorkspaceFilePath using template's with_relative
                let wfp = if let Some(template) = template {
                    // Use template to create new path with correct workspace_root/crate_name
                    template.with_relative(&crate_relative_path)
                } else {
                    // Fallback: create from context (root crate case)
                    Self::create_workspace_file_path_fallback(&crate_relative_path, ctx)
                };

                result.insert(wfp, generated_file.source);
            }
        }

        result
    }

    /// Get template WorkspaceFilePath for each crate.
    ///
    /// Uses the first file found for each crate as a template to derive
    /// workspace_root and crate_name for new files.
    fn get_crate_templates(ctx: &AnalysisContext) -> HashMap<String, WorkspaceFilePath> {
        let mut templates = HashMap::new();

        for wfp in ctx.files.keys() {
            let crate_name = wfp.crate_name().as_str().to_string();
            templates.entry(crate_name).or_insert_with(|| wfp.clone());
        }

        templates
    }

    /// Fallback: create WorkspaceFilePath when no template exists.
    ///
    /// This handles the case where a crate has no existing files (e.g., entirely new crate).
    fn create_workspace_file_path_fallback(
        crate_relative_path: &str,
        ctx: &AnalysisContext,
    ) -> WorkspaceFilePath {
        // Use any existing file as template, just change the path.
        //
        // Panics if `ctx.files` is empty. This is treated as a corrupted
        // internal state — `AnalysisContext` is always constructed with at
        // least one file from the initial parse, so reaching this branch in
        // production indicates the context was emptied between construction
        // and the source-pipeline stage.
        ctx.files
            .keys()
            .next()
            .map(|any_file| any_file.with_relative(crate_relative_path))
            .expect(
                "AnalysisContext.files is unexpectedly empty in source pipeline; \
                 cannot synthesize a WorkspaceFilePath template",
            )
    }
}

// ============================================================================
// Convenience Functions
// ============================================================================

/// Create a standard multi-file dumper.
///
/// This is the recommended way to create a dumper for most use cases.
pub fn multi_file_dumper() -> SourceDumper<RegistryGenerator> {
    SourceDumper::new(RegistryGenerator::multi_file())
}

/// Create a single-file dumper (all code in one file with nested mods).
pub fn single_file_dumper() -> SourceDumper<RegistryGenerator> {
    SourceDumper::new(RegistryGenerator::single_file())
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use ryo_analysis::testing::ContextBuilder;

    #[test]
    fn test_source_generator_trait_implemented() {
        // Verify RegistryGenerator implements SourceGenerator
        let _: Box<dyn SourceGenerator> = Box::new(RegistryGenerator::multi_file());
    }

    #[test]
    fn test_source_dumper_creation() {
        let dumper = multi_file_dumper();
        let ctx = ContextBuilder::new()
            .with_file("src/lib.rs", "pub struct Foo;")
            .build();

        let files = dumper.dump_all(&ctx).unwrap();
        assert!(!files.is_empty(), "Should generate at least one file");
    }

    #[test]
    fn test_get_crate_templates() {
        let ctx = ContextBuilder::new()
            .with_file("src/lib.rs", "pub struct Foo;")
            .build();

        let templates = SourceDumper::<RegistryGenerator>::get_crate_templates(&ctx);

        // Should have at least one template
        assert!(
            !templates.is_empty(),
            "Should have at least one crate template"
        );
    }
}