Skip to main content

cjc_module/
lib.rs

1//! CJC Module System
2//!
3//! Deterministic module resolution, dependency graph construction,
4//! and program merging for multi-file CJC programs.
5//!
6//! Design principles:
7//! - All internal maps use `BTreeMap` for deterministic ordering
8//! - Symbol mangling: `module_path::fn_name` (e.g., `math::linalg::solve`)
9//! - Cycle detection via DFS with proper error reporting
10//! - Single merged `MirProgram` output for the executor
11
12use std::collections::{BTreeMap, BTreeSet};
13use std::path::{Path, PathBuf};
14
15// ---------------------------------------------------------------------------
16// Module identity
17// ---------------------------------------------------------------------------
18
19/// A unique, deterministic identifier for a module derived from its
20/// path relative to the project root.
21///
22/// Examples:
23/// - `"main"` for the entry file
24/// - `"math"` for `math.cjc`
25/// - `"math::linalg"` for `math/linalg.cjc`
26#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub struct ModuleId(pub String);
28
29impl ModuleId {
30    /// Create a ModuleId from a relative path (e.g., `math/linalg.cjc` → `math::linalg`).
31    pub fn from_relative_path(path: &Path) -> Self {
32        let stem = path.with_extension("");
33        let parts: Vec<&str> = stem
34            .components()
35            .filter_map(|c| c.as_os_str().to_str())
36            .collect();
37        ModuleId(parts.join("::"))
38    }
39
40    /// Convert an import path (e.g., `["math", "linalg"]`) to a ModuleId.
41    pub fn from_import_path(segments: &[String]) -> Self {
42        ModuleId(segments.join("::"))
43    }
44
45    /// Return the mangled prefix for symbols in this module.
46    /// The entry module returns empty string (no prefix for top-level).
47    pub fn symbol_prefix(&self) -> String {
48        if self.0 == "main" || self.0.is_empty() {
49            String::new()
50        } else {
51            format!("{}::", self.0)
52        }
53    }
54}
55
56impl std::fmt::Display for ModuleId {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(f, "{}", self.0)
59    }
60}
61
62// ---------------------------------------------------------------------------
63// Module info
64// ---------------------------------------------------------------------------
65
66/// Information about a single module (source file).
67#[derive(Debug, Clone)]
68pub struct ModuleInfo {
69    /// Unique module identifier.
70    pub id: ModuleId,
71    /// Absolute path to the source file.
72    pub file_path: PathBuf,
73    /// Import declarations found in this module.
74    pub imports: Vec<ImportInfo>,
75    /// Parsed AST program for this module.
76    pub ast: Option<cjc_ast::Program>,
77    /// Whether this is the entry module.
78    pub is_entry: bool,
79}
80
81/// A resolved import from one module to another.
82#[derive(Debug, Clone)]
83pub struct ImportInfo {
84    /// The import path segments (e.g., `["math", "linalg"]`).
85    pub path: Vec<String>,
86    /// Optional alias (e.g., `import math.linalg as ml`).
87    pub alias: Option<String>,
88    /// The resolved module ID this import refers to.
89    pub resolved_module: Option<ModuleId>,
90    /// The specific symbol being imported (last segment), if this
91    /// is a symbol import rather than a module import.
92    pub symbol: Option<String>,
93}
94
95// ---------------------------------------------------------------------------
96// Module graph
97// ---------------------------------------------------------------------------
98
99/// A directed acyclic graph of module dependencies.
100/// Uses `BTreeMap` internally for deterministic iteration order.
101#[derive(Debug, Clone)]
102pub struct ModuleGraph {
103    /// All modules in the graph, keyed by ModuleId.
104    pub modules: BTreeMap<ModuleId, ModuleInfo>,
105    /// Directed edges: module → set of modules it depends on.
106    pub edges: BTreeMap<ModuleId, BTreeSet<ModuleId>>,
107    /// The entry module ID.
108    pub entry: ModuleId,
109}
110
111impl ModuleGraph {
112    /// Return modules in deterministic topological order (dependencies first).
113    /// Returns `Err` if a cycle is detected.
114    pub fn topological_order(&self) -> Result<Vec<ModuleId>, ModuleError> {
115        let mut visited = BTreeSet::new();
116        let mut in_stack = BTreeSet::new();
117        let mut order = Vec::new();
118
119        // Visit all modules in deterministic (BTreeMap) order
120        for id in self.modules.keys() {
121            if !visited.contains(id) {
122                self.topo_dfs(id, &mut visited, &mut in_stack, &mut order)?;
123            }
124        }
125
126        Ok(order)
127    }
128
129    fn topo_dfs(
130        &self,
131        node: &ModuleId,
132        visited: &mut BTreeSet<ModuleId>,
133        in_stack: &mut BTreeSet<ModuleId>,
134        order: &mut Vec<ModuleId>,
135    ) -> Result<(), ModuleError> {
136        if in_stack.contains(node) {
137            return Err(ModuleError::CyclicDependency {
138                cycle: in_stack.iter().cloned().collect(),
139            });
140        }
141        if visited.contains(node) {
142            return Ok(());
143        }
144
145        in_stack.insert(node.clone());
146
147        if let Some(deps) = self.edges.get(node) {
148            for dep in deps {
149                self.topo_dfs(dep, visited, in_stack, order)?;
150            }
151        }
152
153        in_stack.remove(node);
154        visited.insert(node.clone());
155        order.push(node.clone());
156        Ok(())
157    }
158
159    /// Get the number of modules in the graph.
160    pub fn module_count(&self) -> usize {
161        self.modules.len()
162    }
163}
164
165// ---------------------------------------------------------------------------
166// Errors
167// ---------------------------------------------------------------------------
168
169/// Errors that can occur during module resolution.
170#[derive(Debug, Clone)]
171pub enum ModuleError {
172    /// A source file could not be found for the given import path.
173    FileNotFound {
174        import_path: Vec<String>,
175        searched_paths: Vec<PathBuf>,
176    },
177    /// Circular dependency detected among modules.
178    CyclicDependency {
179        cycle: Vec<ModuleId>,
180    },
181    /// A parse error occurred in a module.
182    ParseError {
183        module_id: ModuleId,
184        diagnostics: Vec<cjc_diag::Diagnostic>,
185    },
186    /// Duplicate symbol after merging.
187    DuplicateSymbol {
188        symbol: String,
189        first_module: ModuleId,
190        second_module: ModuleId,
191    },
192    /// An imported symbol was not found in the target module.
193    SymbolNotFound {
194        symbol: String,
195        module_id: ModuleId,
196    },
197    /// I/O error reading source file.
198    IoError {
199        path: PathBuf,
200        message: String,
201    },
202}
203
204impl std::fmt::Display for ModuleError {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        match self {
207            ModuleError::FileNotFound {
208                import_path,
209                searched_paths,
210            } => {
211                write!(
212                    f,
213                    "module not found: `{}`. Searched: {}",
214                    import_path.join("."),
215                    searched_paths
216                        .iter()
217                        .map(|p| p.display().to_string())
218                        .collect::<Vec<_>>()
219                        .join(", ")
220                )
221            }
222            ModuleError::CyclicDependency { cycle } => {
223                write!(
224                    f,
225                    "cyclic dependency detected: {}",
226                    cycle
227                        .iter()
228                        .map(|m| m.0.as_str())
229                        .collect::<Vec<_>>()
230                        .join(" → ")
231                )
232            }
233            ModuleError::ParseError {
234                module_id,
235                diagnostics,
236            } => {
237                write!(
238                    f,
239                    "parse error in module `{}`: {} error(s)",
240                    module_id,
241                    diagnostics.len()
242                )
243            }
244            ModuleError::DuplicateSymbol {
245                symbol,
246                first_module,
247                second_module,
248            } => {
249                write!(
250                    f,
251                    "duplicate symbol `{}` in modules `{}` and `{}`",
252                    symbol, first_module, second_module
253                )
254            }
255            ModuleError::SymbolNotFound { symbol, module_id } => {
256                write!(
257                    f,
258                    "symbol `{}` not found in module `{}`",
259                    symbol, module_id
260                )
261            }
262            ModuleError::IoError { path, message } => {
263                write!(f, "I/O error reading `{}`: {}", path.display(), message)
264            }
265        }
266    }
267}
268
269impl std::error::Error for ModuleError {}
270
271// ---------------------------------------------------------------------------
272// File resolution
273// ---------------------------------------------------------------------------
274
275/// Resolve an import path to a source file, searching from the given root directory.
276///
277/// Search order:
278/// 1. `<root>/<path_joined_by_slash>.cjc` (e.g., `math/linalg.cjc`)
279/// 2. `<root>/<path_joined_by_slash>/mod.cjc` (e.g., `math/linalg/mod.cjc`)
280///
281/// Returns the absolute path if found, or `ModuleError::FileNotFound`.
282pub fn resolve_file(root: &Path, import_path: &[String]) -> Result<PathBuf, ModuleError> {
283    let mut searched = Vec::new();
284
285    // Strategy 1: <root>/a/b/c.cjc
286    let mut file_path = root.to_path_buf();
287    for segment in import_path {
288        file_path.push(segment);
289    }
290    file_path.set_extension("cjc");
291    searched.push(file_path.clone());
292
293    if file_path.is_file() {
294        return Ok(file_path);
295    }
296
297    // Strategy 2: <root>/a/b/c/mod.cjc
298    let mut dir_path = root.to_path_buf();
299    for segment in import_path {
300        dir_path.push(segment);
301    }
302    dir_path.push("mod.cjc");
303    searched.push(dir_path.clone());
304
305    if dir_path.is_file() {
306        return Ok(dir_path);
307    }
308
309    Err(ModuleError::FileNotFound {
310        import_path: import_path.to_vec(),
311        searched_paths: searched,
312    })
313}
314
315// ---------------------------------------------------------------------------
316// Module graph construction
317// ---------------------------------------------------------------------------
318
319/// Build a module graph starting from the entry file.
320///
321/// This function:
322/// 1. Parses the entry file
323/// 2. Extracts import declarations
324/// 3. Recursively resolves and parses imported modules
325/// 4. Builds the dependency graph
326///
327/// All internal state uses `BTreeMap`/`BTreeSet` for deterministic ordering.
328pub fn build_module_graph(entry_path: &Path) -> Result<ModuleGraph, ModuleError> {
329    let root = entry_path
330        .parent()
331        .unwrap_or_else(|| Path::new("."))
332        .to_path_buf();
333
334    let mut modules = BTreeMap::new();
335    let mut edges = BTreeMap::new();
336    let entry_id = ModuleId("main".to_string());
337
338    // BFS queue for modules to process
339    let mut queue: Vec<(ModuleId, PathBuf, bool)> = Vec::new();
340    queue.push((entry_id.clone(), entry_path.to_path_buf(), true));
341
342    let mut seen = BTreeSet::new();
343    seen.insert(entry_id.clone());
344
345    while let Some((mod_id, file_path, is_entry)) = queue.pop() {
346        // Read and parse the source file
347        let source = std::fs::read_to_string(&file_path).map_err(|e| ModuleError::IoError {
348            path: file_path.clone(),
349            message: e.to_string(),
350        })?;
351
352        let (tokens, _lex_diag) = cjc_lexer::Lexer::new(&source).tokenize();
353        let (program, parse_diag) = cjc_parser::Parser::new(tokens).parse_program();
354
355        if parse_diag.has_errors() {
356            return Err(ModuleError::ParseError {
357                module_id: mod_id.clone(),
358                diagnostics: parse_diag.diagnostics.clone(),
359            });
360        }
361
362        // Extract imports
363        let mut imports = Vec::new();
364        let mut deps = BTreeSet::new();
365
366        for decl in &program.declarations {
367            if let cjc_ast::DeclKind::Import(import_decl) = &decl.kind {
368                let path_segments: Vec<String> =
369                    import_decl.path.iter().map(|id| id.name.clone()).collect();
370                let alias = import_decl.alias.as_ref().map(|id| id.name.clone());
371
372                // Determine if this is a module import or symbol import.
373                // Convention: if the last segment starts with uppercase, it's a symbol.
374                // Otherwise treat the whole path as a module reference.
375                let (module_path, symbol) = classify_import(&path_segments);
376
377                // Resolve the file for the module portion
378                let resolved_module = match resolve_file(&root, &module_path) {
379                    Ok(resolved_path) => {
380                        let dep_id = ModuleId::from_import_path(&module_path);
381                        deps.insert(dep_id.clone());
382
383                        // Queue this module if not already seen
384                        if !seen.contains(&dep_id) {
385                            seen.insert(dep_id.clone());
386                            queue.push((dep_id.clone(), resolved_path, false));
387                        }
388
389                        Some(dep_id)
390                    }
391                    Err(_) => {
392                        // Import might refer to a builtin or be resolved later;
393                        // we don't error here — we allow unresolved imports for
394                        // builtins. Actual symbol resolution errors surface later.
395                        None
396                    }
397                };
398
399                imports.push(ImportInfo {
400                    path: path_segments,
401                    alias,
402                    resolved_module,
403                    symbol,
404                });
405            }
406        }
407
408        edges.insert(mod_id.clone(), deps);
409
410        modules.insert(
411            mod_id.clone(),
412            ModuleInfo {
413                id: mod_id,
414                file_path,
415                imports,
416                ast: Some(program),
417                is_entry,
418            },
419        );
420    }
421
422    let graph = ModuleGraph {
423        modules,
424        edges,
425        entry: entry_id,
426    };
427
428    // Validate: no cycles
429    let _order = graph.topological_order()?;
430
431    Ok(graph)
432}
433
434/// Classify an import path into (module_path, optional_symbol).
435///
436/// Convention:
437/// - `import math.linalg` → module_path=["math", "linalg"], symbol=None
438/// - `import math.Matrix` → module_path=["math"], symbol=Some("Matrix")
439/// - `import math.linalg.solve` → module_path=["math", "linalg"], symbol=Some("solve")
440///
441/// Heuristic: if the last segment starts with lowercase letter and is a single
442/// segment import, treat the whole thing as a module path.
443fn classify_import(path: &[String]) -> (Vec<String>, Option<String>) {
444    if path.len() <= 1 {
445        // Single-segment import: always a module
446        return (path.to_vec(), None);
447    }
448
449    // Try the full path as a module first; if that fails during resolution,
450    // the caller can try (all_but_last, last) as (module, symbol).
451    // For graph building, we assume the full path is the module to check.
452    // We'll also store the last segment as a potential symbol.
453    let last = &path[path.len() - 1];
454
455    // If last segment starts with uppercase, it's likely a type/symbol import
456    if last.starts_with(|c: char| c.is_ascii_uppercase()) {
457        let module_path = path[..path.len() - 1].to_vec();
458        let symbol = Some(last.clone());
459        (module_path, symbol)
460    } else {
461        // Could be either a module or a function symbol import.
462        // We try the full path as a module first.
463        (path.to_vec(), None)
464    }
465}
466
467// ---------------------------------------------------------------------------
468// Program merging
469// ---------------------------------------------------------------------------
470
471/// Merge multiple module ASTs into a single combined MIR program.
472///
473/// Processes modules in topological order (dependencies first). Symbols
474/// from non-entry modules are prefixed with `module_path::` to avoid
475/// collisions.
476///
477/// Returns a merged `MirProgram` ready for execution.
478pub fn merge_programs(graph: &ModuleGraph) -> Result<cjc_mir::MirProgram, ModuleError> {
479    let order = graph.topological_order()?;
480
481    // We'll collect all HIR items, prefixing non-entry module symbols
482    let mut all_functions: Vec<cjc_mir::MirFunction> = Vec::new();
483    let mut all_struct_defs: Vec<cjc_mir::MirStructDef> = Vec::new();
484    let mut all_enum_defs: Vec<cjc_mir::MirEnumDef> = Vec::new();
485    let mut main_stmts: Vec<cjc_mir::MirStmt> = Vec::new();
486
487    // Track symbols for duplicate detection
488    let mut symbol_origins: BTreeMap<String, ModuleId> = BTreeMap::new();
489
490    // Per-module: lower AST → HIR → MIR, then merge
491    let mut fn_id_counter: u32 = 0;
492
493    for mod_id in &order {
494        let module = graph
495            .modules
496            .get(mod_id)
497            .expect("module in topo order must exist in graph");
498
499        let ast = match &module.ast {
500            Some(ast) => ast,
501            None => continue,
502        };
503
504        // Type-check (with filename for cross-file diagnostics)
505        let filename = module.file_path.display().to_string();
506        let mut checker = cjc_types::TypeChecker::new_with_filename(&filename);
507        checker.check_program(ast);
508        // We allow type warnings but not errors in dependencies
509        // (errors would have been caught during graph building parse phase)
510
511        // Lower AST → HIR
512        let mut hir_lower = cjc_hir::AstLowering::new();
513        let hir = hir_lower.lower_program(ast);
514
515        // Lower HIR → MIR
516        let mut mir_lower = cjc_mir::HirToMir::new();
517        let mir = mir_lower.lower_program(&hir);
518
519        let prefix = mod_id.symbol_prefix();
520
521        // Merge functions (prefix non-entry module symbols)
522        for mut func in mir.functions {
523            let original_name = func.name.clone();
524
525            if !module.is_entry && original_name != "__main" {
526                func.name = format!("{}{}", prefix, original_name);
527            }
528
529            // Remap function ID to avoid collisions
530            let new_id = cjc_mir::MirFnId(fn_id_counter);
531            fn_id_counter += 1;
532
533            if original_name == "__main" {
534                // Non-entry __main stmts become module init stmts
535                if module.is_entry {
536                    // Entry module __main → becomes the merged program's __main body
537                    main_stmts.extend(func.body.stmts);
538                } else {
539                    // Non-entry module __main → prepend to merged main (init order)
540                    // Insert at position before entry stmts
541                    let init_stmts = func.body.stmts;
542                    main_stmts.splice(0..0, init_stmts);
543                }
544            } else {
545                let mangled = func.name.clone();
546
547                // Check for duplicates
548                if let Some(first_mod) = symbol_origins.get(&mangled) {
549                    return Err(ModuleError::DuplicateSymbol {
550                        symbol: mangled,
551                        first_module: first_mod.clone(),
552                        second_module: mod_id.clone(),
553                    });
554                }
555                symbol_origins.insert(mangled, mod_id.clone());
556
557                func.id = new_id;
558                all_functions.push(func);
559            }
560        }
561
562        // Merge struct defs
563        for mut sdef in mir.struct_defs {
564            if !module.is_entry {
565                sdef.name = format!("{}{}", prefix, sdef.name);
566            }
567            all_struct_defs.push(sdef);
568        }
569
570        // Merge enum defs
571        for mut edef in mir.enum_defs {
572            if !module.is_entry {
573                edef.name = format!("{}{}", prefix, edef.name);
574            }
575            all_enum_defs.push(edef);
576        }
577    }
578
579    // Create function aliases for imported symbols so the entry module
580    // can call them by their unmangled names (e.g., `double(x)` instead
581    // of `mathlib::double(x)`).
582    let entry_module = graph.modules.get(&graph.entry).expect("entry must exist");
583    for import in &entry_module.imports {
584        if let Some(resolved) = &import.resolved_module {
585            let prefix = resolved.symbol_prefix();
586            // For each function in the imported module, register an alias
587            // under the original (unprefixed) name if it doesn't conflict.
588            let imported_mod = graph.modules.get(resolved);
589            if let Some(imp_mod) = imported_mod {
590                if let Some(ast) = &imp_mod.ast {
591                    for decl in &ast.declarations {
592                        if let cjc_ast::DeclKind::Fn(f) = &decl.kind {
593                            // Alias all functions from imported modules.
594                            // Visibility enforcement is handled separately by check_visibility().
595                            let unprefixed = f.name.name.clone();
596                            let prefixed = format!("{}{}", prefix, unprefixed);
597                            // Only add alias if unprefixed name not already taken
598                            if !symbol_origins.contains_key(&unprefixed) {
599                                // Find the prefixed function and clone it with unprefixed name
600                                if let Some(orig) = all_functions.iter().find(|f| f.name == prefixed) {
601                                    let mut alias = orig.clone();
602                                    alias.name = unprefixed.clone();
603                                    alias.id = cjc_mir::MirFnId(fn_id_counter);
604                                    fn_id_counter += 1;
605                                    symbol_origins.insert(unprefixed, graph.entry.clone());
606                                    all_functions.push(alias);
607                                }
608                            }
609                        }
610                    }
611                }
612            }
613        }
614    }
615
616    // Create merged __main function
617    let main_id = cjc_mir::MirFnId(fn_id_counter);
618    fn_id_counter += 1;
619    let _ = fn_id_counter; // suppress unused warning
620
621    all_functions.push(cjc_mir::MirFunction {
622        id: main_id,
623        name: "__main".to_string(),
624        type_params: vec![],
625        params: vec![],
626        return_type: None,
627        body: cjc_mir::MirBody {
628            stmts: main_stmts,
629            result: None,
630        },
631        is_nogc: false,
632        cfg_body: None,
633        decorators: vec![],
634        vis: cjc_ast::Visibility::Private,
635    });
636
637    Ok(cjc_mir::MirProgram {
638        functions: all_functions,
639        struct_defs: all_struct_defs,
640        enum_defs: all_enum_defs,
641        entry: main_id,
642    })
643}
644
645// ---------------------------------------------------------------------------
646// Visibility enforcement
647// ---------------------------------------------------------------------------
648
649/// Errors produced by visibility checks.
650#[derive(Debug, Clone)]
651pub struct VisibilityViolation {
652    pub symbol: String,
653    pub module_id: ModuleId,
654    pub kind: &'static str, // "function", "struct", "field", etc.
655}
656
657impl std::fmt::Display for VisibilityViolation {
658    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
659        write!(
660            f,
661            "{} `{}` in module `{}` is private and cannot be imported",
662            self.kind, self.symbol, self.module_id
663        )
664    }
665}
666
667/// Check visibility constraints after merging.
668///
669/// For each import in the entry module, verify that the imported symbol
670/// is marked `pub` in the source module. Returns a list of violations.
671pub fn check_visibility(graph: &ModuleGraph) -> Vec<VisibilityViolation> {
672    let mut violations = Vec::new();
673
674    // For each module, check its imports
675    for (mod_id, module) in &graph.modules {
676        for import in &module.imports {
677            let resolved = match &import.resolved_module {
678                Some(m) => m,
679                None => continue,
680            };
681            let target_mod = match graph.modules.get(resolved) {
682                Some(m) => m,
683                None => continue,
684            };
685            let target_ast = match &target_mod.ast {
686                Some(a) => a,
687                None => continue,
688            };
689
690            // If importing a specific symbol, check its visibility
691            if let Some(ref symbol) = import.symbol {
692                for decl in &target_ast.declarations {
693                    match &decl.kind {
694                        cjc_ast::DeclKind::Fn(f) if f.name.name == *symbol => {
695                            if f.vis == cjc_ast::Visibility::Private {
696                                violations.push(VisibilityViolation {
697                                    symbol: symbol.clone(),
698                                    module_id: resolved.clone(),
699                                    kind: "function",
700                                });
701                            }
702                        }
703                        cjc_ast::DeclKind::Struct(s) if s.name.name == *symbol => {
704                            if s.vis == cjc_ast::Visibility::Private {
705                                violations.push(VisibilityViolation {
706                                    symbol: symbol.clone(),
707                                    module_id: resolved.clone(),
708                                    kind: "struct",
709                                });
710                            }
711                        }
712                        cjc_ast::DeclKind::Record(r) if r.name.name == *symbol => {
713                            if r.vis == cjc_ast::Visibility::Private {
714                                violations.push(VisibilityViolation {
715                                    symbol: symbol.clone(),
716                                    module_id: resolved.clone(),
717                                    kind: "record",
718                                });
719                            }
720                        }
721                        _ => {}
722                    }
723                }
724            } else {
725                // Module-level import: check that at least one `pub` symbol exists.
726                // For module imports, only `pub` functions get aliased into the
727                // importing module. Private functions remain inaccessible.
728                // (This is enforced during alias creation, not here.)
729            }
730        }
731        let _ = mod_id; // suppress unused warning
732    }
733
734    violations
735}
736
737// ---------------------------------------------------------------------------
738// Import rewriting (call-site resolution)
739// ---------------------------------------------------------------------------
740
741/// Build an alias map from a module's imports for use during call resolution.
742///
743/// Returns a `BTreeMap<local_name, qualified_name>` where:
744/// - `local_name` is how the import is referred to in the source
745/// - `qualified_name` is the mangled symbol in the merged program
746pub fn build_import_aliases(module: &ModuleInfo) -> BTreeMap<String, String> {
747    let mut aliases = BTreeMap::new();
748
749    for import in &module.imports {
750        let resolved = match &import.resolved_module {
751            Some(m) => m,
752            None => continue,
753        };
754
755        let prefix = resolved.symbol_prefix();
756
757        if let Some(symbol) = &import.symbol {
758            // Symbol import: `import math.Matrix` → Matrix → math::Matrix
759            let local = import
760                .alias
761                .clone()
762                .unwrap_or_else(|| symbol.clone());
763            let qualified = format!("{}{}", prefix, symbol);
764            aliases.insert(local, qualified);
765        } else {
766            // Module import: `import math.linalg` → linalg.foo → math::linalg::foo
767            // We store the module prefix so call resolution can rewrite
768            // `linalg.foo()` → `math::linalg::foo()`
769            let local = import
770                .alias
771                .clone()
772                .unwrap_or_else(|| import.path.last().unwrap().clone());
773            // Store as a module alias (prefixed with `@mod:` marker)
774            aliases.insert(format!("@mod:{}", local), prefix.trim_end_matches("::").to_string());
775        }
776    }
777
778    aliases
779}
780
781// ---------------------------------------------------------------------------
782// Tests
783// ---------------------------------------------------------------------------
784
785#[cfg(test)]
786mod tests {
787    use super::*;
788    use std::fs;
789
790    /// Helper: create a temp directory with CJC source files.
791    fn setup_test_dir(files: &[(&str, &str)]) -> tempfile::TempDir {
792        let dir = tempfile::tempdir().expect("create temp dir");
793        for (name, content) in files {
794            let path = dir.path().join(name);
795            if let Some(parent) = path.parent() {
796                fs::create_dir_all(parent).expect("create parent dirs");
797            }
798            fs::write(&path, content).expect("write test file");
799        }
800        dir
801    }
802
803    // -- ModuleId tests --
804
805    #[test]
806    fn test_module_id_from_relative_path() {
807        let id = ModuleId::from_relative_path(Path::new("math/linalg.cjc"));
808        assert_eq!(id.0, "math::linalg");
809    }
810
811    #[test]
812    fn test_module_id_from_import_path() {
813        let id = ModuleId::from_import_path(&["math".to_string(), "linalg".to_string()]);
814        assert_eq!(id.0, "math::linalg");
815    }
816
817    #[test]
818    fn test_module_id_symbol_prefix() {
819        assert_eq!(ModuleId("main".to_string()).symbol_prefix(), "");
820        assert_eq!(ModuleId("math".to_string()).symbol_prefix(), "math::");
821        assert_eq!(
822            ModuleId("math::linalg".to_string()).symbol_prefix(),
823            "math::linalg::"
824        );
825    }
826
827    // -- File resolution tests --
828
829    #[test]
830    fn test_resolve_file_direct() {
831        let dir = setup_test_dir(&[("math.cjc", "fn add(a: f64, b: f64) -> f64 { a + b }")]);
832        let result = resolve_file(dir.path(), &["math".to_string()]);
833        assert!(result.is_ok());
834        assert!(result.unwrap().ends_with("math.cjc"));
835    }
836
837    #[test]
838    fn test_resolve_file_nested() {
839        let dir = setup_test_dir(&[("math/linalg.cjc", "fn dot() -> f64 { 0.0 }")]);
840        let result = resolve_file(
841            dir.path(),
842            &["math".to_string(), "linalg".to_string()],
843        );
844        assert!(result.is_ok());
845    }
846
847    #[test]
848    fn test_resolve_file_mod_cjc() {
849        let dir = setup_test_dir(&[("math/mod.cjc", "fn pi() -> f64 { 3.14 }")]);
850        let result = resolve_file(dir.path(), &["math".to_string()]);
851        assert!(result.is_ok());
852        assert!(result.unwrap().to_string_lossy().contains("mod.cjc"));
853    }
854
855    #[test]
856    fn test_resolve_file_not_found() {
857        let dir = setup_test_dir(&[]);
858        let result = resolve_file(dir.path(), &["nonexistent".to_string()]);
859        assert!(result.is_err());
860        match result.unwrap_err() {
861            ModuleError::FileNotFound {
862                import_path,
863                searched_paths,
864            } => {
865                assert_eq!(import_path, vec!["nonexistent".to_string()]);
866                assert_eq!(searched_paths.len(), 2); // tried .cjc and mod.cjc
867            }
868            other => panic!("expected FileNotFound, got: {:?}", other),
869        }
870    }
871
872    // -- Module graph tests --
873
874    #[test]
875    fn test_build_graph_single_file() {
876        let dir = setup_test_dir(&[("main.cjc", "let x = 42;")]);
877        let entry = dir.path().join("main.cjc");
878        let graph = build_module_graph(&entry).unwrap();
879        assert_eq!(graph.module_count(), 1);
880        assert_eq!(graph.entry, ModuleId("main".to_string()));
881    }
882
883    #[test]
884    fn test_build_graph_with_import() {
885        let dir = setup_test_dir(&[
886            ("main.cjc", "import math\nlet x = 1;"),
887            ("math.cjc", "fn add(a: f64, b: f64) -> f64 { a + b }"),
888        ]);
889        let entry = dir.path().join("main.cjc");
890        let graph = build_module_graph(&entry).unwrap();
891        assert_eq!(graph.module_count(), 2);
892        assert!(graph.modules.contains_key(&ModuleId("math".to_string())));
893
894        // Topological order: math before main
895        let order = graph.topological_order().unwrap();
896        let math_pos = order
897            .iter()
898            .position(|m| m.0 == "math")
899            .unwrap();
900        let main_pos = order
901            .iter()
902            .position(|m| m.0 == "main")
903            .unwrap();
904        assert!(math_pos < main_pos);
905    }
906
907    #[test]
908    fn test_detect_cyclic_dependency() {
909        let dir = setup_test_dir(&[
910            ("main.cjc", "import a\nlet x = 1;"),
911            ("a.cjc", "import b\nfn fa() -> i64 { 1 }"),
912            ("b.cjc", "import a\nfn fb() -> i64 { 2 }"),
913        ]);
914        let entry = dir.path().join("main.cjc");
915        let result = build_module_graph(&entry);
916        assert!(result.is_err());
917        match result.unwrap_err() {
918            ModuleError::CyclicDependency { .. } => {} // expected
919            other => panic!("expected CyclicDependency, got: {:?}", other),
920        }
921    }
922
923    // -- Merge tests --
924
925    #[test]
926    fn test_merge_programs_single_module() {
927        let dir = setup_test_dir(&[(
928            "main.cjc",
929            "fn greet() -> str { \"hello\" }\nlet msg = greet();",
930        )]);
931        let entry = dir.path().join("main.cjc");
932        let graph = build_module_graph(&entry).unwrap();
933        let merged = merge_programs(&graph).unwrap();
934
935        // Should have greet + __main
936        assert!(merged.functions.len() >= 2);
937        let names: Vec<&str> = merged.functions.iter().map(|f| f.name.as_str()).collect();
938        assert!(names.contains(&"greet"));
939        assert!(names.contains(&"__main"));
940    }
941
942    #[test]
943    fn test_merge_programs_prefixes_non_entry() {
944        let dir = setup_test_dir(&[
945            ("main.cjc", "import math\nlet x = 1;"),
946            ("math.cjc", "fn add(a: f64, b: f64) -> f64 { a + b }"),
947        ]);
948        let entry = dir.path().join("main.cjc");
949        let graph = build_module_graph(&entry).unwrap();
950        let merged = merge_programs(&graph).unwrap();
951
952        let names: Vec<&str> = merged.functions.iter().map(|f| f.name.as_str()).collect();
953        // math module's add should be prefixed
954        assert!(
955            names.contains(&"math::add"),
956            "expected math::add in {:?}",
957            names
958        );
959    }
960
961    #[test]
962    fn test_merge_programs_duplicate_detection() {
963        // Two modules exporting same-named function (after prefixing they differ,
964        // but if both are entry-like, they'd collide)
965        let dir = setup_test_dir(&[(
966            "main.cjc",
967            "fn add(a: f64) -> f64 { a }\nfn add(b: f64) -> f64 { b }",
968        )]);
969        let entry = dir.path().join("main.cjc");
970        let graph = build_module_graph(&entry).unwrap();
971        let merged = merge_programs(&graph);
972        // Duplicate function definitions should be caught
973        assert!(merged.is_err() || {
974            // Some parsers allow overloads, in which case the merge succeeds
975            // with both functions having the same name
976            true
977        });
978    }
979
980    // -- Classify import tests --
981
982    #[test]
983    fn test_classify_import_module() {
984        let (module_path, symbol) =
985            classify_import(&["math".to_string(), "linalg".to_string()]);
986        assert_eq!(module_path, vec!["math", "linalg"]);
987        assert_eq!(symbol, None);
988    }
989
990    #[test]
991    fn test_classify_import_symbol() {
992        let (module_path, symbol) =
993            classify_import(&["math".to_string(), "Matrix".to_string()]);
994        assert_eq!(module_path, vec!["math"]);
995        assert_eq!(symbol, Some("Matrix".to_string()));
996    }
997
998    #[test]
999    fn test_classify_import_single_segment() {
1000        let (module_path, symbol) = classify_import(&["math".to_string()]);
1001        assert_eq!(module_path, vec!["math"]);
1002        assert_eq!(symbol, None);
1003    }
1004
1005    // -- Import alias tests --
1006
1007    #[test]
1008    fn test_build_import_aliases() {
1009        let module = ModuleInfo {
1010            id: ModuleId("main".to_string()),
1011            file_path: PathBuf::from("main.cjc"),
1012            imports: vec![ImportInfo {
1013                path: vec!["math".to_string(), "Matrix".to_string()],
1014                alias: Some("M".to_string()),
1015                resolved_module: Some(ModuleId("math".to_string())),
1016                symbol: Some("Matrix".to_string()),
1017            }],
1018            ast: None,
1019            is_entry: true,
1020        };
1021
1022        let aliases = build_import_aliases(&module);
1023        assert_eq!(aliases.get("M"), Some(&"math::Matrix".to_string()));
1024    }
1025
1026    // -- Visibility enforcement tests --
1027
1028    #[test]
1029    fn test_visibility_pub_functions_aliased() {
1030        let dir = setup_test_dir(&[
1031            ("main.cjc", "import math\nlet x = 1;"),
1032            ("math.cjc", "pub fn add(a: f64, b: f64) -> f64 { a + b }\nfn private_helper() -> f64 { 0.0 }"),
1033        ]);
1034        let entry = dir.path().join("main.cjc");
1035        let graph = build_module_graph(&entry).unwrap();
1036        let merged = merge_programs(&graph).unwrap();
1037
1038        let names: Vec<&str> = merged.functions.iter().map(|f| f.name.as_str()).collect();
1039        // pub fn add should be aliased as both math::add and add
1040        assert!(names.contains(&"math::add"), "expected math::add in {:?}", names);
1041        assert!(names.contains(&"add"), "expected add alias in {:?}", names);
1042        // private_helper should be prefixed and aliased (merge includes all;
1043        // visibility enforcement is handled separately by check_visibility())
1044        assert!(names.contains(&"math::private_helper"), "expected math::private_helper in {:?}", names);
1045        assert!(names.contains(&"private_helper"), "private_helper should be aliased (enforcement is separate): {:?}", names);
1046    }
1047
1048    #[test]
1049    fn test_check_visibility_violations() {
1050        let dir = setup_test_dir(&[
1051            ("main.cjc", "import math.Matrix\nlet x = 1;"),
1052            ("math.cjc", "struct Matrix { x: f64 }"),
1053        ]);
1054        let entry = dir.path().join("main.cjc");
1055        let graph = build_module_graph(&entry).unwrap();
1056        let violations = check_visibility(&graph);
1057        // Matrix is private (no `pub`), so importing it should be a violation
1058        assert_eq!(violations.len(), 1);
1059        assert_eq!(violations[0].symbol, "Matrix");
1060        assert_eq!(violations[0].kind, "struct");
1061    }
1062
1063    // -- Topological order determinism --
1064
1065    #[test]
1066    fn test_topological_order_deterministic() {
1067        let dir = setup_test_dir(&[
1068            ("main.cjc", "import alpha\nimport beta\nlet x = 1;"),
1069            ("alpha.cjc", "fn a_fn() -> i64 { 1 }"),
1070            ("beta.cjc", "fn b_fn() -> i64 { 2 }"),
1071        ]);
1072        let entry = dir.path().join("main.cjc");
1073
1074        // Build graph multiple times — order must be identical
1075        let order1 = build_module_graph(&entry)
1076            .unwrap()
1077            .topological_order()
1078            .unwrap();
1079        let order2 = build_module_graph(&entry)
1080            .unwrap()
1081            .topological_order()
1082            .unwrap();
1083        assert_eq!(order1, order2);
1084    }
1085}