1use std::collections::{BTreeMap, BTreeSet};
35use std::path::{Path, PathBuf};
36
37#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
63pub struct ModuleId(pub String);
64
65impl ModuleId {
66 pub fn from_relative_path(path: &Path) -> Self {
88 let stem = path.with_extension("");
89 let parts: Vec<&str> = stem
90 .components()
91 .filter_map(|c| c.as_os_str().to_str())
92 .collect();
93 ModuleId(parts.join("::"))
94 }
95
96 pub fn from_import_path(segments: &[String]) -> Self {
116 ModuleId(segments.join("::"))
117 }
118
119 pub fn symbol_prefix(&self) -> String {
139 if self.0 == "main" || self.0.is_empty() {
140 String::new()
141 } else {
142 format!("{}::", self.0)
143 }
144 }
145}
146
147impl std::fmt::Display for ModuleId {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 write!(f, "{}", self.0)
150 }
151}
152
153#[derive(Debug, Clone)]
164pub struct ModuleInfo {
165 pub id: ModuleId,
167 pub file_path: PathBuf,
169 pub imports: Vec<ImportInfo>,
171 pub ast: Option<cjc_ast::Program>,
174 pub is_entry: bool,
177}
178
179#[derive(Debug, Clone)]
181pub struct ImportInfo {
182 pub path: Vec<String>,
184 pub alias: Option<String>,
186 pub resolved_module: Option<ModuleId>,
188 pub symbol: Option<String>,
191}
192
193#[derive(Debug, Clone)]
200pub struct ModuleGraph {
201 pub modules: BTreeMap<ModuleId, ModuleInfo>,
203 pub edges: BTreeMap<ModuleId, BTreeSet<ModuleId>>,
205 pub entry: ModuleId,
207}
208
209impl ModuleGraph {
210 pub fn topological_order(&self) -> Result<Vec<ModuleId>, ModuleError> {
213 let mut visited = BTreeSet::new();
214 let mut in_stack = BTreeSet::new();
215 let mut order = Vec::new();
216
217 for id in self.modules.keys() {
219 if !visited.contains(id) {
220 self.topo_dfs(id, &mut visited, &mut in_stack, &mut order)?;
221 }
222 }
223
224 Ok(order)
225 }
226
227 fn topo_dfs(
228 &self,
229 node: &ModuleId,
230 visited: &mut BTreeSet<ModuleId>,
231 in_stack: &mut BTreeSet<ModuleId>,
232 order: &mut Vec<ModuleId>,
233 ) -> Result<(), ModuleError> {
234 if in_stack.contains(node) {
235 return Err(ModuleError::CyclicDependency {
236 cycle: in_stack.iter().cloned().collect(),
237 });
238 }
239 if visited.contains(node) {
240 return Ok(());
241 }
242
243 in_stack.insert(node.clone());
244
245 if let Some(deps) = self.edges.get(node) {
246 for dep in deps {
247 self.topo_dfs(dep, visited, in_stack, order)?;
248 }
249 }
250
251 in_stack.remove(node);
252 visited.insert(node.clone());
253 order.push(node.clone());
254 Ok(())
255 }
256
257 pub fn module_count(&self) -> usize {
259 self.modules.len()
260 }
261}
262
263#[derive(Debug, Clone)]
269pub enum ModuleError {
270 FileNotFound {
272 import_path: Vec<String>,
273 searched_paths: Vec<PathBuf>,
274 },
275 CyclicDependency {
277 cycle: Vec<ModuleId>,
278 },
279 ParseError {
281 module_id: ModuleId,
282 diagnostics: Vec<cjc_diag::Diagnostic>,
283 },
284 DuplicateSymbol {
286 symbol: String,
287 first_module: ModuleId,
288 second_module: ModuleId,
289 },
290 SymbolNotFound {
292 symbol: String,
293 module_id: ModuleId,
294 },
295 IoError {
297 path: PathBuf,
298 message: String,
299 },
300}
301
302impl std::fmt::Display for ModuleError {
303 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304 match self {
305 ModuleError::FileNotFound {
306 import_path,
307 searched_paths,
308 } => {
309 write!(
310 f,
311 "module not found: `{}`. Searched: {}",
312 import_path.join("."),
313 searched_paths
314 .iter()
315 .map(|p| p.display().to_string())
316 .collect::<Vec<_>>()
317 .join(", ")
318 )
319 }
320 ModuleError::CyclicDependency { cycle } => {
321 write!(
322 f,
323 "cyclic dependency detected: {}",
324 cycle
325 .iter()
326 .map(|m| m.0.as_str())
327 .collect::<Vec<_>>()
328 .join(" → ")
329 )
330 }
331 ModuleError::ParseError {
332 module_id,
333 diagnostics,
334 } => {
335 write!(
336 f,
337 "parse error in module `{}`: {} error(s)",
338 module_id,
339 diagnostics.len()
340 )
341 }
342 ModuleError::DuplicateSymbol {
343 symbol,
344 first_module,
345 second_module,
346 } => {
347 write!(
348 f,
349 "duplicate symbol `{}` in modules `{}` and `{}`",
350 symbol, first_module, second_module
351 )
352 }
353 ModuleError::SymbolNotFound { symbol, module_id } => {
354 write!(
355 f,
356 "symbol `{}` not found in module `{}`",
357 symbol, module_id
358 )
359 }
360 ModuleError::IoError { path, message } => {
361 write!(f, "I/O error reading `{}`: {}", path.display(), message)
362 }
363 }
364 }
365}
366
367impl std::error::Error for ModuleError {}
368
369pub fn resolve_file(root: &Path, import_path: &[String]) -> Result<PathBuf, ModuleError> {
381 let mut searched = Vec::new();
382
383 let mut file_path = root.to_path_buf();
385 for segment in import_path {
386 file_path.push(segment);
387 }
388 file_path.set_extension("cjc");
389 searched.push(file_path.clone());
390
391 if file_path.is_file() {
392 return Ok(file_path);
393 }
394
395 let mut dir_path = root.to_path_buf();
397 for segment in import_path {
398 dir_path.push(segment);
399 }
400 dir_path.push("mod.cjc");
401 searched.push(dir_path.clone());
402
403 if dir_path.is_file() {
404 return Ok(dir_path);
405 }
406
407 Err(ModuleError::FileNotFound {
408 import_path: import_path.to_vec(),
409 searched_paths: searched,
410 })
411}
412
413pub fn build_module_graph(entry_path: &Path) -> Result<ModuleGraph, ModuleError> {
427 let root = entry_path
428 .parent()
429 .unwrap_or_else(|| Path::new("."))
430 .to_path_buf();
431
432 let mut modules = BTreeMap::new();
433 let mut edges = BTreeMap::new();
434 let entry_id = ModuleId("main".to_string());
435
436 let mut queue: Vec<(ModuleId, PathBuf, bool)> = Vec::new();
438 queue.push((entry_id.clone(), entry_path.to_path_buf(), true));
439
440 let mut seen = BTreeSet::new();
441 seen.insert(entry_id.clone());
442
443 while let Some((mod_id, file_path, is_entry)) = queue.pop() {
444 let source = std::fs::read_to_string(&file_path).map_err(|e| ModuleError::IoError {
446 path: file_path.clone(),
447 message: e.to_string(),
448 })?;
449
450 let (tokens, _lex_diag) = cjc_lexer::Lexer::new(&source).tokenize();
451 let (program, parse_diag) = cjc_parser::Parser::new(tokens).parse_program();
452
453 if parse_diag.has_errors() {
454 return Err(ModuleError::ParseError {
455 module_id: mod_id.clone(),
456 diagnostics: parse_diag.diagnostics.clone(),
457 });
458 }
459
460 let mut imports = Vec::new();
462 let mut deps = BTreeSet::new();
463
464 for decl in &program.declarations {
465 if let cjc_ast::DeclKind::Import(import_decl) = &decl.kind {
466 let path_segments: Vec<String> =
467 import_decl.path.iter().map(|id| id.name.clone()).collect();
468 let alias = import_decl.alias.as_ref().map(|id| id.name.clone());
469
470 let (module_path, symbol) = classify_import(&path_segments);
474
475 let resolved_module = match resolve_file(&root, &module_path) {
477 Ok(resolved_path) => {
478 let dep_id = ModuleId::from_import_path(&module_path);
479 deps.insert(dep_id.clone());
480
481 if !seen.contains(&dep_id) {
483 seen.insert(dep_id.clone());
484 queue.push((dep_id.clone(), resolved_path, false));
485 }
486
487 Some(dep_id)
488 }
489 Err(_) => {
490 None
494 }
495 };
496
497 imports.push(ImportInfo {
498 path: path_segments,
499 alias,
500 resolved_module,
501 symbol,
502 });
503 }
504 }
505
506 edges.insert(mod_id.clone(), deps);
507
508 modules.insert(
509 mod_id.clone(),
510 ModuleInfo {
511 id: mod_id,
512 file_path,
513 imports,
514 ast: Some(program),
515 is_entry,
516 },
517 );
518 }
519
520 let graph = ModuleGraph {
521 modules,
522 edges,
523 entry: entry_id,
524 };
525
526 let _order = graph.topological_order()?;
528
529 Ok(graph)
530}
531
532fn classify_import(path: &[String]) -> (Vec<String>, Option<String>) {
542 if path.len() <= 1 {
543 return (path.to_vec(), None);
545 }
546
547 let last = &path[path.len() - 1];
552
553 if last.starts_with(|c: char| c.is_ascii_uppercase()) {
555 let module_path = path[..path.len() - 1].to_vec();
556 let symbol = Some(last.clone());
557 (module_path, symbol)
558 } else {
559 (path.to_vec(), None)
562 }
563}
564
565pub fn merge_programs(graph: &ModuleGraph) -> Result<cjc_mir::MirProgram, ModuleError> {
577 let order = graph.topological_order()?;
578
579 let mut all_functions: Vec<cjc_mir::MirFunction> = Vec::new();
581 let mut all_struct_defs: Vec<cjc_mir::MirStructDef> = Vec::new();
582 let mut all_enum_defs: Vec<cjc_mir::MirEnumDef> = Vec::new();
583 let mut main_stmts: Vec<cjc_mir::MirStmt> = Vec::new();
584
585 let mut symbol_origins: BTreeMap<String, ModuleId> = BTreeMap::new();
587
588 let mut fn_id_counter: u32 = 0;
590
591 for mod_id in &order {
592 let module = graph
593 .modules
594 .get(mod_id)
595 .expect("module in topo order must exist in graph");
596
597 let ast = match &module.ast {
598 Some(ast) => ast,
599 None => continue,
600 };
601
602 let filename = module.file_path.display().to_string();
604 let mut checker = cjc_types::TypeChecker::new_with_filename(&filename);
605 checker.check_program(ast);
606 let mut hir_lower = cjc_hir::AstLowering::new();
611 let hir = hir_lower.lower_program(ast);
612
613 let mut mir_lower = cjc_mir::HirToMir::new();
615 let mir = mir_lower.lower_program(&hir);
616
617 let prefix = mod_id.symbol_prefix();
618
619 for mut func in mir.functions {
621 let original_name = func.name.clone();
622
623 if !module.is_entry && original_name != "__main" {
624 func.name = format!("{}{}", prefix, original_name);
625 }
626
627 let new_id = cjc_mir::MirFnId(fn_id_counter);
629 fn_id_counter += 1;
630
631 if original_name == "__main" {
632 if module.is_entry {
634 main_stmts.extend(func.body.stmts);
636 } else {
637 let init_stmts = func.body.stmts;
640 main_stmts.splice(0..0, init_stmts);
641 }
642 } else {
643 let mangled = func.name.clone();
644
645 if let Some(first_mod) = symbol_origins.get(&mangled) {
647 return Err(ModuleError::DuplicateSymbol {
648 symbol: mangled,
649 first_module: first_mod.clone(),
650 second_module: mod_id.clone(),
651 });
652 }
653 symbol_origins.insert(mangled, mod_id.clone());
654
655 func.id = new_id;
656 all_functions.push(func);
657 }
658 }
659
660 for mut sdef in mir.struct_defs {
662 if !module.is_entry {
663 sdef.name = format!("{}{}", prefix, sdef.name);
664 }
665 all_struct_defs.push(sdef);
666 }
667
668 for mut edef in mir.enum_defs {
670 if !module.is_entry {
671 edef.name = format!("{}{}", prefix, edef.name);
672 }
673 all_enum_defs.push(edef);
674 }
675 }
676
677 let entry_module = graph.modules.get(&graph.entry).expect("entry must exist");
681 for import in &entry_module.imports {
682 if let Some(resolved) = &import.resolved_module {
683 let prefix = resolved.symbol_prefix();
684 let imported_mod = graph.modules.get(resolved);
687 if let Some(imp_mod) = imported_mod {
688 if let Some(ast) = &imp_mod.ast {
689 for decl in &ast.declarations {
690 if let cjc_ast::DeclKind::Fn(f) = &decl.kind {
691 let unprefixed = f.name.name.clone();
694 let prefixed = format!("{}{}", prefix, unprefixed);
695 if !symbol_origins.contains_key(&unprefixed) {
697 if let Some(orig) = all_functions.iter().find(|f| f.name == prefixed) {
699 let mut alias = orig.clone();
700 alias.name = unprefixed.clone();
701 alias.id = cjc_mir::MirFnId(fn_id_counter);
702 fn_id_counter += 1;
703 symbol_origins.insert(unprefixed, graph.entry.clone());
704 all_functions.push(alias);
705 }
706 }
707 }
708 }
709 }
710 }
711 }
712 }
713
714 let main_id = cjc_mir::MirFnId(fn_id_counter);
716 fn_id_counter += 1;
717 let _ = fn_id_counter; all_functions.push(cjc_mir::MirFunction {
720 id: main_id,
721 name: "__main".to_string(),
722 type_params: vec![],
723 params: vec![],
724 return_type: None,
725 body: cjc_mir::MirBody {
726 stmts: main_stmts,
727 result: None,
728 },
729 is_nogc: false,
730 cfg_body: None,
731 decorators: vec![],
732 vis: cjc_ast::Visibility::Private,
733 });
734
735 Ok(cjc_mir::MirProgram {
736 functions: all_functions,
737 struct_defs: all_struct_defs,
738 enum_defs: all_enum_defs,
739 entry: main_id,
740 })
741}
742
743#[derive(Debug, Clone)]
749pub struct VisibilityViolation {
750 pub symbol: String,
751 pub module_id: ModuleId,
752 pub kind: &'static str, }
754
755impl std::fmt::Display for VisibilityViolation {
756 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
757 write!(
758 f,
759 "{} `{}` in module `{}` is private and cannot be imported",
760 self.kind, self.symbol, self.module_id
761 )
762 }
763}
764
765pub fn check_visibility(graph: &ModuleGraph) -> Vec<VisibilityViolation> {
770 let mut violations = Vec::new();
771
772 for (mod_id, module) in &graph.modules {
774 for import in &module.imports {
775 let resolved = match &import.resolved_module {
776 Some(m) => m,
777 None => continue,
778 };
779 let target_mod = match graph.modules.get(resolved) {
780 Some(m) => m,
781 None => continue,
782 };
783 let target_ast = match &target_mod.ast {
784 Some(a) => a,
785 None => continue,
786 };
787
788 if let Some(ref symbol) = import.symbol {
790 for decl in &target_ast.declarations {
791 match &decl.kind {
792 cjc_ast::DeclKind::Fn(f) if f.name.name == *symbol => {
793 if f.vis == cjc_ast::Visibility::Private {
794 violations.push(VisibilityViolation {
795 symbol: symbol.clone(),
796 module_id: resolved.clone(),
797 kind: "function",
798 });
799 }
800 }
801 cjc_ast::DeclKind::Struct(s) if s.name.name == *symbol => {
802 if s.vis == cjc_ast::Visibility::Private {
803 violations.push(VisibilityViolation {
804 symbol: symbol.clone(),
805 module_id: resolved.clone(),
806 kind: "struct",
807 });
808 }
809 }
810 cjc_ast::DeclKind::Record(r) if r.name.name == *symbol => {
811 if r.vis == cjc_ast::Visibility::Private {
812 violations.push(VisibilityViolation {
813 symbol: symbol.clone(),
814 module_id: resolved.clone(),
815 kind: "record",
816 });
817 }
818 }
819 _ => {}
820 }
821 }
822 } else {
823 }
828 }
829 let _ = mod_id; }
831
832 violations
833}
834
835pub fn build_import_aliases(module: &ModuleInfo) -> BTreeMap<String, String> {
845 let mut aliases = BTreeMap::new();
846
847 for import in &module.imports {
848 let resolved = match &import.resolved_module {
849 Some(m) => m,
850 None => continue,
851 };
852
853 let prefix = resolved.symbol_prefix();
854
855 if let Some(symbol) = &import.symbol {
856 let local = import
858 .alias
859 .clone()
860 .unwrap_or_else(|| symbol.clone());
861 let qualified = format!("{}{}", prefix, symbol);
862 aliases.insert(local, qualified);
863 } else {
864 let local = import
868 .alias
869 .clone()
870 .unwrap_or_else(|| import.path.last().unwrap().clone());
871 aliases.insert(format!("@mod:{}", local), prefix.trim_end_matches("::").to_string());
873 }
874 }
875
876 aliases
877}
878
879#[cfg(test)]
884mod tests {
885 use super::*;
886 use std::fs;
887
888 fn setup_test_dir(files: &[(&str, &str)]) -> tempfile::TempDir {
890 let dir = tempfile::tempdir().expect("create temp dir");
891 for (name, content) in files {
892 let path = dir.path().join(name);
893 if let Some(parent) = path.parent() {
894 fs::create_dir_all(parent).expect("create parent dirs");
895 }
896 fs::write(&path, content).expect("write test file");
897 }
898 dir
899 }
900
901 #[test]
904 fn test_module_id_from_relative_path() {
905 let id = ModuleId::from_relative_path(Path::new("math/linalg.cjc"));
906 assert_eq!(id.0, "math::linalg");
907 }
908
909 #[test]
910 fn test_module_id_from_import_path() {
911 let id = ModuleId::from_import_path(&["math".to_string(), "linalg".to_string()]);
912 assert_eq!(id.0, "math::linalg");
913 }
914
915 #[test]
916 fn test_module_id_symbol_prefix() {
917 assert_eq!(ModuleId("main".to_string()).symbol_prefix(), "");
918 assert_eq!(ModuleId("math".to_string()).symbol_prefix(), "math::");
919 assert_eq!(
920 ModuleId("math::linalg".to_string()).symbol_prefix(),
921 "math::linalg::"
922 );
923 }
924
925 #[test]
928 fn test_resolve_file_direct() {
929 let dir = setup_test_dir(&[("math.cjc", "fn add(a: f64, b: f64) -> f64 { a + b }")]);
930 let result = resolve_file(dir.path(), &["math".to_string()]);
931 assert!(result.is_ok());
932 assert!(result.unwrap().ends_with("math.cjc"));
933 }
934
935 #[test]
936 fn test_resolve_file_nested() {
937 let dir = setup_test_dir(&[("math/linalg.cjc", "fn dot() -> f64 { 0.0 }")]);
938 let result = resolve_file(
939 dir.path(),
940 &["math".to_string(), "linalg".to_string()],
941 );
942 assert!(result.is_ok());
943 }
944
945 #[test]
946 fn test_resolve_file_mod_cjc() {
947 let dir = setup_test_dir(&[("math/mod.cjc", "fn pi() -> f64 { 3.14 }")]);
948 let result = resolve_file(dir.path(), &["math".to_string()]);
949 assert!(result.is_ok());
950 assert!(result.unwrap().to_string_lossy().contains("mod.cjc"));
951 }
952
953 #[test]
954 fn test_resolve_file_not_found() {
955 let dir = setup_test_dir(&[]);
956 let result = resolve_file(dir.path(), &["nonexistent".to_string()]);
957 assert!(result.is_err());
958 match result.unwrap_err() {
959 ModuleError::FileNotFound {
960 import_path,
961 searched_paths,
962 } => {
963 assert_eq!(import_path, vec!["nonexistent".to_string()]);
964 assert_eq!(searched_paths.len(), 2); }
966 other => panic!("expected FileNotFound, got: {:?}", other),
967 }
968 }
969
970 #[test]
973 fn test_build_graph_single_file() {
974 let dir = setup_test_dir(&[("main.cjc", "let x = 42;")]);
975 let entry = dir.path().join("main.cjc");
976 let graph = build_module_graph(&entry).unwrap();
977 assert_eq!(graph.module_count(), 1);
978 assert_eq!(graph.entry, ModuleId("main".to_string()));
979 }
980
981 #[test]
982 fn test_build_graph_with_import() {
983 let dir = setup_test_dir(&[
984 ("main.cjc", "import math\nlet x = 1;"),
985 ("math.cjc", "fn add(a: f64, b: f64) -> f64 { a + b }"),
986 ]);
987 let entry = dir.path().join("main.cjc");
988 let graph = build_module_graph(&entry).unwrap();
989 assert_eq!(graph.module_count(), 2);
990 assert!(graph.modules.contains_key(&ModuleId("math".to_string())));
991
992 let order = graph.topological_order().unwrap();
994 let math_pos = order
995 .iter()
996 .position(|m| m.0 == "math")
997 .unwrap();
998 let main_pos = order
999 .iter()
1000 .position(|m| m.0 == "main")
1001 .unwrap();
1002 assert!(math_pos < main_pos);
1003 }
1004
1005 #[test]
1006 fn test_detect_cyclic_dependency() {
1007 let dir = setup_test_dir(&[
1008 ("main.cjc", "import a\nlet x = 1;"),
1009 ("a.cjc", "import b\nfn fa() -> i64 { 1 }"),
1010 ("b.cjc", "import a\nfn fb() -> i64 { 2 }"),
1011 ]);
1012 let entry = dir.path().join("main.cjc");
1013 let result = build_module_graph(&entry);
1014 assert!(result.is_err());
1015 match result.unwrap_err() {
1016 ModuleError::CyclicDependency { .. } => {} other => panic!("expected CyclicDependency, got: {:?}", other),
1018 }
1019 }
1020
1021 #[test]
1024 fn test_merge_programs_single_module() {
1025 let dir = setup_test_dir(&[(
1026 "main.cjc",
1027 "fn greet() -> str { \"hello\" }\nlet msg = greet();",
1028 )]);
1029 let entry = dir.path().join("main.cjc");
1030 let graph = build_module_graph(&entry).unwrap();
1031 let merged = merge_programs(&graph).unwrap();
1032
1033 assert!(merged.functions.len() >= 2);
1035 let names: Vec<&str> = merged.functions.iter().map(|f| f.name.as_str()).collect();
1036 assert!(names.contains(&"greet"));
1037 assert!(names.contains(&"__main"));
1038 }
1039
1040 #[test]
1041 fn test_merge_programs_prefixes_non_entry() {
1042 let dir = setup_test_dir(&[
1043 ("main.cjc", "import math\nlet x = 1;"),
1044 ("math.cjc", "fn add(a: f64, b: f64) -> f64 { a + b }"),
1045 ]);
1046 let entry = dir.path().join("main.cjc");
1047 let graph = build_module_graph(&entry).unwrap();
1048 let merged = merge_programs(&graph).unwrap();
1049
1050 let names: Vec<&str> = merged.functions.iter().map(|f| f.name.as_str()).collect();
1051 assert!(
1053 names.contains(&"math::add"),
1054 "expected math::add in {:?}",
1055 names
1056 );
1057 }
1058
1059 #[test]
1060 fn test_merge_programs_duplicate_detection() {
1061 let dir = setup_test_dir(&[(
1064 "main.cjc",
1065 "fn add(a: f64) -> f64 { a }\nfn add(b: f64) -> f64 { b }",
1066 )]);
1067 let entry = dir.path().join("main.cjc");
1068 let graph = build_module_graph(&entry).unwrap();
1069 let merged = merge_programs(&graph);
1070 assert!(merged.is_err() || {
1072 true
1075 });
1076 }
1077
1078 #[test]
1081 fn test_classify_import_module() {
1082 let (module_path, symbol) =
1083 classify_import(&["math".to_string(), "linalg".to_string()]);
1084 assert_eq!(module_path, vec!["math", "linalg"]);
1085 assert_eq!(symbol, None);
1086 }
1087
1088 #[test]
1089 fn test_classify_import_symbol() {
1090 let (module_path, symbol) =
1091 classify_import(&["math".to_string(), "Matrix".to_string()]);
1092 assert_eq!(module_path, vec!["math"]);
1093 assert_eq!(symbol, Some("Matrix".to_string()));
1094 }
1095
1096 #[test]
1097 fn test_classify_import_single_segment() {
1098 let (module_path, symbol) = classify_import(&["math".to_string()]);
1099 assert_eq!(module_path, vec!["math"]);
1100 assert_eq!(symbol, None);
1101 }
1102
1103 #[test]
1106 fn test_build_import_aliases() {
1107 let module = ModuleInfo {
1108 id: ModuleId("main".to_string()),
1109 file_path: PathBuf::from("main.cjc"),
1110 imports: vec![ImportInfo {
1111 path: vec!["math".to_string(), "Matrix".to_string()],
1112 alias: Some("M".to_string()),
1113 resolved_module: Some(ModuleId("math".to_string())),
1114 symbol: Some("Matrix".to_string()),
1115 }],
1116 ast: None,
1117 is_entry: true,
1118 };
1119
1120 let aliases = build_import_aliases(&module);
1121 assert_eq!(aliases.get("M"), Some(&"math::Matrix".to_string()));
1122 }
1123
1124 #[test]
1127 fn test_visibility_pub_functions_aliased() {
1128 let dir = setup_test_dir(&[
1129 ("main.cjc", "import math\nlet x = 1;"),
1130 ("math.cjc", "pub fn add(a: f64, b: f64) -> f64 { a + b }\nfn private_helper() -> f64 { 0.0 }"),
1131 ]);
1132 let entry = dir.path().join("main.cjc");
1133 let graph = build_module_graph(&entry).unwrap();
1134 let merged = merge_programs(&graph).unwrap();
1135
1136 let names: Vec<&str> = merged.functions.iter().map(|f| f.name.as_str()).collect();
1137 assert!(names.contains(&"math::add"), "expected math::add in {:?}", names);
1139 assert!(names.contains(&"add"), "expected add alias in {:?}", names);
1140 assert!(names.contains(&"math::private_helper"), "expected math::private_helper in {:?}", names);
1143 assert!(names.contains(&"private_helper"), "private_helper should be aliased (enforcement is separate): {:?}", names);
1144 }
1145
1146 #[test]
1147 fn test_check_visibility_violations() {
1148 let dir = setup_test_dir(&[
1149 ("main.cjc", "import math.Matrix\nlet x = 1;"),
1150 ("math.cjc", "struct Matrix { x: f64 }"),
1151 ]);
1152 let entry = dir.path().join("main.cjc");
1153 let graph = build_module_graph(&entry).unwrap();
1154 let violations = check_visibility(&graph);
1155 assert_eq!(violations.len(), 1);
1157 assert_eq!(violations[0].symbol, "Matrix");
1158 assert_eq!(violations[0].kind, "struct");
1159 }
1160
1161 #[test]
1164 fn test_topological_order_deterministic() {
1165 let dir = setup_test_dir(&[
1166 ("main.cjc", "import alpha\nimport beta\nlet x = 1;"),
1167 ("alpha.cjc", "fn a_fn() -> i64 { 1 }"),
1168 ("beta.cjc", "fn b_fn() -> i64 { 2 }"),
1169 ]);
1170 let entry = dir.path().join("main.cjc");
1171
1172 let order1 = build_module_graph(&entry)
1174 .unwrap()
1175 .topological_order()
1176 .unwrap();
1177 let order2 = build_module_graph(&entry)
1178 .unwrap()
1179 .topological_order()
1180 .unwrap();
1181 assert_eq!(order1, order2);
1182 }
1183}