Coverage Report

Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/ruchy/src/backend/module_resolver.rs
Line
Count
Source
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
32
use crate::frontend::ast::{Expr, ExprKind, ImportItem, Span};
33
use crate::backend::module_loader::ModuleLoader;
34
use 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.
40
pub struct ModuleResolver {
41
    /// Module loader for file system operations
42
    pub(crate) module_loader: ModuleLoader,
43
}
44
45
impl 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
0
    pub fn new() -> Self {
54
0
        Self {
55
0
            module_loader: ModuleLoader::new(),
56
0
        }
57
0
    }
58
    
59
    /// Add a directory to the module search path
60
    /// 
61
    /// # Arguments
62
    /// 
63
    /// * `path` - Directory to search for modules
64
0
    pub fn add_search_path<P: AsRef<std::path::Path>>(&mut self, path: P) {
65
0
        self.module_loader.add_search_path(path);
66
0
    }
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
0
    pub fn resolve_imports(&mut self, ast: Expr) -> Result<Expr> {
88
0
        self.resolve_expr(ast)
89
0
    }
90
    
91
    /// Recursively resolve imports in an expression
92
0
    fn resolve_expr(&mut self, expr: Expr) -> Result<Expr> {
93
0
        match expr.kind {
94
0
            ExprKind::Import { ref path, ref items } => {
95
                // Check if this is a file import (no :: and not std:: or http)
96
0
                if self.is_file_import(path) {
97
                    // Load the module file
98
0
                    let parsed_module = self.module_loader.load_module(path)
99
0
                        .with_context(|| format!("Failed to resolve import '{path}'"))?;
100
                    
101
                    // Recursively resolve imports in the loaded module
102
0
                    let resolved_module_ast = self.resolve_expr(parsed_module.ast)?;
103
                    
104
                    // Create an inline module declaration
105
0
                    let module_expr = Expr::new(
106
0
                        ExprKind::Module {
107
0
                            name: path.clone(),
108
0
                            body: Box::new(resolved_module_ast),
109
0
                        },
110
0
                        expr.span,
111
                    );
112
                    
113
                    // Create a use statement to import from the module
114
0
                    let use_statement = if items.iter().any(|item| matches!(item, ImportItem::Wildcard)) || items.is_empty() {
115
                        // Wildcard import: use module::*;
116
0
                        Expr::new(
117
0
                            ExprKind::Import {
118
0
                                path: path.clone(),
119
0
                                items: vec![ImportItem::Wildcard],
120
0
                            },
121
0
                            Span { start: 0, end: 0 },
122
                        )
123
                    } else {
124
                        // Specific imports: use module::{item1, item2};
125
0
                        self.create_use_statements(path, items)
126
                    };
127
                    
128
                    // Return a block with the module declaration and use statement
129
0
                    Ok(Expr::new(
130
0
                        ExprKind::Block(vec![module_expr, use_statement]),
131
0
                        expr.span,
132
0
                    ))
133
                } else {
134
                    // Not a file import, keep as-is
135
0
                    Ok(expr)
136
                }
137
            }
138
0
            ExprKind::Block(exprs) => {
139
                // Resolve imports in all block expressions
140
0
                let resolved_exprs: Result<Vec<_>> = exprs
141
0
                    .into_iter()
142
0
                    .map(|e| self.resolve_expr(e))
143
0
                    .collect();
144
0
                Ok(Expr::new(ExprKind::Block(resolved_exprs?), expr.span))
145
            }
146
0
            ExprKind::Module { name, body } => {
147
                // Resolve imports in module body
148
0
                let resolved_body = self.resolve_expr(*body)?;
149
0
                Ok(Expr::new(
150
0
                    ExprKind::Module {
151
0
                        name,
152
0
                        body: Box::new(resolved_body),
153
0
                    },
154
0
                    expr.span,
155
0
                ))
156
            }
157
            ExprKind::Function { 
158
0
                name, 
159
0
                type_params, 
160
0
                params, 
161
0
                body, 
162
0
                is_async,
163
0
                return_type,
164
0
                is_pub,
165
            } => {
166
                // Resolve imports in function body
167
0
                let resolved_body = self.resolve_expr(*body)?;
168
0
                Ok(Expr::new(
169
0
                    ExprKind::Function {
170
0
                        name,
171
0
                        type_params,
172
0
                        params,
173
0
                        body: Box::new(resolved_body),
174
0
                        is_async,
175
0
                        return_type,
176
0
                        is_pub,
177
0
                    },
178
0
                    expr.span,
179
0
                ))
180
            }
181
0
            ExprKind::If { condition, then_branch, else_branch } => {
182
0
                let resolved_condition = self.resolve_expr(*condition)?;
183
0
                let resolved_then = self.resolve_expr(*then_branch)?;
184
0
                let resolved_else = else_branch.map(|e| self.resolve_expr(*e)).transpose()?;
185
0
                Ok(Expr::new(
186
0
                    ExprKind::If {
187
0
                        condition: Box::new(resolved_condition),
188
0
                        then_branch: Box::new(resolved_then),
189
0
                        else_branch: resolved_else.map(Box::new),
190
0
                    },
191
0
                    expr.span,
192
0
                ))
193
            }
194
            // For other expression types, recursively process children as needed
195
            // For now, just return the expression as-is
196
0
            _ => Ok(expr),
197
        }
198
0
    }
