//! Module system for the Lux language //! //! Handles loading, parsing, and resolving module imports. #![allow(dead_code)] use crate::ast::{Declaration, ImportDecl, Program, Visibility}; use crate::parser::Parser; use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; /// Error during module loading #[derive(Debug, Clone)] pub struct ModuleError { pub message: String, pub module_path: String, } impl std::fmt::Display for ModuleError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Module error in '{}': {}", self.module_path, self.message ) } } impl std::error::Error for ModuleError {} /// A loaded and parsed module #[derive(Debug, Clone)] pub struct Module { /// The module's canonical path (e.g., "std/list") pub path: String, /// The parsed program pub program: Program, /// Names exported by this module (public declarations) pub exports: HashSet, } impl Module { /// Get all public declarations from this module pub fn public_declarations(&self) -> Vec<&Declaration> { self.program .declarations .iter() .filter(|d| { match d { Declaration::Function(f) => f.visibility == Visibility::Public, Declaration::Let(l) => l.visibility == Visibility::Public, Declaration::Type(t) => t.visibility == Visibility::Public, Declaration::Trait(t) => t.visibility == Visibility::Public, // Effects, handlers, and impls are always public for now Declaration::Effect(_) | Declaration::Handler(_) | Declaration::Impl(_) => true, } }) .collect() } } /// Module loader and resolver pub struct ModuleLoader { /// Base directories to search for modules search_paths: Vec, /// Cache of loaded modules (path -> module) cache: HashMap, /// Modules currently being loaded (for circular dependency detection) loading: HashSet, } impl ModuleLoader { pub fn new() -> Self { let mut loader = Self { search_paths: vec![PathBuf::from(".")], cache: HashMap::new(), loading: HashSet::new(), }; // Add package paths if in a project with lux.toml loader.add_package_paths(); loader } /// Create a loader with custom search paths pub fn with_paths(paths: Vec) -> Self { let mut loader = Self { search_paths: paths, cache: HashMap::new(), loading: HashSet::new(), }; // Add package paths if in a project with lux.toml loader.add_package_paths(); loader } /// Add a search path pub fn add_search_path(&mut self, path: PathBuf) { self.search_paths.push(path); } /// Add package search paths from .lux_packages directory fn add_package_paths(&mut self) { // Find project root by looking for lux.toml if let Some(project_root) = Self::find_project_root() { let packages_dir = project_root.join(".lux_packages"); if packages_dir.exists() { // Add the packages directory itself self.search_paths.push(packages_dir.clone()); // Add each installed package directory if let Ok(entries) = fs::read_dir(&packages_dir) { for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { self.search_paths.push(path); } } } } } } /// Find the project root by looking for lux.toml fn find_project_root() -> Option { let mut current = std::env::current_dir().ok()?; loop { if current.join("lux.toml").exists() { return Some(current); } if !current.pop() { return None; } } } /// Resolve a module path to a file path fn resolve_path(&self, module_path: &str) -> Option { // Convert module path (e.g., "std/list") to file path (e.g., "std/list.lux") let relative_path = format!("{}.lux", module_path); for search_path in &self.search_paths { // Try direct path (e.g., search_path/module_path.lux) let full_path = search_path.join(&relative_path); if full_path.exists() { return Some(full_path); } // For packages, try module_path/lib.lux (e.g., .lux_packages/http/lib.lux) let lib_path = search_path.join(module_path).join("lib.lux"); if lib_path.exists() { return Some(lib_path); } // Also try module_path/src/lib.lux for common package layout let src_lib_path = search_path.join(module_path).join("src").join("lib.lux"); if src_lib_path.exists() { return Some(src_lib_path); } } None } /// Load a module by its import path pub fn load_module(&mut self, module_path: &str) -> Result<&Module, ModuleError> { // Check if already cached if self.cache.contains_key(module_path) { return Ok(self.cache.get(module_path).unwrap()); } // Check for circular dependency if self.loading.contains(module_path) { return Err(ModuleError { message: "Circular dependency detected".to_string(), module_path: module_path.to_string(), }); } // Mark as loading self.loading.insert(module_path.to_string()); // Resolve to file path let file_path = self.resolve_path(module_path).ok_or_else(|| ModuleError { message: format!("Module not found. Searched in: {:?}", self.search_paths), module_path: module_path.to_string(), })?; // Load the module let module = self.load_file(&file_path, module_path)?; // Remove from loading set self.loading.remove(module_path); // Cache the module self.cache.insert(module_path.to_string(), module); Ok(self.cache.get(module_path).unwrap()) } /// Load a module from a file path fn load_file(&mut self, file_path: &Path, module_path: &str) -> Result { // Read the file let source = fs::read_to_string(file_path).map_err(|e| ModuleError { message: format!("Failed to read file: {}", e), module_path: module_path.to_string(), })?; // Parse the source let program = Parser::parse_source(&source).map_err(|e| ModuleError { message: format!("Parse error: {}", e), module_path: module_path.to_string(), })?; // Load any imports this module has for import in &program.imports { let import_path = import.path.to_string(); self.load_module(&import_path)?; } // Collect exports let exports = self.collect_exports(&program); Ok(Module { path: module_path.to_string(), program, exports, }) } /// Load a program from source (for REPL or direct execution) pub fn load_source( &mut self, source: &str, base_path: Option<&Path>, ) -> Result { // Add base path to search paths if provided if let Some(base) = base_path { if let Some(parent) = base.parent() { if !self.search_paths.contains(&parent.to_path_buf()) { self.search_paths.push(parent.to_path_buf()); } } } // Parse the source let program = Parser::parse_source(source).map_err(|e| ModuleError { message: format!("Parse error: {}", e), module_path: "
".to_string(), })?; // Load any imports for import in &program.imports { let import_path = import.path.to_string(); self.load_module(&import_path)?; } Ok(program) } /// Collect exported names from a program fn collect_exports(&self, program: &Program) -> HashSet { let mut exports = HashSet::new(); for decl in &program.declarations { match decl { Declaration::Function(f) if f.visibility == Visibility::Public => { exports.insert(f.name.name.clone()); } Declaration::Let(l) if l.visibility == Visibility::Public => { exports.insert(l.name.name.clone()); } Declaration::Type(t) if t.visibility == Visibility::Public => { exports.insert(t.name.name.clone()); } Declaration::Effect(e) => { // Effects are always exported exports.insert(e.name.name.clone()); } Declaration::Handler(h) => { // Handlers are always exported exports.insert(h.name.name.clone()); } _ => {} } } exports } /// Get a cached module pub fn get_module(&self, module_path: &str) -> Option<&Module> { self.cache.get(module_path) } /// Get all loaded modules pub fn loaded_modules(&self) -> impl Iterator { self.cache.iter() } /// Clear the module cache pub fn clear_cache(&mut self) { self.cache.clear(); } /// Resolve imports for a program and return the names to be imported pub fn resolve_imports( &self, imports: &[ImportDecl], ) -> Result, ModuleError> { let mut resolved = HashMap::new(); for import in imports { let module_path = import.path.to_string(); let module = self.get_module(&module_path).ok_or_else(|| ModuleError { message: "Module not loaded".to_string(), module_path: module_path.clone(), })?; let import_name = if let Some(ref alias) = import.alias { // import foo/bar as Baz -> use "Baz" as the name alias.name.clone() } else { // import foo/bar -> use "bar" as the name (last segment) import .path .segments .last() .map(|s| s.name.clone()) .unwrap_or_else(|| module_path.clone()) }; if import.wildcard { // import foo.* -> import all exports directly for export in &module.exports { resolved.insert( export.clone(), ResolvedImport { module_path: module_path.clone(), name: export.clone(), kind: ImportKind::Direct, }, ); } } else if let Some(ref items) = import.items { // import foo.{a, b, c} -> import specific items for item in items { if !module.exports.contains(&item.name) { return Err(ModuleError { message: format!("'{}' is not exported from module", item.name), module_path: module_path.clone(), }); } resolved.insert( item.name.clone(), ResolvedImport { module_path: module_path.clone(), name: item.name.clone(), kind: ImportKind::Direct, }, ); } } else { // import foo/bar -> import as module object resolved.insert( import_name, ResolvedImport { module_path: module_path.clone(), name: module_path.clone(), kind: ImportKind::Module, }, ); } } Ok(resolved) } } impl Default for ModuleLoader { fn default() -> Self { Self::new() } } /// A resolved import #[derive(Debug, Clone)] pub struct ResolvedImport { /// The module path this import comes from pub module_path: String, /// The name being imported pub name: String, /// What kind of import this is pub kind: ImportKind, } /// Kind of import #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ImportKind { /// Import as a module object (import foo/bar) Module, /// Direct import of a name (import foo.{bar} or import foo.*) Direct, } #[cfg(test)] mod tests { use super::*; use std::io::Write; use tempfile::TempDir; fn create_test_module(dir: &Path, name: &str, content: &str) -> PathBuf { let path = dir.join(format!("{}.lux", name)); if let Some(parent) = path.parent() { fs::create_dir_all(parent).unwrap(); } let mut file = fs::File::create(&path).unwrap(); file.write_all(content.as_bytes()).unwrap(); path } #[test] fn test_load_simple_module() { let dir = TempDir::new().unwrap(); create_test_module( dir.path(), "math", r#" pub fn add(a: Int, b: Int): Int = a + b pub fn sub(a: Int, b: Int): Int = a - b fn private_fn(): Int = 42 "#, ); let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); let module = loader.load_module("math").unwrap(); assert_eq!(module.path, "math"); assert!(module.exports.contains("add")); assert!(module.exports.contains("sub")); assert!(!module.exports.contains("private_fn")); } #[test] fn test_load_nested_module() { let dir = TempDir::new().unwrap(); create_test_module( dir.path(), "std/list", r#" pub fn length(list: List): Int = 0 "#, ); let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); let module = loader.load_module("std/list").unwrap(); assert_eq!(module.path, "std/list"); assert!(module.exports.contains("length")); } #[test] fn test_module_not_found() { let dir = TempDir::new().unwrap(); let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); let result = loader.load_module("nonexistent"); assert!(result.is_err()); assert!(result.unwrap_err().message.contains("not found")); } #[test] fn test_circular_dependency_detection() { let dir = TempDir::new().unwrap(); create_test_module( dir.path(), "a", r#" import b pub fn foo(): Int = 1 "#, ); create_test_module( dir.path(), "b", r#" import a pub fn bar(): Int = 2 "#, ); let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); let result = loader.load_module("a"); assert!(result.is_err()); assert!(result.unwrap_err().message.contains("Circular")); } #[test] fn test_module_caching() { let dir = TempDir::new().unwrap(); create_test_module( dir.path(), "cached", r#" pub fn foo(): Int = 42 "#, ); let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); // Load twice loader.load_module("cached").unwrap(); loader.load_module("cached").unwrap(); // Should only be in cache once assert_eq!(loader.cache.len(), 1); } #[test] fn test_end_to_end_module_import() { use crate::interpreter::Interpreter; use crate::typechecker::TypeChecker; let dir = TempDir::new().unwrap(); // Create a utility module with public functions create_test_module( dir.path(), "utils", r#" pub fn double(x: Int): Int = x * 2 pub fn square(x: Int): Int = x * x fn private_helper(): Int = 0 "#, ); // Create a main program that imports and uses the module let main_source = r#" import utils let result = utils.double(21) "#; // Set up module loader let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); // Load and parse the main program let main_path = dir.path().join("main.lux"); let program = loader.load_source(main_source, Some(&main_path)).unwrap(); // Type check with module support let mut checker = TypeChecker::new(); checker .check_program_with_modules(&program, &loader) .unwrap(); // Run with module support let mut interp = Interpreter::new(); let result = interp.run_with_modules(&program, &loader).unwrap(); // Should evaluate to 42 assert_eq!(format!("{}", result), "42"); } #[test] fn test_selective_import() { use crate::interpreter::Interpreter; use crate::typechecker::TypeChecker; let dir = TempDir::new().unwrap(); // Create a module with multiple exports create_test_module( dir.path(), "math", r#" pub fn add(a: Int, b: Int): Int = a + b pub fn mul(a: Int, b: Int): Int = a * b "#, ); // Import only the add function let main_source = r#" import math.{add} let result = add(10, 5) "#; let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); let main_path = dir.path().join("main.lux"); let program = loader.load_source(main_source, Some(&main_path)).unwrap(); let mut checker = TypeChecker::new(); checker .check_program_with_modules(&program, &loader) .unwrap(); let mut interp = Interpreter::new(); let result = interp.run_with_modules(&program, &loader).unwrap(); assert_eq!(format!("{}", result), "15"); } #[test] fn test_module_with_alias() { use crate::interpreter::Interpreter; use crate::typechecker::TypeChecker; let dir = TempDir::new().unwrap(); // Create a nested module create_test_module( dir.path(), "lib/helpers", r#" pub fn greet(): String = "hello" "#, ); // Import with alias let main_source = r#" import lib/helpers as h let result = h.greet() "#; let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); let main_path = dir.path().join("main.lux"); let program = loader.load_source(main_source, Some(&main_path)).unwrap(); let mut checker = TypeChecker::new(); checker .check_program_with_modules(&program, &loader) .unwrap(); let mut interp = Interpreter::new(); let result = interp.run_with_modules(&program, &loader).unwrap(); assert_eq!(format!("{}", result), "\"hello\""); } #[test] fn test_transitive_imports() { use crate::interpreter::Interpreter; use crate::typechecker::TypeChecker; let dir = TempDir::new().unwrap(); // Create base module create_test_module( dir.path(), "base", r#" pub fn value(): Int = 100 "#, ); // Create mid module that imports base create_test_module( dir.path(), "mid", r#" import base pub fn doubled(): Int = base.value() * 2 "#, ); // Create main that imports mid let main_source = r#" import mid let result = mid.doubled() "#; let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); let main_path = dir.path().join("main.lux"); let program = loader.load_source(main_source, Some(&main_path)).unwrap(); let mut checker = TypeChecker::new(); checker .check_program_with_modules(&program, &loader) .unwrap(); let mut interp = Interpreter::new(); let result = interp.run_with_modules(&program, &loader).unwrap(); assert_eq!(format!("{}", result), "200"); } #[test] fn test_package_lib_lux_entry_point() { // Test that packages with lib.lux are correctly resolved let dir = TempDir::new().unwrap(); // Create a package structure: mypackage/lib.lux let pkg_dir = dir.path().join("mypackage"); fs::create_dir_all(&pkg_dir).unwrap(); let lib_content = r#" pub fn getValue(): Int = 42 "#; fs::write(pkg_dir.join("lib.lux"), lib_content).unwrap(); // The loader should find mypackage via lib.lux let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); let module = loader.load_module("mypackage"); assert!(module.is_ok(), "Should find package via lib.lux"); let module = module.unwrap(); assert!(module.exports.contains("getValue")); } #[test] fn test_package_src_lib_lux_entry_point() { // Test that packages with src/lib.lux are correctly resolved let dir = TempDir::new().unwrap(); // Create a package structure: mypackage/src/lib.lux let src_dir = dir.path().join("anotherpackage").join("src"); fs::create_dir_all(&src_dir).unwrap(); let lib_content = r#" pub fn compute(): Int = 100 "#; fs::write(src_dir.join("lib.lux"), lib_content).unwrap(); // The loader should find anotherpackage via src/lib.lux let mut loader = ModuleLoader::with_paths(vec![dir.path().to_path_buf()]); let module = loader.load_module("anotherpackage"); assert!(module.is_ok(), "Should find package via src/lib.lux"); let module = module.unwrap(); assert!(module.exports.contains("compute")); } #[test] fn test_lux_packages_directory_resolution() { use crate::interpreter::Interpreter; use crate::typechecker::TypeChecker; let dir = TempDir::new().unwrap(); // Create a lux.toml to make this a "project" fs::write( dir.path().join("lux.toml"), "[project]\nname = \"testproj\"\nversion = \"0.1.0\"\n[dependencies]\n", ) .unwrap(); // Create .lux_packages/mylib/lib.lux let pkg_dir = dir.path().join(".lux_packages").join("mylib"); fs::create_dir_all(&pkg_dir).unwrap(); fs::write( pkg_dir.join("lib.lux"), "pub fn helper(): Int = 999\n", ) .unwrap(); // Create main.lux that imports mylib let main_source = r#" import mylib let result = mylib.helper() "#; // Change to the project directory to test find_project_root let original_dir = std::env::current_dir().unwrap(); std::env::set_current_dir(dir.path()).unwrap(); let mut loader = ModuleLoader::new(); let main_path = dir.path().join("main.lux"); let program = loader.load_source(main_source, Some(&main_path)).unwrap(); let mut checker = TypeChecker::new(); checker .check_program_with_modules(&program, &loader) .unwrap(); let mut interp = Interpreter::new(); let result = interp.run_with_modules(&program, &loader).unwrap(); // Restore original directory std::env::set_current_dir(original_dir).unwrap(); assert_eq!(format!("{}", result), "999"); } }