use super::{Module, ModuleId, ModuleNamespace, ModuleError, ModuleProvider, ModuleSource, ModuleMetadata};
use crate::diagnostics::{Error, Result};
use crate::runtime::LibraryPathResolver;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::fs;
pub struct ModuleLoader {
search_paths: Vec<PathBuf>,
builtin_providers: HashMap<String, Box<dyn ModuleProvider>>,
stdlib_path: Option<PathBuf>,
library_resolver: LibraryPathResolver,
}
impl std::fmt::Debug for ModuleLoader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModuleLoader")
.field("search_paths", &self.search_paths)
.field("builtin_providers", &format!("{} providers", self.builtin_providers.len()))
.field("stdlib_path", &self.stdlib_path)
.field("library_resolver", &self.library_resolver)
.finish()
}
}
impl ModuleLoader {
pub fn new() -> Result<Self> {
let library_resolver = LibraryPathResolver::new()?;
let mut loader = Self {
search_paths: Vec::new(),
builtin_providers: HashMap::new(),
stdlib_path: library_resolver.primary_lib_dir().map(|p| p.to_path_buf()),
library_resolver,
};
loader.initialize_from_library_resolver();
loader.register_builtin_providers();
Ok(loader)
}
pub fn load(&mut self, id: &ModuleId) -> Result<Module> {
match id.namespace {
ModuleNamespace::Builtin => self.load_builtin_module(id),
ModuleNamespace::R7RS => self.load_r7rs_module(id),
ModuleNamespace::SRFI => self.load_srfi_module(id),
ModuleNamespace::User => self.load_user_module(id),
ModuleNamespace::File => self.load_file_module(id),
}
}
pub fn add_search_path<P: AsRef<Path>>(&mut self, path: P) {
self.search_paths.push(path.as_ref().to_path_buf());
}
pub fn set_stdlib_path<P: AsRef<Path>>(&mut self, path: P) {
self.stdlib_path = Some(path.as_ref().to_path_buf());
}
pub fn register_builtin_provider(&mut self, namespace: String, provider: Box<dyn ModuleProvider>) {
self.builtin_providers.insert(namespace, provider);
}
fn load_builtin_module(&self, id: &ModuleId) -> Result<Module> {
if id.components.is_empty() {
return Err(Box::new(Error::from(ModuleError::InvalidDefinition(
"Built-in module name cannot be empty".to_string()
))));
}
let module_name = &id.components[0];
if let Some(provider) = self.builtin_providers.get(module_name) {
return provider.get_module(id);
}
self.load_from_stdlib_path(id, "modules")
}
fn load_r7rs_module(&self, id: &ModuleId) -> Result<Module> {
if id.components.is_empty() {
return Err(Box::new(Error::from(ModuleError::InvalidDefinition(
"R7RS module name cannot be empty".to_string()
))));
}
self.load_from_stdlib_path(id, "r7rs")
}
fn load_srfi_module(&self, id: &ModuleId) -> Result<Module> {
if id.components.is_empty() {
return Err(Box::new(Error::from(ModuleError::InvalidDefinition(
"SRFI module name cannot be empty".to_string()
))));
}
if id.components.len() == 1 {
self.load_single_srfi(id, &id.components[0])
} else {
self.load_multiple_srfis(id)
}
}
fn load_single_srfi(&self, id: &ModuleId, srfi_number: &str) -> Result<Module> {
let srfi_filename = format!("{srfi_number}.scm");
match self.library_resolver.resolve_library_file("modules/srfi", &srfi_filename) {
Ok(module_path) => self.load_from_file(id, &module_path),
Err(_) => {
let stdlib_path = self.stdlib_path.as_ref().ok_or_else(|| {
Error::from(ModuleError::NotFound(id.clone()))
})?;
let module_path = stdlib_path.join("modules").join("srfi").join(&srfi_filename);
if !module_path.exists() {
return Err(Box::new(Error::from(ModuleError::NotFound(id.clone()))));
}
self.load_from_file(id, &module_path)
}
}
}
fn load_multiple_srfis(&self, id: &ModuleId) -> Result<Module> {
let mut combined_exports = HashMap::new();
let mut all_dependencies = Vec::new();
let mut metadata = ModuleMetadata::default();
for srfi_number in &id.components {
let single_srfi_id = super::name::srfi_module(
srfi_number.parse::<u32>().map_err(|_| {
Error::from(ModuleError::InvalidDefinition(
format!("Invalid SRFI number: {srfi_number}")
))
})?
);
let srfi_module = self.load_single_srfi(&single_srfi_id, srfi_number)?;
for (name, value) in srfi_module.exports {
combined_exports.insert(name, value);
}
all_dependencies.extend(srfi_module.dependencies);
if let Some(desc) = srfi_module.metadata.description {
if let Some(existing_desc) = &metadata.description {
metadata.description = Some(format!("{existing_desc}, {desc}"));
} else {
metadata.description = Some(desc);
}
}
}
all_dependencies.sort();
all_dependencies.dedup();
Ok(Module {
id: id.clone(),
exports: combined_exports,
dependencies: all_dependencies,
source: Some(ModuleSource::Source(
format!("Combined SRFI modules: {}", id.components.join(", "))
)),
metadata,
})
}
fn load_user_module(&self, id: &ModuleId) -> Result<Module> {
let module_filename = format!("{}.scm", id.components.join("-"));
for search_path in &self.search_paths {
let module_path = search_path.join(&module_filename);
if module_path.exists() {
return self.load_from_file(id, &module_path);
}
}
if let Some(stdlib_path) = &self.stdlib_path {
let user_path = stdlib_path.join("user").join(&module_filename);
if user_path.exists() {
return self.load_from_file(id, &user_path);
}
}
Err(Box::new(Error::from(ModuleError::NotFound(id.clone()))))
}
fn load_file_module(&self, id: &ModuleId) -> Result<Module> {
if id.components.len() != 1 {
return Err(Box::new(Error::from(ModuleError::InvalidDefinition(
"File module must specify exactly one path".to_string()
))));
}
let file_path = PathBuf::from(&id.components[0]);
if !file_path.exists() {
return Err(Box::new(Error::from(ModuleError::NotFound(id.clone()))));
}
self.load_from_file(id, &file_path)
}
fn load_from_file(&self, id: &ModuleId, path: &Path) -> Result<Module> {
let _source_code = fs::read_to_string(path).map_err(|e| {
Error::io_error(format!("Failed to read module file {}: {}", path.display(), e))
})?;
Ok(Module {
id: id.clone(),
exports: HashMap::new(),
dependencies: Vec::new(),
source: Some(ModuleSource::File(path.to_path_buf())),
metadata: ModuleMetadata::default(),
})
}
fn load_from_stdlib_path(&self, id: &ModuleId, subdir: &str) -> Result<Module> {
let module_filename = format!("{}.scm", id.components.join("-"));
match self.library_resolver.resolve_library_file(subdir, &module_filename) {
Ok(module_path) => self.load_from_file(id, &module_path),
Err(_) => {
let stdlib_path = self.stdlib_path.as_ref().ok_or_else(|| {
Error::from(ModuleError::NotFound(id.clone()))
})?;
let module_path = stdlib_path.join(subdir).join(&module_filename);
if !module_path.exists() {
return Err(Box::new(Error::from(ModuleError::NotFound(id.clone()))));
}
self.load_from_file(id, &module_path)
}
}
}
fn initialize_from_library_resolver(&mut self) {
for path in self.library_resolver.search_paths() {
self.search_paths.push(path.clone());
}
if let Ok(current_dir) = std::env::current_dir() {
if !self.search_paths.contains(¤t_dir) {
self.search_paths.push(current_dir);
}
}
if let Some(home_dir) = dirs::home_dir() {
let home_modules = home_dir.join(".lambdust").join("modules");
if home_modules.exists() && !self.search_paths.contains(&home_modules) {
self.search_paths.push(home_modules);
}
}
}
fn register_builtin_providers(&mut self) {
self.builtin_providers.insert(
"string".to_string(),
Box::new(BuiltinStringModuleProvider)
);
self.builtin_providers.insert(
"list".to_string(),
Box::new(BuiltinListModuleProvider)
);
}
pub fn discover_modules(&self) -> Vec<ModuleId> {
let mut modules = Vec::new();
for provider_name in self.builtin_providers.keys() {
modules.push(ModuleId {
components: vec![provider_name.clone()],
namespace: ModuleNamespace::Builtin,
});
}
for search_path in &self.search_paths {
if let Ok(entries) = fs::read_dir(search_path) {
for entry in entries.flatten() {
if let Some(filename) = entry.file_name().to_str() {
if filename.ends_with(".scm") {
let module_name = filename.strip_suffix(".scm").unwrap();
modules.push(ModuleId {
components: vec![module_name.to_string()],
namespace: ModuleNamespace::User,
});
}
}
}
}
}
let r7rs_files = self.library_resolver.find_library_files("r7rs", ".scm");
for file_path in r7rs_files {
if let Some(filename) = file_path.file_stem().and_then(|s| s.to_str()) {
modules.push(ModuleId {
components: vec![filename.to_string()],
namespace: ModuleNamespace::R7RS,
});
}
}
modules
}
pub fn library_resolver(&self) -> &LibraryPathResolver {
&self.library_resolver
}
pub fn validate_library_setup(&self) -> Result<crate::runtime::LibraryValidationReport> {
self.library_resolver.validate_library_setup()
}
}
struct BuiltinStringModuleProvider;
impl ModuleProvider for BuiltinStringModuleProvider {
fn get_module(&self, id: &ModuleId) -> Result<Module> {
if id.components.len() != 1 || id.components[0] != "string" {
return Err(Box::new(Error::from(ModuleError::NotFound(id.clone()))));
}
let exports = HashMap::new();
Ok(Module {
id: id.clone(),
exports,
dependencies: Vec::new(),
source: Some(ModuleSource::Builtin),
metadata: ModuleMetadata {
description: Some("String manipulation operations".to_string()),
..Default::default()
},
})
}
fn has_module(&self, id: &ModuleId) -> bool {
id.namespace == ModuleNamespace::Builtin
&& id.components.len() == 1
&& id.components[0] == "string"
}
fn list_modules(&self) -> Vec<ModuleId> {
vec![ModuleId {
components: vec!["string".to_string()],
namespace: ModuleNamespace::Builtin,
}]
}
}
struct BuiltinListModuleProvider;
impl ModuleProvider for BuiltinListModuleProvider {
fn get_module(&self, id: &ModuleId) -> Result<Module> {
if id.components.len() != 1 || id.components[0] != "list" {
return Err(Box::new(Error::from(ModuleError::NotFound(id.clone()))));
}
let exports = HashMap::new();
Ok(Module {
id: id.clone(),
exports,
dependencies: Vec::new(),
source: Some(ModuleSource::Builtin),
metadata: ModuleMetadata {
description: Some("List processing operations".to_string()),
..Default::default()
},
})
}
fn has_module(&self, id: &ModuleId) -> bool {
id.namespace == ModuleNamespace::Builtin
&& id.components.len() == 1
&& id.components[0] == "list"
}
fn list_modules(&self) -> Vec<ModuleId> {
vec![ModuleId {
components: vec!["list".to_string()],
namespace: ModuleNamespace::Builtin,
}]
}
}
#[cfg(not(test))]
mod dirs {
use std::path::PathBuf;
pub fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME").map(PathBuf::from)
}
}
#[cfg(test)]
mod dirs {
use std::path::PathBuf;
pub fn home_dir() -> Option<PathBuf> {
Some(PathBuf::from("/tmp/test-home"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::name;
#[test]
fn test_module_loader_creation() {
let loader = ModuleLoader::new();
assert!(loader.is_ok());
}
#[test]
fn test_builtin_string_module() {
let provider = BuiltinStringModuleProvider;
let id = name::builtin_module("string");
assert!(provider.has_module(&id));
let module = provider.get_module(&id);
assert!(module.is_ok());
}
#[test]
fn test_search_path_management() {
let mut loader = ModuleLoader::new().unwrap();
let test_path = PathBuf::from("/test/path");
loader.add_search_path(&test_path);
assert!(loader.search_paths.contains(&test_path));
}
}