Coverage Report

Created: 2025-09-05 15: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
    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
104
    pub fn new() -> Self {
54
104
        Self {
55
104
            module_loader: ModuleLoader::new(),
56
104
        }
57
104
    }
58
    
59
    /// Add a directory to the module search path
60
    /// 
61
    /// # Arguments
62
    /// 
63
    /// * `path` - Directory to search for modules
64
4
    pub fn add_search_path<P: AsRef<std::path::Path>>(&mut self, path: P) {
65
4
        self.module_loader.add_search_path(path);
66
4
    }
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
101
    pub fn resolve_imports(&mut self, ast: Expr) -> Result<Expr> {
88
101
        self.resolve_expr(ast)
89
101
    }
90
    
91
    /// Recursively resolve imports in an expression
92
156
    fn resolve_expr(&mut self, expr: Expr) -> Result<Expr> {
93
156
        match expr.kind {
94
5
            ExprKind::Import { ref path, ref items } => {
95
                // Check if this is a file import (no :: and not std:: or http)
96
5
                if self.is_file_import(path) {
97
                    // Load the module file
98
3
                    let parsed_module = self.module_loader.load_module(path)
99
3
                        .with_context(|| format!(
"Failed to resolve import '{path}'"0
))
?0
;
100
                    
101
                    // Recursively resolve imports in the loaded module
102
3
                    let resolved_module_ast = self.resolve_expr(parsed_module.ast)
?0
;
103
                    
104
                    // Create an inline module declaration
105
3
                    let module_expr = Expr::new(
106
3
                        ExprKind::Module {
107
3
                            name: path.clone(),
108
3
                            body: Box::new(resolved_module_ast),
109
3
                        },
110
3
                        expr.span,
111
                    );
112
                    
113
                    // Create a use statement to import from the module
114
3
                    let use_statement = if items.iter().any(|item| matches!(item, ImportItem::Wildcard)) || 
items0
.
is_empty0
() {
115
                        // Wildcard import: use module::*;
116
3
                        Expr::new(
117
3
                            ExprKind::Import {
118
3
                                path: path.clone(),
119
3
                                items: vec![ImportItem::Wildcard],
120
3
                            },
121
3
                            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
3
                    Ok(Expr::new(
130
3
                        ExprKind::Block(vec![module_expr, use_statement]),
131
3
                        expr.span,
132
3
                    ))
133
                } else {
134
                    // Not a file import, keep as-is
135
2
                    Ok(expr)
136
                }
137
            }
138
22
            ExprKind::Block(exprs) => {
139
                // Resolve imports in all block expressions
140
22
                let resolved_exprs: Result<Vec<_>> = exprs
141
22
                    .into_iter()
142
26
                    .
map22
(|e| self.resolve_expr(e))
143
22
                    .collect();
144
22
                Ok(Expr::new(ExprKind::Block(resolved_exprs
?0
), 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
11
                name, 
159
11
                type_params, 
160
11
                params, 
161
11
                body, 
162
11
                is_async,
163
11
                return_type,
164
11
                is_pub,
165
            } => {
166
                // Resolve imports in function body
167
11
                let resolved_body = self.resolve_expr(*body)
?0
;
168
11
                Ok(Expr::new(
169
11
                    ExprKind::Function {
170
11
                        name,
171
11
                        type_params,
172
11
                        params,
173
11
                        body: Box::new(resolved_body),
174
11
                        is_async,
175
11
                        return_type,
176
11
                        is_pub,
177
11
                    },
178
11
                    expr.span,
179
11
                ))
180
            }
181
5
            ExprKind::If { condition, then_branch, else_branch } => {
182
5
                let resolved_condition = self.resolve_expr(*condition)
?0
;
183
5
                let resolved_then = self.resolve_expr(*then_branch)
?0
;
184
5
                let resolved_else = else_branch.map(|e| self.resolve_expr(*e)).transpose()
?0
;
185
5
                Ok(Expr::new(
186
5
                    ExprKind::If {
187
5
                        condition: Box::new(resolved_condition),
188
5
                        then_branch: Box::new(resolved_then),
189
5
                        else_branch: resolved_else.map(Box::new),
190
5
                    },
191
5
                    expr.span,
192
5
                ))
193
            }
194
            // For other expression types, recursively process children as needed
195
            // For now, just return the expression as-is
196
113
            _ => Ok(expr),
197
        }
198
156
    }
199
    
200
    /// Check if an import path represents a file import
201
13
    fn is_file_import(&self, path: &str) -> bool {
202
13
        !path.contains("::")
203
9
            && !path.starts_with("std::")
204
9
            && !path.starts_with("http")
205
7
            && !path.is_empty()
206
13
    }
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
4
    pub fn stats(&self) -> crate::backend::module_loader::ModuleLoaderStats {
224
4
        self.module_loader.stats()
225
4
    }
226
    
227
    /// Clear the module cache
228
    /// 
229
    /// Forces all modules to be reloaded from disk on next access.
230
1
    pub fn clear_cache(&mut self) {
231
1
        self.module_loader.clear_cache();
232
1
    }
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
3
    fn create_test_module(temp_dir: &TempDir, name: &str, content: &str) -> Result<()> {
249
3
        let file_path = temp_dir.path().join(format!("{name}.ruchy"));
250
3
        fs::write(file_path, content)
?0
;
251
3
        Ok(())
252
3
    }
253
254
    #[test]
255
1
    fn test_module_resolver_creation() {
256
1
        let resolver = ModuleResolver::new();
257
1
        let stats = resolver.stats();
258
1
        assert_eq!(stats.cached_modules, 0);
259
1
    }
260
261
    #[test] 
262
1
    fn test_add_search_path() {
263
1
        let mut resolver = ModuleResolver::new();
264
1
        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
1
    }
268
269
    #[test]
270
1
    fn test_is_file_import() {
271
1
        let resolver = ModuleResolver::new();
272
        
273
        // Should be file imports
274
1
        assert!(resolver.is_file_import("math"));
275
1
        assert!(resolver.is_file_import("utils"));
276
1
        assert!(resolver.is_file_import("snake_case_module"));
277
        
278
        // Should NOT be file imports
279
1
        assert!(!resolver.is_file_import("std::collections"));
280
1
        assert!(!resolver.is_file_import("std::io::Read"));
281
1
        assert!(!resolver.is_file_import("https://example.com/module.ruchy"));
282
1
        assert!(!resolver.is_file_import("http://localhost/module.ruchy"));
283
1
        assert!(!resolver.is_file_import(""));
284
1
    }
285
286
    #[test]
287
1
    fn test_resolve_simple_file_import() -> Result<()> {
288
1
        let temp_dir = TempDir::new()
?0
;
289
1
        let mut resolver = ModuleResolver::new();
290
1
        resolver.add_search_path(temp_dir.path());
291
        
292
        // Create a simple math module
293
1
        create_test_module(&temp_dir, "math", r"
294
1
            pub fun add(a: i32, b: i32) -> i32 {
295
1
                a + b
296
1
            }
297
1
        ")
?0
;
298
        
299
        // Create an import expression
300
1
        let import_expr = Expr::new(
301
1
            ExprKind::Import {
302
1
                path: "math".to_string(),
303
1
                items: vec![ImportItem::Wildcard],
304
1
            },
305
1
            Span { start: 0, end: 0 },
306
        );
307
        
308
        // Resolve the import
309
1
        let resolved_expr = resolver.resolve_imports(import_expr)
?0
;
310
        
311
        // Should be converted to a Block with Module declaration and use statement
312
1
        match resolved_expr.kind {
313
1
            ExprKind::Block(exprs) => {
314
1
                assert_eq!(exprs.len(), 2);
315
                // First should be Module declaration
316
1
                match &exprs[0].kind {
317
1
                    ExprKind::Module { name, .. } => {
318
1
                        assert_eq!(name, "math");
319
                    }
320
0
                    _ => unreachable!("Expected first element to be Module, got {:?}", exprs[0].kind),
321
                }
322
                // Second should be use statement
323
1
                match &exprs[1].kind {
324
1
                    ExprKind::Import { path, items } => {
325
1
                        assert_eq!(path, "math");
326
1
                        assert_eq!(items.len(), 1);
327
1
                        assert!(
matches!0
(items[0], ImportItem::Wildcard));
328
                    }
329
0
                    _ => unreachable!("Expected second element to be Import, got {:?}", exprs[1].kind),
330
                }
331
            }
332
0
            _ => unreachable!("Expected Block expression, got {:?}", resolved_expr.kind),
333
        }
334
        
335
1
        Ok(())
336
1
    }
337
338
    #[test]
339
1
    fn test_resolve_non_file_import() -> Result<()> {
340
1
        let mut resolver = ModuleResolver::new();
341
        
342
        // Create a standard library import
343
1
        let import_expr = Expr::new(
344
1
            ExprKind::Import {
345
1
                path: "std::collections".to_string(),
346
1
                items: vec![ImportItem::Named("HashMap".to_string())],
347
1
            },
348
1
            Span { start: 0, end: 0 },
349
        );
350
        
351
        // Resolve the import - should remain unchanged
352
1
        let resolved_expr = resolver.resolve_imports(import_expr)
?0
;
353
        
354
1
        match resolved_expr.kind {
355
1
            ExprKind::Import { path, items } => {
356
1
                assert_eq!(path, "std::collections");
357
1
                assert_eq!(items.len(), 1);
358
            }
359
0
            _ => unreachable!("Expected Import expression to remain unchanged"),
360
        }
361
        
362
1
        Ok(())
363
1
    }
364
365
    #[test]
366
1
    fn test_resolve_block_with_imports() -> Result<()> {
367
1
        let temp_dir = TempDir::new()
?0
;
368
1
        let mut resolver = ModuleResolver::new();
369
1
        resolver.add_search_path(temp_dir.path());
370
        
371
1
        create_test_module(&temp_dir, "math", "pub fun add() {}")
?0
;
372
        
373
        // Create a block with mixed imports
374
1
        let block_expr = Expr::new(
375
1
            ExprKind::Block(vec![
376
1
                Expr::new(
377
1
                    ExprKind::Import {
378
1
                        path: "math".to_string(),
379
1
                        items: vec![ImportItem::Wildcard],
380
1
                    },
381
1
                    Span { start: 0, end: 0 },
382
1
                ),
383
1
                Expr::new(
384
1
                    ExprKind::Import {
385
1
                        path: "std::io".to_string(),
386
1
                        items: vec![ImportItem::Named("Read".to_string())],
387
1
                    },
388
1
                    Span { start: 0, end: 0 },
389
1
                ),
390
1
                Expr::new(
391
1
                    ExprKind::Literal(Literal::Integer(42)),
392
1
                    Span { start: 0, end: 0 },
393
1
                ),
394
1
            ]),
395
1
            Span { start: 0, end: 0 },
396
        );
397
        
398
1
        let resolved_block = resolver.resolve_imports(block_expr)
?0
;
399
        
400
1
        if let ExprKind::Block(exprs) = resolved_block.kind {
401
1
            assert_eq!(exprs.len(), 3);
402
            
403
            // First should be Block containing Module and use statement (from file import)
404
1
            match &exprs[0].kind {
405
1
                ExprKind::Block(inner_exprs) => {
406
1
                    assert_eq!(inner_exprs.len(), 2);
407
1
                    assert!(
matches!0
(inner_exprs[0].kind, ExprKind::Module { .. }));
408
1
                    assert!(
matches!0
(inner_exprs[1].kind, ExprKind::Import { .. }));
409
                }
410
0
                _ => 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
1
            assert!(
matches!0
(exprs[1].kind, ExprKind::Import { .. }));
415
            
416
            // Third should remain as Literal
417
1
            assert!(
matches!0
(exprs[2].kind, ExprKind::Literal(Literal::Integer(42))));
418
        } else {
419
0
            unreachable!("Expected Block expression");
420
        }
421
        
422
1
        Ok(())
423
1
    }
424
425
    #[test]
426
1
    fn test_stats_and_cache() -> Result<()> {
427
1
        let temp_dir = TempDir::new()
?0
;
428
1
        let mut resolver = ModuleResolver::new();
429
1
        resolver.add_search_path(temp_dir.path());
430
        
431
1
        create_test_module(&temp_dir, "test", "pub fun test() {}")
?0
;
432
        
433
1
        let initial_stats = resolver.stats();
434
1
        assert_eq!(initial_stats.files_loaded, 0);
435
        
436
        // Load a module
437
1
        let import_expr = Expr::new(
438
1
            ExprKind::Import {
439
1
                path: "test".to_string(),
440
1
                items: vec![ImportItem::Wildcard],
441
1
            },
442
1
            Span { start: 0, end: 0 },
443
        );
444
        
445
1
        resolver.resolve_imports(import_expr)
?0
;
446
        
447
1
        let after_stats = resolver.stats();
448
1
        assert_eq!(after_stats.files_loaded, 1);
449
1
        assert_eq!(after_stats.cached_modules, 1);
450
        
451
        // Clear cache
452
1
        resolver.clear_cache();
453
1
        let cleared_stats = resolver.stats();
454
1
        assert_eq!(cleared_stats.files_loaded, 0);
455
1
        assert_eq!(cleared_stats.cached_modules, 0);
456
        
457
1
        Ok(())
458
1
    }
459
}