Skip to main content

ryo_executor/engine/
source_pipeline.rs

1//! Source Pipeline - Type-Safe Generator → Dumper Architecture
2//!
3//! This module defines the type-safe pipeline for source code generation and output.
4//! It ensures that `RegistryGenerator` and file dumping are always used together,
5//! preventing the bug where mod declarations are missing due to bypassing the generator.
6//!
7//! # Architecture Overview
8//!
9//! ```text
10//!                     ┌─────────────────────────┐
11//!                     │    SourceGenerator      │
12//!                     │    (trait)              │
13//!                     └───────────┬─────────────┘
14//!                                 │
15//!                     ┌───────────┴─────────────┐
16//!                     │   RegistryGenerator     │
17//!                     │   (impl SourceGenerator)│
18//!                     │   - Module structure    │
19//!                     │   - Empty mod handling  │
20//!                     │   - mod decl generation │
21//!                     └───────────┬─────────────┘
22//!                                 │ GeneratedWorkspace
23//!                                 │ (crate-relative paths)
24//!                                 ▼
25//!                     ┌─────────────────────────┐
26//!                     │    SourceDumper<G>      │
27//!                     │    G: SourceGenerator   │
28//!                     │   - Path conversion     │
29//!                     │   - crate-relative →    │
30//!                     │     WorkspaceFilePath   │
31//!                     └───────────┬─────────────┘
32//!                                 │ HashMap<WorkspaceFilePath, String>
33//!                                 ▼
34//!                     ┌─────────────────────────┐
35//!                     │ sync_files_and_rebuild  │
36//!                     │   - ctx update          │
37//!                     │   - graph rebuild       │
38//!                     └─────────────────────────┘
39//! ```
40//!
41//! # Design Rationale
42//!
43//! ## Problem: Decoupled Generator and Dumper
44//!
45//! Previously, `FileDumper` and `RegistryGenerator` were independent:
46//!
47//! - `FileDumper`: Dumps from `ast_registry` directly (no empty module handling)
48//! - `RegistryGenerator`: Generates with empty module handling (but wasn't used!)
49//!
50//! This caused `CreateMod` to fail because:
51//! 1. `CreateMod` registers module in `symbol_registry` but NOT in `ast_registry`
52//! 2. `FileDumper` only looks at `ast_registry`, missing the new module
53//! 3. Result: No `mod xxx;` declaration in parent file
54//!
55//! ## Solution: Type-Enforced Pipeline
56//!
57//! By making `SourceDumper` generic over `SourceGenerator`:
58//!
59//! ```rust,ignore
60//! // The ONLY way to get file output is through the pipeline
61//! let dumper = SourceDumper::new(RegistryGenerator::multi_file());
62//! let files = dumper.dump_all(ctx);
63//! ```
64//!
65//! This ensures:
66//! 1. Generator is always invoked (empty modules handled correctly)
67//! 2. Path conversion is always applied
68//! 3. Cannot accidentally bypass the generator
69//!
70//! # Responsibilities
71//!
72//! | Component | Responsibility | Input | Output |
73//! |-----------|----------------|-------|--------|
74//! | `SourceGenerator` | Module structure, source generation | ASTRegistry + SymbolRegistry | GeneratedWorkspace |
75//! | `SourceDumper<G>` | Path conversion, file mapping | GeneratedWorkspace + ctx | HashMap<WorkspaceFilePath, String> |
76//!
77//! # Empty Module Handling
78//!
79//! `RegistryGenerator` handles empty modules by:
80//!
81//! 1. Scanning `symbol_registry` for `SymbolKind::Mod` entries
82//! 2. For modules without child items in `ast_registry`, creating `PureMod { content: None }`
83//! 3. Adding these to parent module's items for `mod xxx;` declaration output
84//!
85//! This happens in `RegistryGenerator::generate()`, specifically in:
86//! - `collect Mod symbols from SymbolRegistry` phase
87//! - `add_mod_symbols_to_tree()` function
88
89use ryo_analysis::{ASTRegistry, AnalysisContext};
90use ryo_symbol::{SymbolRegistry, WorkspaceFilePath};
91use std::collections::HashMap;
92
93use super::registry_generator::{GeneratedWorkspace, RegistryGenerator};
94use ryo_source::pure::ToSynError;
95
96// ============================================================================
97// SourceGenerator Trait
98// ============================================================================
99
100/// Trait for source code generators.
101///
102/// Implementors transform `ASTRegistry + SymbolRegistry` into `GeneratedWorkspace`.
103/// The key responsibility is determining module structure and handling edge cases
104/// like empty modules.
105pub trait SourceGenerator {
106    /// Generate source files from registries.
107    ///
108    /// # Returns
109    ///
110    /// `GeneratedWorkspace` containing crate-relative paths (e.g., `"src/lib.rs"`).
111    /// The caller is responsible for converting these to workspace-relative paths.
112    fn generate(
113        &self,
114        ast_registry: &ASTRegistry,
115        symbol_registry: &SymbolRegistry,
116    ) -> Result<GeneratedWorkspace, ToSynError>;
117}
118
119impl SourceGenerator for RegistryGenerator {
120    fn generate(
121        &self,
122        ast_registry: &ASTRegistry,
123        symbol_registry: &SymbolRegistry,
124    ) -> Result<GeneratedWorkspace, ToSynError> {
125        self.generate(ast_registry, symbol_registry)
126    }
127}
128
129// ============================================================================
130// SourceDumper
131// ============================================================================
132
133/// Type-safe source dumper that enforces generator usage.
134///
135/// This struct ensures that source generation always goes through a `SourceGenerator`,
136/// preventing bugs where mod declarations are missing.
137///
138/// # Type Safety
139///
140/// By requiring a `SourceGenerator` at construction time, we guarantee that:
141/// - Empty modules are handled correctly
142/// - Module hierarchy is properly derived
143/// - `mod xxx;` declarations are generated
144///
145/// # Example
146///
147/// ```rust,ignore
148/// use ryo_executor::engine::{SourceDumper, RegistryGenerator};
149///
150/// // Create dumper with generator (type-enforced)
151/// let dumper = SourceDumper::new(RegistryGenerator::multi_file());
152///
153/// // Dump all files (generator is automatically invoked)
154/// let files = dumper.dump_all(&ctx);
155/// ```
156pub struct SourceDumper<G: SourceGenerator> {
157    generator: G,
158}
159
160impl<G: SourceGenerator> SourceDumper<G> {
161    /// Create a new SourceDumper with the given generator.
162    pub fn new(generator: G) -> Self {
163        Self { generator }
164    }
165
166    /// Generate and dump all files from the analysis context.
167    ///
168    /// This method:
169    /// 1. Invokes the generator to create `GeneratedWorkspace`
170    /// 2. Converts crate-relative paths to `WorkspaceFilePath`
171    /// 3. Returns the final file map
172    ///
173    /// # Path Conversion
174    ///
175    /// Crate-relative paths (e.g., `"src/lib.rs"`) are converted to
176    /// workspace-relative paths (e.g., `"crates/core/src/lib.rs"`) by:
177    ///
178    /// 1. Finding existing files for each crate to determine crate_root
179    /// 2. Combining crate_root + crate-relative path
180    pub fn dump_all(
181        &self,
182        ctx: &AnalysisContext,
183    ) -> Result<HashMap<WorkspaceFilePath, String>, ToSynError> {
184        // Step 1: Generate via the generator (handles empty modules, etc.)
185        let workspace = self.generator.generate(&ctx.ast_registry, &ctx.registry)?;
186
187        // Step 2: Convert to WorkspaceFilePath
188        Ok(self.convert_to_workspace_paths(workspace, ctx))
189    }
190
191    /// Convert GeneratedWorkspace to HashMap<WorkspaceFilePath, String>.
192    ///
193    /// This handles the crate-relative → workspace-relative path conversion.
194    fn convert_to_workspace_paths(
195        &self,
196        workspace: GeneratedWorkspace,
197        ctx: &AnalysisContext,
198    ) -> HashMap<WorkspaceFilePath, String> {
199        let mut result = HashMap::new();
200
201        // Build crate_name → template WorkspaceFilePath mapping
202        let crate_templates = Self::get_crate_templates(ctx);
203
204        for (crate_name, generated_crate) in workspace.crates {
205            // Get template file for this crate (to derive workspace_root and crate_name)
206            let template = crate_templates.get(&crate_name);
207
208            for (crate_relative_path, generated_file) in generated_crate.files {
209                // Create WorkspaceFilePath using template's with_relative
210                let wfp = if let Some(template) = template {
211                    // Use template to create new path with correct workspace_root/crate_name
212                    template.with_relative(&crate_relative_path)
213                } else {
214                    // Fallback: create from context (root crate case)
215                    Self::create_workspace_file_path_fallback(&crate_relative_path, ctx)
216                };
217
218                result.insert(wfp, generated_file.source);
219            }
220        }
221
222        result
223    }
224
225    /// Get template WorkspaceFilePath for each crate.
226    ///
227    /// Uses the first file found for each crate as a template to derive
228    /// workspace_root and crate_name for new files.
229    fn get_crate_templates(ctx: &AnalysisContext) -> HashMap<String, WorkspaceFilePath> {
230        let mut templates = HashMap::new();
231
232        for wfp in ctx.files.keys() {
233            let crate_name = wfp.crate_name().as_str().to_string();
234            templates.entry(crate_name).or_insert_with(|| wfp.clone());
235        }
236
237        templates
238    }
239
240    /// Fallback: create WorkspaceFilePath when no template exists.
241    ///
242    /// This handles the case where a crate has no existing files (e.g., entirely new crate).
243    fn create_workspace_file_path_fallback(
244        crate_relative_path: &str,
245        ctx: &AnalysisContext,
246    ) -> WorkspaceFilePath {
247        // Use any existing file as template, just change the path.
248        //
249        // Panics if `ctx.files` is empty. This is treated as a corrupted
250        // internal state — `AnalysisContext` is always constructed with at
251        // least one file from the initial parse, so reaching this branch in
252        // production indicates the context was emptied between construction
253        // and the source-pipeline stage.
254        ctx.files
255            .keys()
256            .next()
257            .map(|any_file| any_file.with_relative(crate_relative_path))
258            .expect(
259                "AnalysisContext.files is unexpectedly empty in source pipeline; \
260                 cannot synthesize a WorkspaceFilePath template",
261            )
262    }
263}
264
265// ============================================================================
266// Convenience Functions
267// ============================================================================
268
269/// Create a standard multi-file dumper.
270///
271/// This is the recommended way to create a dumper for most use cases.
272pub fn multi_file_dumper() -> SourceDumper<RegistryGenerator> {
273    SourceDumper::new(RegistryGenerator::multi_file())
274}
275
276/// Create a single-file dumper (all code in one file with nested mods).
277pub fn single_file_dumper() -> SourceDumper<RegistryGenerator> {
278    SourceDumper::new(RegistryGenerator::single_file())
279}
280
281// ============================================================================
282// Tests
283// ============================================================================
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use ryo_analysis::testing::ContextBuilder;
289
290    #[test]
291    fn test_source_generator_trait_implemented() {
292        // Verify RegistryGenerator implements SourceGenerator
293        let _: Box<dyn SourceGenerator> = Box::new(RegistryGenerator::multi_file());
294    }
295
296    #[test]
297    fn test_source_dumper_creation() {
298        let dumper = multi_file_dumper();
299        let ctx = ContextBuilder::new()
300            .with_file("src/lib.rs", "pub struct Foo;")
301            .build();
302
303        let files = dumper.dump_all(&ctx).unwrap();
304        assert!(!files.is_empty(), "Should generate at least one file");
305    }
306
307    #[test]
308    fn test_get_crate_templates() {
309        let ctx = ContextBuilder::new()
310            .with_file("src/lib.rs", "pub struct Foo;")
311            .build();
312
313        let templates = SourceDumper::<RegistryGenerator>::get_crate_templates(&ctx);
314
315        // Should have at least one template
316        assert!(
317            !templates.is_empty(),
318            "Should have at least one crate template"
319        );
320    }
321}