1use std::collections::{BTreeMap, BTreeSet};
13use std::path::{Path, PathBuf};
14
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub struct ModuleId(pub String);
28
29impl ModuleId {
30 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 pub fn from_import_path(segments: &[String]) -> Self {
42 ModuleId(segments.join("::"))
43 }
44
45 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#[derive(Debug, Clone)]
68pub struct ModuleInfo {
69 pub id: ModuleId,
71 pub file_path: PathBuf,
73 pub imports: Vec<ImportInfo>,
75 pub ast: Option<cjc_ast::Program>,
77 pub is_entry: bool,
79}
80
81#[derive(Debug, Clone)]
83pub struct ImportInfo {
84 pub path: Vec<String>,
86 pub alias: Option<String>,
88 pub resolved_module: Option<ModuleId>,
90 pub symbol: Option<String>,
93}
94
95#[derive(Debug, Clone)]
102pub struct ModuleGraph {
103 pub modules: BTreeMap<ModuleId, ModuleInfo>,
105 pub edges: BTreeMap<ModuleId, BTreeSet<ModuleId>>,
107 pub entry: ModuleId,
109}
110
111impl ModuleGraph {
112 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 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 pub fn module_count(&self) -> usize {
161 self.modules.len()
162 }
163}
164
165#[derive(Debug, Clone)]
171pub enum ModuleError {
172 FileNotFound {
174 import_path: Vec<String>,
175 searched_paths: Vec<PathBuf>,
176 },
177 CyclicDependency {
179 cycle: Vec<ModuleId>,
180 },
181 ParseError {
183 module_id: ModuleId,
184 diagnostics: Vec<cjc_diag::Diagnostic>,
185 },
186 DuplicateSymbol {
188 symbol: String,
189 first_module: ModuleId,
190 second_module: ModuleId,
191 },
192 SymbolNotFound {
194 symbol: String,
195 module_id: ModuleId,
196 },
197 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
271pub fn resolve_file(root: &Path, import_path: &[String]) -> Result<PathBuf, ModuleError> {
283 let mut searched = Vec::new();
284
285 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 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
315pub 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 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 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 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 let (module_path, symbol) = classify_import(&path_segments);
376
377 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 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 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 let _order = graph.topological_order()?;
430
431 Ok(graph)
432}
433
434fn classify_import(path: &[String]) -> (Vec<String>, Option<String>) {
444 if path.len() <= 1 {
445 return (path.to_vec(), None);
447 }
448
449 let last = &path[path.len() - 1];
454
455 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 (path.to_vec(), None)
464 }
465}
466
467pub fn merge_programs(graph: &ModuleGraph) -> Result<cjc_mir::MirProgram, ModuleError> {
479 let order = graph.topological_order()?;
480
481 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 let mut symbol_origins: BTreeMap<String, ModuleId> = BTreeMap::new();
489
490 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 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 let mut hir_lower = cjc_hir::AstLowering::new();
513 let hir = hir_lower.lower_program(ast);
514
515 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 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 let new_id = cjc_mir::MirFnId(fn_id_counter);
531 fn_id_counter += 1;
532
533 if original_name == "__main" {
534 if module.is_entry {
536 main_stmts.extend(func.body.stmts);
538 } else {
539 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 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 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 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 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 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 let unprefixed = f.name.name.clone();
596 let prefixed = format!("{}{}", prefix, unprefixed);
597 if !symbol_origins.contains_key(&unprefixed) {
599 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 let main_id = cjc_mir::MirFnId(fn_id_counter);
618 fn_id_counter += 1;
619 let _ = fn_id_counter; 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#[derive(Debug, Clone)]
651pub struct VisibilityViolation {
652 pub symbol: String,
653 pub module_id: ModuleId,
654 pub kind: &'static str, }
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
667pub fn check_visibility(graph: &ModuleGraph) -> Vec<VisibilityViolation> {
672 let mut violations = Vec::new();
673
674 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 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 }
730 }
731 let _ = mod_id; }
733
734 violations
735}
736
737pub 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 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 let local = import
770 .alias
771 .clone()
772 .unwrap_or_else(|| import.path.last().unwrap().clone());
773 aliases.insert(format!("@mod:{}", local), prefix.trim_end_matches("::").to_string());
775 }
776 }
777
778 aliases
779}
780
781#[cfg(test)]
786mod tests {
787 use super::*;
788 use std::fs;
789
790 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 #[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 #[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); }
868 other => panic!("expected FileNotFound, got: {:?}", other),
869 }
870 }
871
872 #[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 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 { .. } => {} other => panic!("expected CyclicDependency, got: {:?}", other),
920 }
921 }
922
923 #[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 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 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 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 assert!(merged.is_err() || {
974 true
977 });
978 }
979
980 #[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 #[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 #[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 assert!(names.contains(&"math::add"), "expected math::add in {:?}", names);
1041 assert!(names.contains(&"add"), "expected add alias in {:?}", names);
1042 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 assert_eq!(violations.len(), 1);
1059 assert_eq!(violations[0].symbol, "Matrix");
1060 assert_eq!(violations[0].kind, "struct");
1061 }
1062
1063 #[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 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}