ruchy/backend/
module_resolver.rs

1//! Module resolver for multi-file imports
2//!
3//! This module provides functionality to resolve file imports in Ruchy programs
4//! by pre-processing the AST to inline external modules before transpilation.
5//!
6//! # Architecture
7//! 
8//! The module resolver works as a pre-processing step before transpilation:
9//! 1. Parse the main file into an AST
10//! 2. Scan for file imports (`use module_name;` where `module_name` has no `::`)
11//! 3. Load and parse external module files 
12//! 4. Replace Import nodes with inline Module nodes
13//! 5. Pass the resolved AST to the transpiler
14//!
15//! # Usage
16//!
17//! ```rust
18//! use ruchy::{ModuleResolver, Parser, Transpiler};
19//! 
20//! let mut resolver = ModuleResolver::new();
21//! resolver.add_search_path("./src");
22//! 
23//! let mut parser = Parser::new("use math; math::add(1, 2)");
24//! let ast = parser.parse()?;
25//! 
26//! let resolved_ast = resolver.resolve_imports(ast)?;
27//! 
28//! let transpiler = Transpiler::new();
29//! let rust_code = transpiler.transpile(&resolved_ast)?;
30//! ```
31
32use crate::frontend::ast::{Expr, ExprKind, ImportItem, Span};
33use crate::backend::module_loader::ModuleLoader;
34use anyhow::{Result, Context};
35
36/// Module resolver for processing file imports
37/// 
38/// Resolves file imports by loading external modules and inlining them
39/// as Module declarations in the AST before transpilation.
40pub struct ModuleResolver {
41    /// Module loader for file system operations
42    module_loader: ModuleLoader,
43}
44
45impl ModuleResolver {
46    /// Create a new module resolver with default search paths
47    /// 
48    /// Default search paths:
49    /// - `.` (current directory)
50    /// - `./src` (source directory)
51    /// - `./modules` (modules directory)
52    #[must_use]
53    pub fn new() -> Self {
54        Self {
55            module_loader: ModuleLoader::new(),
56        }
57    }
58    
59    /// Add a directory to the module search path
60    /// 
61    /// # Arguments
62    /// 
63    /// * `path` - Directory to search for modules
64    pub fn add_search_path<P: AsRef<std::path::Path>>(&mut self, path: P) {
65        self.module_loader.add_search_path(path);
66    }
67    
68    /// Resolve all file imports in an AST
69    /// 
70    /// Recursively processes the AST to find file imports, loads the corresponding
71    /// modules, and replaces Import nodes with inline Module nodes.
72    /// 
73    /// # Arguments
74    /// 
75    /// * `ast` - The AST to process
76    /// 
77    /// # Returns
78    /// 
79    /// A new AST with all file imports resolved to inline modules
80    /// 
81    /// # Errors
82    /// 
83    /// Returns an error if:
84    /// - Module files cannot be found or loaded
85    /// - Module files contain invalid syntax  
86    /// - Circular dependencies are detected
87    pub fn resolve_imports(&mut self, ast: Expr) -> Result<Expr> {
88        self.resolve_expr(ast)
89    }
90    
91    /// Recursively resolve imports in an expression
92    fn resolve_expr(&mut self, expr: Expr) -> Result<Expr> {
93        match expr.kind {
94            ExprKind::Import { ref path, ref items } => {
95                // Check if this is a file import (no :: and not std:: or http)
96                if self.is_file_import(path) {
97                    // Load the module file
98                    let parsed_module = self.module_loader.load_module(path)
99                        .with_context(|| format!("Failed to resolve import '{path}'"))?;
100                    
101                    // Recursively resolve imports in the loaded module
102                    let resolved_module_ast = self.resolve_expr(parsed_module.ast)?;
103                    
104                    // Create an inline module declaration
105                    let module_expr = Expr::new(
106                        ExprKind::Module {
107                            name: path.clone(),
108                            body: Box::new(resolved_module_ast),
109                        },
110                        expr.span,
111                    );
112                    
113                    // Create a use statement to import from the module
114                    let use_statement = if items.iter().any(|item| matches!(item, ImportItem::Wildcard)) || items.is_empty() {
115                        // Wildcard import: use module::*;
116                        Expr::new(
117                            ExprKind::Import {
118                                path: path.clone(),
119                                items: vec![ImportItem::Wildcard],
120                            },
121                            Span { start: 0, end: 0 },
122                        )
123                    } else {
124                        // Specific imports: use module::{item1, item2};
125                        self.create_use_statements(path, items)
126                    };
127                    
128                    // Return a block with the module declaration and use statement
129                    Ok(Expr::new(
130                        ExprKind::Block(vec![module_expr, use_statement]),
131                        expr.span,
132                    ))
133                } else {
134                    // Not a file import, keep as-is
135                    Ok(expr)
136                }
137            }
138            ExprKind::Block(exprs) => {
139                // Resolve imports in all block expressions
140                let resolved_exprs: Result<Vec<_>> = exprs
141                    .into_iter()
142                    .map(|e| self.resolve_expr(e))
143                    .collect();
144                Ok(Expr::new(ExprKind::Block(resolved_exprs?), expr.span))
145            }
146            ExprKind::Module { name, body } => {
147                // Resolve imports in module body
148                let resolved_body = self.resolve_expr(*body)?;
149                Ok(Expr::new(
150                    ExprKind::Module {
151                        name,
152                        body: Box::new(resolved_body),
153                    },
154                    expr.span,
155                ))
156            }
157            ExprKind::Function { 
158                name, 
159                type_params, 
160                params, 
161                body, 
162                is_async,
163                return_type,
164                is_pub,
165            } => {
166                // Resolve imports in function body
167                let resolved_body = self.resolve_expr(*body)?;
168                Ok(Expr::new(
169                    ExprKind::Function {
170                        name,
171                        type_params,
172                        params,
173                        body: Box::new(resolved_body),
174                        is_async,
175                        return_type,
176                        is_pub,
177                    },
178                    expr.span,
179                ))
180            }
181            ExprKind::If { condition, then_branch, else_branch } => {
182                let resolved_condition = self.resolve_expr(*condition)?;
183                let resolved_then = self.resolve_expr(*then_branch)?;
184                let resolved_else = else_branch.map(|e| self.resolve_expr(*e)).transpose()?;
185                Ok(Expr::new(
186                    ExprKind::If {
187                        condition: Box::new(resolved_condition),
188                        then_branch: Box::new(resolved_then),
189                        else_branch: resolved_else.map(Box::new),
190                    },
191                    expr.span,
192                ))
193            }
194            // For other expression types, recursively process children as needed
195            // For now, just return the expression as-is
196            _ => Ok(expr),
197        }
198    }
199    
200    /// Check if an import path represents a file import
201    fn is_file_import(&self, path: &str) -> bool {
202        !path.contains("::")
203            && !path.starts_with("std::")
204            && !path.starts_with("http")
205            && !path.is_empty()
206    }
207    
208    /// Create use statements for specific imports
209    fn create_use_statements(&self, module_path: &str, items: &[ImportItem]) -> Expr {
210        // Create a use statement that imports specific items from the module
211        // This will be transpiled to proper Rust use statements
212        Expr::new(
213            ExprKind::Import {
214                path: module_path.to_string(), // Use the module path as-is
215                items: items.to_vec(),
216            },
217            Span { start: 0, end: 0 },
218        )
219    }
220    
221    /// Get module loading statistics
222    #[must_use]
223    pub fn stats(&self) -> crate::backend::module_loader::ModuleLoaderStats {
224        self.module_loader.stats()
225    }
226    
227    /// Clear the module cache
228    /// 
229    /// Forces all modules to be reloaded from disk on next access.
230    pub fn clear_cache(&mut self) {
231        self.module_loader.clear_cache();
232    }
233}
234
235impl Default for ModuleResolver {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use tempfile::TempDir;
245    use std::fs;
246    use crate::frontend::ast::Literal;
247
248    fn create_test_module(temp_dir: &TempDir, name: &str, content: &str) -> Result<()> {
249        let file_path = temp_dir.path().join(format!("{name}.ruchy"));
250        fs::write(file_path, content)?;
251        Ok(())
252    }
253
254    #[test]
255    fn test_module_resolver_creation() {
256        let resolver = ModuleResolver::new();
257        let stats = resolver.stats();
258        assert_eq!(stats.cached_modules, 0);
259    }
260
261    #[test] 
262    fn test_add_search_path() {
263        let mut resolver = ModuleResolver::new();
264        resolver.add_search_path("/custom/path");
265        // Module loader doesn't expose search paths, so we can't directly test this
266        // But we can test that it doesn't panic
267    }
268
269    #[test]
270    fn test_is_file_import() {
271        let resolver = ModuleResolver::new();
272        
273        // Should be file imports
274        assert!(resolver.is_file_import("math"));
275        assert!(resolver.is_file_import("utils"));
276        assert!(resolver.is_file_import("snake_case_module"));
277        
278        // Should NOT be file imports
279        assert!(!resolver.is_file_import("std::collections"));
280        assert!(!resolver.is_file_import("std::io::Read"));
281        assert!(!resolver.is_file_import("https://example.com/module.ruchy"));
282        assert!(!resolver.is_file_import("http://localhost/module.ruchy"));
283        assert!(!resolver.is_file_import(""));
284    }
285
286    #[test]
287    fn test_resolve_simple_file_import() -> Result<()> {
288        let temp_dir = TempDir::new()?;
289        let mut resolver = ModuleResolver::new();
290        resolver.add_search_path(temp_dir.path());
291        
292        // Create a simple math module
293        create_test_module(&temp_dir, "math", r"
294            pub fun add(a: i32, b: i32) -> i32 {
295                a + b
296            }
297        ")?;
298        
299        // Create an import expression
300        let import_expr = Expr::new(
301            ExprKind::Import {
302                path: "math".to_string(),
303                items: vec![ImportItem::Wildcard],
304            },
305            Span { start: 0, end: 0 },
306        );
307        
308        // Resolve the import
309        let resolved_expr = resolver.resolve_imports(import_expr)?;
310        
311        // Should be converted to a Block with Module declaration and use statement
312        match resolved_expr.kind {
313            ExprKind::Block(exprs) => {
314                assert_eq!(exprs.len(), 2);
315                // First should be Module declaration
316                match &exprs[0].kind {
317                    ExprKind::Module { name, .. } => {
318                        assert_eq!(name, "math");
319                    }
320                    _ => unreachable!("Expected first element to be Module, got {:?}", exprs[0].kind),
321                }
322                // Second should be use statement
323                match &exprs[1].kind {
324                    ExprKind::Import { path, items } => {
325                        assert_eq!(path, "math");
326                        assert_eq!(items.len(), 1);
327                        assert!(matches!(items[0], ImportItem::Wildcard));
328                    }
329                    _ => unreachable!("Expected second element to be Import, got {:?}", exprs[1].kind),
330                }
331            }
332            _ => unreachable!("Expected Block expression, got {:?}", resolved_expr.kind),
333        }
334        
335        Ok(())
336    }
337
338    #[test]
339    fn test_resolve_non_file_import() -> Result<()> {
340        let mut resolver = ModuleResolver::new();
341        
342        // Create a standard library import
343        let import_expr = Expr::new(
344            ExprKind::Import {
345                path: "std::collections".to_string(),
346                items: vec![ImportItem::Named("HashMap".to_string())],
347            },
348            Span { start: 0, end: 0 },
349        );
350        
351        // Resolve the import - should remain unchanged
352        let resolved_expr = resolver.resolve_imports(import_expr)?;
353        
354        match resolved_expr.kind {
355            ExprKind::Import { path, items } => {
356                assert_eq!(path, "std::collections");
357                assert_eq!(items.len(), 1);
358            }
359            _ => unreachable!("Expected Import expression to remain unchanged"),
360        }
361        
362        Ok(())
363    }
364
365    #[test]
366    fn test_resolve_block_with_imports() -> Result<()> {
367        let temp_dir = TempDir::new()?;
368        let mut resolver = ModuleResolver::new();
369        resolver.add_search_path(temp_dir.path());
370        
371        create_test_module(&temp_dir, "math", "pub fun add() {}")?;
372        
373        // Create a block with mixed imports
374        let block_expr = Expr::new(
375            ExprKind::Block(vec![
376                Expr::new(
377                    ExprKind::Import {
378                        path: "math".to_string(),
379                        items: vec![ImportItem::Wildcard],
380                    },
381                    Span { start: 0, end: 0 },
382                ),
383                Expr::new(
384                    ExprKind::Import {
385                        path: "std::io".to_string(),
386                        items: vec![ImportItem::Named("Read".to_string())],
387                    },
388                    Span { start: 0, end: 0 },
389                ),
390                Expr::new(
391                    ExprKind::Literal(Literal::Integer(42)),
392                    Span { start: 0, end: 0 },
393                ),
394            ]),
395            Span { start: 0, end: 0 },
396        );
397        
398        let resolved_block = resolver.resolve_imports(block_expr)?;
399        
400        if let ExprKind::Block(exprs) = resolved_block.kind {
401            assert_eq!(exprs.len(), 3);
402            
403            // First should be Block containing Module and use statement (from file import)
404            match &exprs[0].kind {
405                ExprKind::Block(inner_exprs) => {
406                    assert_eq!(inner_exprs.len(), 2);
407                    assert!(matches!(inner_exprs[0].kind, ExprKind::Module { .. }));
408                    assert!(matches!(inner_exprs[1].kind, ExprKind::Import { .. }));
409                }
410                _ => unreachable!("Expected first element to be Block, got {:?}", exprs[0].kind),
411            }
412            
413            // Second should remain as Import (std::io - not a file import)
414            assert!(matches!(exprs[1].kind, ExprKind::Import { .. }));
415            
416            // Third should remain as Literal
417            assert!(matches!(exprs[2].kind, ExprKind::Literal(Literal::Integer(42))));
418        } else {
419            unreachable!("Expected Block expression");
420        }
421        
422        Ok(())
423    }
424
425    #[test]
426    fn test_stats_and_cache() -> Result<()> {
427        let temp_dir = TempDir::new()?;
428        let mut resolver = ModuleResolver::new();
429        resolver.add_search_path(temp_dir.path());
430        
431        create_test_module(&temp_dir, "test", "pub fun test() {}")?;
432        
433        let initial_stats = resolver.stats();
434        assert_eq!(initial_stats.files_loaded, 0);
435        
436        // Load a module
437        let import_expr = Expr::new(
438            ExprKind::Import {
439                path: "test".to_string(),
440                items: vec![ImportItem::Wildcard],
441            },
442            Span { start: 0, end: 0 },
443        );
444        
445        resolver.resolve_imports(import_expr)?;
446        
447        let after_stats = resolver.stats();
448        assert_eq!(after_stats.files_loaded, 1);
449        assert_eq!(after_stats.cached_modules, 1);
450        
451        // Clear cache
452        resolver.clear_cache();
453        let cleared_stats = resolver.stats();
454        assert_eq!(cleared_stats.files_loaded, 0);
455        assert_eq!(cleared_stats.cached_modules, 0);
456        
457        Ok(())
458    }
459}