init lux
This commit is contained in:
634
src/modules.rs
Normal file
634
src/modules.rs
Normal file
@@ -0,0 +1,634 @@
|
||||
//! Module system for the Lux language
|
||||
//!
|
||||
//! Handles loading, parsing, and resolving module imports.
|
||||
|
||||
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<String>,
|
||||
}
|
||||
|
||||
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,
|
||||
// Effects and handlers are always public for now
|
||||
Declaration::Effect(_) | Declaration::Handler(_) => true,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Module loader and resolver
|
||||
pub struct ModuleLoader {
|
||||
/// Base directories to search for modules
|
||||
search_paths: Vec<PathBuf>,
|
||||
/// Cache of loaded modules (path -> module)
|
||||
cache: HashMap<String, Module>,
|
||||
/// Modules currently being loaded (for circular dependency detection)
|
||||
loading: HashSet<String>,
|
||||
}
|
||||
|
||||
impl ModuleLoader {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
search_paths: vec![PathBuf::from(".")],
|
||||
cache: HashMap::new(),
|
||||
loading: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a loader with custom search paths
|
||||
pub fn with_paths(paths: Vec<PathBuf>) -> Self {
|
||||
Self {
|
||||
search_paths: paths,
|
||||
cache: HashMap::new(),
|
||||
loading: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a search path
|
||||
pub fn add_search_path(&mut self, path: PathBuf) {
|
||||
self.search_paths.push(path);
|
||||
}
|
||||
|
||||
/// Resolve a module path to a file path
|
||||
fn resolve_path(&self, module_path: &str) -> Option<PathBuf> {
|
||||
// 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 {
|
||||
let full_path = search_path.join(&relative_path);
|
||||
if full_path.exists() {
|
||||
return Some(full_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<Module, ModuleError> {
|
||||
// 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<Program, ModuleError> {
|
||||
// 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: "<main>".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<String> {
|
||||
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<Item = (&String, &Module)> {
|
||||
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<HashMap<String, ResolvedImport>, 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>): 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user