use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ModuleId(pub String);
impl ModuleId {
pub fn from_relative_path(path: &Path) -> Self {
let stem = path.with_extension("");
let parts: Vec<&str> = stem
.components()
.filter_map(|c| c.as_os_str().to_str())
.collect();
ModuleId(parts.join("::"))
}
pub fn from_import_path(segments: &[String]) -> Self {
ModuleId(segments.join("::"))
}
pub fn symbol_prefix(&self) -> String {
if self.0 == "main" || self.0.is_empty() {
String::new()
} else {
format!("{}::", self.0)
}
}
}
impl std::fmt::Display for ModuleId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct ModuleInfo {
pub id: ModuleId,
pub file_path: PathBuf,
pub imports: Vec<ImportInfo>,
pub ast: Option<cjc_ast::Program>,
pub is_entry: bool,
}
#[derive(Debug, Clone)]
pub struct ImportInfo {
pub path: Vec<String>,
pub alias: Option<String>,
pub resolved_module: Option<ModuleId>,
pub symbol: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ModuleGraph {
pub modules: BTreeMap<ModuleId, ModuleInfo>,
pub edges: BTreeMap<ModuleId, BTreeSet<ModuleId>>,
pub entry: ModuleId,
}
impl ModuleGraph {
pub fn topological_order(&self) -> Result<Vec<ModuleId>, ModuleError> {
let mut visited = BTreeSet::new();
let mut in_stack = BTreeSet::new();
let mut order = Vec::new();
for id in self.modules.keys() {
if !visited.contains(id) {
self.topo_dfs(id, &mut visited, &mut in_stack, &mut order)?;
}
}
Ok(order)
}
fn topo_dfs(
&self,
node: &ModuleId,
visited: &mut BTreeSet<ModuleId>,
in_stack: &mut BTreeSet<ModuleId>,
order: &mut Vec<ModuleId>,
) -> Result<(), ModuleError> {
if in_stack.contains(node) {
return Err(ModuleError::CyclicDependency {
cycle: in_stack.iter().cloned().collect(),
});
}
if visited.contains(node) {
return Ok(());
}
in_stack.insert(node.clone());
if let Some(deps) = self.edges.get(node) {
for dep in deps {
self.topo_dfs(dep, visited, in_stack, order)?;
}
}
in_stack.remove(node);
visited.insert(node.clone());
order.push(node.clone());
Ok(())
}
pub fn module_count(&self) -> usize {
self.modules.len()
}
}
#[derive(Debug, Clone)]
pub enum ModuleError {
FileNotFound {
import_path: Vec<String>,
searched_paths: Vec<PathBuf>,
},
CyclicDependency {
cycle: Vec<ModuleId>,
},
ParseError {
module_id: ModuleId,
diagnostics: Vec<cjc_diag::Diagnostic>,
},
DuplicateSymbol {
symbol: String,
first_module: ModuleId,
second_module: ModuleId,
},
SymbolNotFound {
symbol: String,
module_id: ModuleId,
},
IoError {
path: PathBuf,
message: String,
},
}
impl std::fmt::Display for ModuleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModuleError::FileNotFound {
import_path,
searched_paths,
} => {
write!(
f,
"module not found: `{}`. Searched: {}",
import_path.join("."),
searched_paths
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
ModuleError::CyclicDependency { cycle } => {
write!(
f,
"cyclic dependency detected: {}",
cycle
.iter()
.map(|m| m.0.as_str())
.collect::<Vec<_>>()
.join(" → ")
)
}
ModuleError::ParseError {
module_id,
diagnostics,
} => {
write!(
f,
"parse error in module `{}`: {} error(s)",
module_id,
diagnostics.len()
)
}
ModuleError::DuplicateSymbol {
symbol,
first_module,
second_module,
} => {
write!(
f,
"duplicate symbol `{}` in modules `{}` and `{}`",
symbol, first_module, second_module
)
}
ModuleError::SymbolNotFound { symbol, module_id } => {
write!(
f,
"symbol `{}` not found in module `{}`",
symbol, module_id
)
}
ModuleError::IoError { path, message } => {
write!(f, "I/O error reading `{}`: {}", path.display(), message)
}
}
}
}
impl std::error::Error for ModuleError {}
pub fn resolve_file(root: &Path, import_path: &[String]) -> Result<PathBuf, ModuleError> {
let mut searched = Vec::new();
let mut file_path = root.to_path_buf();
for segment in import_path {
file_path.push(segment);
}
file_path.set_extension("cjcl");
searched.push(file_path.clone());
if file_path.is_file() {
return Ok(file_path);
}
let mut dir_path = root.to_path_buf();
for segment in import_path {
dir_path.push(segment);
}
dir_path.push("mod.cjcl");
searched.push(dir_path.clone());
if dir_path.is_file() {
return Ok(dir_path);
}
Err(ModuleError::FileNotFound {
import_path: import_path.to_vec(),
searched_paths: searched,
})
}
pub fn build_module_graph(entry_path: &Path) -> Result<ModuleGraph, ModuleError> {
let root = entry_path
.parent()
.unwrap_or_else(|| Path::new("."))
.to_path_buf();
let mut modules = BTreeMap::new();
let mut edges = BTreeMap::new();
let entry_id = ModuleId("main".to_string());
let mut queue: Vec<(ModuleId, PathBuf, bool)> = Vec::new();
queue.push((entry_id.clone(), entry_path.to_path_buf(), true));
let mut seen = BTreeSet::new();
seen.insert(entry_id.clone());
while let Some((mod_id, file_path, is_entry)) = queue.pop() {
let source = std::fs::read_to_string(&file_path).map_err(|e| ModuleError::IoError {
path: file_path.clone(),
message: e.to_string(),
})?;
let (tokens, _lex_diag) = cjc_lexer::Lexer::new(&source).tokenize();
let (program, parse_diag) = cjc_parser::Parser::new(tokens).parse_program();
if parse_diag.has_errors() {
return Err(ModuleError::ParseError {
module_id: mod_id.clone(),
diagnostics: parse_diag.diagnostics.clone(),
});
}
let mut imports = Vec::new();
let mut deps = BTreeSet::new();
for decl in &program.declarations {
if let cjc_ast::DeclKind::Import(import_decl) = &decl.kind {
let path_segments: Vec<String> =
import_decl.path.iter().map(|id| id.name.clone()).collect();
let alias = import_decl.alias.as_ref().map(|id| id.name.clone());
let (module_path, symbol) = classify_import(&path_segments);
let resolved_module = match resolve_file(&root, &module_path) {
Ok(resolved_path) => {
let dep_id = ModuleId::from_import_path(&module_path);
deps.insert(dep_id.clone());
if !seen.contains(&dep_id) {
seen.insert(dep_id.clone());
queue.push((dep_id.clone(), resolved_path, false));
}
Some(dep_id)
}
Err(_) => {
None
}
};
imports.push(ImportInfo {
path: path_segments,
alias,
resolved_module,
symbol,
});
}
}
edges.insert(mod_id.clone(), deps);
modules.insert(
mod_id.clone(),
ModuleInfo {
id: mod_id,
file_path,
imports,
ast: Some(program),
is_entry,
},
);
}
let graph = ModuleGraph {
modules,
edges,
entry: entry_id,
};
let _order = graph.topological_order()?;
Ok(graph)
}
fn classify_import(path: &[String]) -> (Vec<String>, Option<String>) {
if path.len() <= 1 {
return (path.to_vec(), None);
}
let last = &path[path.len() - 1];
if last.starts_with(|c: char| c.is_ascii_uppercase()) {
let module_path = path[..path.len() - 1].to_vec();
let symbol = Some(last.clone());
(module_path, symbol)
} else {
(path.to_vec(), None)
}
}
pub fn merge_programs(graph: &ModuleGraph) -> Result<cjc_mir::MirProgram, ModuleError> {
let order = graph.topological_order()?;
let mut all_functions: Vec<cjc_mir::MirFunction> = Vec::new();
let mut all_struct_defs: Vec<cjc_mir::MirStructDef> = Vec::new();
let mut all_enum_defs: Vec<cjc_mir::MirEnumDef> = Vec::new();
let mut main_stmts: Vec<cjc_mir::MirStmt> = Vec::new();
let mut symbol_origins: BTreeMap<String, ModuleId> = BTreeMap::new();
let mut fn_id_counter: u32 = 0;
for mod_id in &order {
let module = graph
.modules
.get(mod_id)
.expect("module in topo order must exist in graph");
let ast = match &module.ast {
Some(ast) => ast,
None => continue,
};
let filename = module.file_path.display().to_string();
let mut checker = cjc_types::TypeChecker::new_with_filename(&filename);
checker.check_program(ast);
let mut hir_lower = cjc_hir::AstLowering::new();
let hir = hir_lower.lower_program(ast);
let mut mir_lower = cjc_mir::HirToMir::new();
let mir = mir_lower.lower_program(&hir);
let prefix = mod_id.symbol_prefix();
for mut func in mir.functions {
let original_name = func.name.clone();
if !module.is_entry && original_name != "__main" {
func.name = format!("{}{}", prefix, original_name);
}
let new_id = cjc_mir::MirFnId(fn_id_counter);
fn_id_counter += 1;
if original_name == "__main" {
if module.is_entry {
main_stmts.extend(func.body.stmts);
} else {
let init_stmts = func.body.stmts;
main_stmts.splice(0..0, init_stmts);
}
} else {
let mangled = func.name.clone();
if let Some(first_mod) = symbol_origins.get(&mangled) {
return Err(ModuleError::DuplicateSymbol {
symbol: mangled,
first_module: first_mod.clone(),
second_module: mod_id.clone(),
});
}
symbol_origins.insert(mangled, mod_id.clone());
func.id = new_id;
all_functions.push(func);
}
}
for mut sdef in mir.struct_defs {
if !module.is_entry {
sdef.name = format!("{}{}", prefix, sdef.name);
}
all_struct_defs.push(sdef);
}
for mut edef in mir.enum_defs {
if !module.is_entry {
edef.name = format!("{}{}", prefix, edef.name);
}
all_enum_defs.push(edef);
}
}
let entry_module = graph.modules.get(&graph.entry).expect("entry must exist");
for import in &entry_module.imports {
if let Some(resolved) = &import.resolved_module {
let prefix = resolved.symbol_prefix();
let imported_mod = graph.modules.get(resolved);
if let Some(imp_mod) = imported_mod {
if let Some(ast) = &imp_mod.ast {
for decl in &ast.declarations {
if let cjc_ast::DeclKind::Fn(f) = &decl.kind {
let unprefixed = f.name.name.clone();
let prefixed = format!("{}{}", prefix, unprefixed);
if !symbol_origins.contains_key(&unprefixed) {
if let Some(orig) = all_functions.iter().find(|f| f.name == prefixed) {
let mut alias = orig.clone();
alias.name = unprefixed.clone();
alias.id = cjc_mir::MirFnId(fn_id_counter);
fn_id_counter += 1;
symbol_origins.insert(unprefixed, graph.entry.clone());
all_functions.push(alias);
}
}
}
}
}
}
}
}
let main_id = cjc_mir::MirFnId(fn_id_counter);
fn_id_counter += 1;
let _ = fn_id_counter;
all_functions.push(cjc_mir::MirFunction {
id: main_id,
name: "__main".to_string(),
type_params: vec![],
params: vec![],
return_type: None,
body: cjc_mir::MirBody {
stmts: main_stmts,
result: None,
},
is_nogc: false,
cfg_body: None,
decorators: vec![],
vis: cjc_ast::Visibility::Private,
local_count: 0,
});
Ok(cjc_mir::MirProgram {
functions: all_functions,
struct_defs: all_struct_defs,
enum_defs: all_enum_defs,
entry: main_id,
})
}
#[derive(Debug, Clone)]
pub struct VisibilityViolation {
pub symbol: String,
pub module_id: ModuleId,
pub kind: &'static str, }
impl std::fmt::Display for VisibilityViolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} `{}` in module `{}` is private and cannot be imported",
self.kind, self.symbol, self.module_id
)
}
}
pub fn check_visibility(graph: &ModuleGraph) -> Vec<VisibilityViolation> {
let mut violations = Vec::new();
for (mod_id, module) in &graph.modules {
for import in &module.imports {
let resolved = match &import.resolved_module {
Some(m) => m,
None => continue,
};
let target_mod = match graph.modules.get(resolved) {
Some(m) => m,
None => continue,
};
let target_ast = match &target_mod.ast {
Some(a) => a,
None => continue,
};
if let Some(ref symbol) = import.symbol {
for decl in &target_ast.declarations {
match &decl.kind {
cjc_ast::DeclKind::Fn(f) if f.name.name == *symbol => {
if f.vis == cjc_ast::Visibility::Private {
violations.push(VisibilityViolation {
symbol: symbol.clone(),
module_id: resolved.clone(),
kind: "function",
});
}
}
cjc_ast::DeclKind::Struct(s) if s.name.name == *symbol => {
if s.vis == cjc_ast::Visibility::Private {
violations.push(VisibilityViolation {
symbol: symbol.clone(),
module_id: resolved.clone(),
kind: "struct",
});
}
}
cjc_ast::DeclKind::Record(r) if r.name.name == *symbol => {
if r.vis == cjc_ast::Visibility::Private {
violations.push(VisibilityViolation {
symbol: symbol.clone(),
module_id: resolved.clone(),
kind: "record",
});
}
}
_ => {}
}
}
} else {
}
}
let _ = mod_id; }
violations
}
pub fn build_import_aliases(module: &ModuleInfo) -> BTreeMap<String, String> {
let mut aliases = BTreeMap::new();
for import in &module.imports {
let resolved = match &import.resolved_module {
Some(m) => m,
None => continue,
};
let prefix = resolved.symbol_prefix();
if let Some(symbol) = &import.symbol {
let local = import
.alias
.clone()
.unwrap_or_else(|| symbol.clone());
let qualified = format!("{}{}", prefix, symbol);
aliases.insert(local, qualified);
} else {
let local = import
.alias
.clone()
.unwrap_or_else(|| import.path.last().unwrap().clone());
aliases.insert(format!("@mod:{}", local), prefix.trim_end_matches("::").to_string());
}
}
aliases
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn setup_test_dir(files: &[(&str, &str)]) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("create temp dir");
for (name, content) in files {
let path = dir.path().join(name);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create parent dirs");
}
fs::write(&path, content).expect("write test file");
}
dir
}
#[test]
fn test_module_id_from_relative_path() {
let id = ModuleId::from_relative_path(Path::new("math/linalg.cjcl"));
assert_eq!(id.0, "math::linalg");
}
#[test]
fn test_module_id_from_import_path() {
let id = ModuleId::from_import_path(&["math".to_string(), "linalg".to_string()]);
assert_eq!(id.0, "math::linalg");
}
#[test]
fn test_module_id_symbol_prefix() {
assert_eq!(ModuleId("main".to_string()).symbol_prefix(), "");
assert_eq!(ModuleId("math".to_string()).symbol_prefix(), "math::");
assert_eq!(
ModuleId("math::linalg".to_string()).symbol_prefix(),
"math::linalg::"
);
}
#[test]
fn test_resolve_file_direct() {
let dir = setup_test_dir(&[("math.cjcl", "fn add(a: f64, b: f64) -> f64 { a + b }")]);
let result = resolve_file(dir.path(), &["math".to_string()]);
assert!(result.is_ok());
assert!(result.unwrap().ends_with("math.cjcl"));
}
#[test]
fn test_resolve_file_nested() {
let dir = setup_test_dir(&[("math/linalg.cjcl", "fn dot() -> f64 { 0.0 }")]);
let result = resolve_file(
dir.path(),
&["math".to_string(), "linalg".to_string()],
);
assert!(result.is_ok());
}
#[test]
fn test_resolve_file_mod_cjc() {
let dir = setup_test_dir(&[("math/mod.cjcl", "fn pi() -> f64 { 3.14 }")]);
let result = resolve_file(dir.path(), &["math".to_string()]);
assert!(result.is_ok());
assert!(result.unwrap().to_string_lossy().contains("mod.cjcl"));
}
#[test]
fn test_resolve_file_not_found() {
let dir = setup_test_dir(&[]);
let result = resolve_file(dir.path(), &["nonexistent".to_string()]);
assert!(result.is_err());
match result.unwrap_err() {
ModuleError::FileNotFound {
import_path,
searched_paths,
} => {
assert_eq!(import_path, vec!["nonexistent".to_string()]);
assert_eq!(searched_paths.len(), 2); }
other => panic!("expected FileNotFound, got: {:?}", other),
}
}
#[test]
fn test_build_graph_single_file() {
let dir = setup_test_dir(&[("main.cjcl", "let x = 42;")]);
let entry = dir.path().join("main.cjcl");
let graph = build_module_graph(&entry).unwrap();
assert_eq!(graph.module_count(), 1);
assert_eq!(graph.entry, ModuleId("main".to_string()));
}
#[test]
fn test_build_graph_with_import() {
let dir = setup_test_dir(&[
("main.cjcl", "import math\nlet x = 1;"),
("math.cjcl", "fn add(a: f64, b: f64) -> f64 { a + b }"),
]);
let entry = dir.path().join("main.cjcl");
let graph = build_module_graph(&entry).unwrap();
assert_eq!(graph.module_count(), 2);
assert!(graph.modules.contains_key(&ModuleId("math".to_string())));
let order = graph.topological_order().unwrap();
let math_pos = order
.iter()
.position(|m| m.0 == "math")
.unwrap();
let main_pos = order
.iter()
.position(|m| m.0 == "main")
.unwrap();
assert!(math_pos < main_pos);
}
#[test]
fn test_detect_cyclic_dependency() {
let dir = setup_test_dir(&[
("main.cjcl", "import a\nlet x = 1;"),
("a.cjcl", "import b\nfn fa() -> i64 { 1 }"),
("b.cjcl", "import a\nfn fb() -> i64 { 2 }"),
]);
let entry = dir.path().join("main.cjcl");
let result = build_module_graph(&entry);
assert!(result.is_err());
match result.unwrap_err() {
ModuleError::CyclicDependency { .. } => {} other => panic!("expected CyclicDependency, got: {:?}", other),
}
}
#[test]
fn test_merge_programs_single_module() {
let dir = setup_test_dir(&[(
"main.cjcl",
"fn greet() -> str { \"hello\" }\nlet msg = greet();",
)]);
let entry = dir.path().join("main.cjcl");
let graph = build_module_graph(&entry).unwrap();
let merged = merge_programs(&graph).unwrap();
assert!(merged.functions.len() >= 2);
let names: Vec<&str> = merged.functions.iter().map(|f| f.name.as_str()).collect();
assert!(names.contains(&"greet"));
assert!(names.contains(&"__main"));
}
#[test]
fn test_merge_programs_prefixes_non_entry() {
let dir = setup_test_dir(&[
("main.cjcl", "import math\nlet x = 1;"),
("math.cjcl", "fn add(a: f64, b: f64) -> f64 { a + b }"),
]);
let entry = dir.path().join("main.cjcl");
let graph = build_module_graph(&entry).unwrap();
let merged = merge_programs(&graph).unwrap();
let names: Vec<&str> = merged.functions.iter().map(|f| f.name.as_str()).collect();
assert!(
names.contains(&"math::add"),
"expected math::add in {:?}",
names
);
}
#[test]
fn test_merge_programs_duplicate_detection() {
let dir = setup_test_dir(&[(
"main.cjcl",
"fn add(a: f64) -> f64 { a }\nfn add(b: f64) -> f64 { b }",
)]);
let entry = dir.path().join("main.cjcl");
let graph = build_module_graph(&entry).unwrap();
let merged = merge_programs(&graph);
assert!(merged.is_err() || {
true
});
}
#[test]
fn test_classify_import_module() {
let (module_path, symbol) =
classify_import(&["math".to_string(), "linalg".to_string()]);
assert_eq!(module_path, vec!["math", "linalg"]);
assert_eq!(symbol, None);
}
#[test]
fn test_classify_import_symbol() {
let (module_path, symbol) =
classify_import(&["math".to_string(), "Matrix".to_string()]);
assert_eq!(module_path, vec!["math"]);
assert_eq!(symbol, Some("Matrix".to_string()));
}
#[test]
fn test_classify_import_single_segment() {
let (module_path, symbol) = classify_import(&["math".to_string()]);
assert_eq!(module_path, vec!["math"]);
assert_eq!(symbol, None);
}
#[test]
fn test_build_import_aliases() {
let module = ModuleInfo {
id: ModuleId("main".to_string()),
file_path: PathBuf::from("main.cjcl"),
imports: vec![ImportInfo {
path: vec!["math".to_string(), "Matrix".to_string()],
alias: Some("M".to_string()),
resolved_module: Some(ModuleId("math".to_string())),
symbol: Some("Matrix".to_string()),
}],
ast: None,
is_entry: true,
};
let aliases = build_import_aliases(&module);
assert_eq!(aliases.get("M"), Some(&"math::Matrix".to_string()));
}
#[test]
fn test_visibility_pub_functions_aliased() {
let dir = setup_test_dir(&[
("main.cjcl", "import math\nlet x = 1;"),
("math.cjcl", "pub fn add(a: f64, b: f64) -> f64 { a + b }\nfn private_helper() -> f64 { 0.0 }"),
]);
let entry = dir.path().join("main.cjcl");
let graph = build_module_graph(&entry).unwrap();
let merged = merge_programs(&graph).unwrap();
let names: Vec<&str> = merged.functions.iter().map(|f| f.name.as_str()).collect();
assert!(names.contains(&"math::add"), "expected math::add in {:?}", names);
assert!(names.contains(&"add"), "expected add alias in {:?}", names);
assert!(names.contains(&"math::private_helper"), "expected math::private_helper in {:?}", names);
assert!(names.contains(&"private_helper"), "private_helper should be aliased (enforcement is separate): {:?}", names);
}
#[test]
fn test_check_visibility_violations() {
let dir = setup_test_dir(&[
("main.cjcl", "import math.Matrix\nlet x = 1;"),
("math.cjcl", "struct Matrix { x: f64 }"),
]);
let entry = dir.path().join("main.cjcl");
let graph = build_module_graph(&entry).unwrap();
let violations = check_visibility(&graph);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].symbol, "Matrix");
assert_eq!(violations[0].kind, "struct");
}
#[test]
fn test_topological_order_deterministic() {
let dir = setup_test_dir(&[
("main.cjcl", "import alpha\nimport beta\nlet x = 1;"),
("alpha.cjcl", "fn a_fn() -> i64 { 1 }"),
("beta.cjcl", "fn b_fn() -> i64 { 2 }"),
]);
let entry = dir.path().join("main.cjcl");
let order1 = build_module_graph(&entry)
.unwrap()
.topological_order()
.unwrap();
let order2 = build_module_graph(&entry)
.unwrap()
.topological_order()
.unwrap();
assert_eq!(order1, order2);
}
}