199
    
200
    /// Check if an import path represents a file import
201
0
    fn is_file_import(&self, path: &str) -> bool {
202
0
        !path.contains("::")
203
0
            && !path.starts_with("std::")
204
0
            && !path.starts_with("http")
205
0
            && !path.is_empty()
206
0
    }
207
    
208
    /// Create use statements for specific imports
209
0
    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
0
        Expr::new(
213
0
            ExprKind::Import {
214
0
                path: module_path.to_string(), // Use the module path as-is
215
0
                items: items.to_vec(),
216
0
            },
217
0
            Span { start: 0, end: 0 },
218
        )
219
0
    }
220
    
221
    /// Get module loading statistics
222
    #[must_use]
223
0
    pub fn stats(&self) -> crate::backend::module_loader::ModuleLoaderStats {
224
0
        self.module_loader.stats()
225
0
    }
226
    
227
    /// Clear the module cache
228
    /// 
229
    /// Forces all modules to be reloaded from disk on next access.
230
0
    pub fn clear_cache(&mut self) {
231
0
        self.module_loader.clear_cache();
232
0
    }
233
}
234
235
impl Default for ModuleResolver {
236
0
    fn default() -> Self {
237
0
        Self::new()
238
0
    }
239
}
240
241
#[cfg(test)]
242
mod 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", "42")?;
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.module_loader.search_paths.clear(); // Remove default paths
430
        resolver.add_search_path(temp_dir.path());
431
        
432
        create_test_module(&temp_dir, "test", "42")?;
433
        
434
        let initial_stats = resolver.stats();
435
        assert_eq!(initial_stats.files_loaded, 0);
436
        
437
        // Load a module
438
        let import_expr = Expr::new(
439
            ExprKind::Import {
440
                path: "test".to_string(),
441
                items: vec![ImportItem::Wildcard],
442
            },
443
            Span { start: 0, end: 0 },
444
        );
445
        
446
        resolver.resolve_imports(import_expr)?;
447
        
448
        let after_stats = resolver.stats();
449
        assert_eq!(after_stats.files_loaded, 1);
450
        assert_eq!(after_stats.cached_modules, 1);
451
        
452
        // Clear cache
453
        resolver.clear_cache();
454
        let cleared_stats = resolver.stats();
455
        assert_eq!(cleared_stats.files_loaded, 0);
456
        assert_eq!(cleared_stats.cached_modules, 0);
457
        
458
        Ok(())
459
    }
460
}