use crate::ppu::PpuFile;
use crate::{Module, ModuleError, ModuleResult};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
pub struct ModuleLoader {
cache: Arc<RwLock<HashMap<String, Module>>>,
search_paths: Vec<PathBuf>,
unit_extension: String,
}
impl ModuleLoader {
pub fn new() -> Self {
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
search_paths: vec![PathBuf::from("."), PathBuf::from("./stdlib")],
unit_extension: "pas".to_string(),
}
}
pub fn clone_for_thread(&self) -> Self {
Self {
cache: Arc::clone(&self.cache),
search_paths: self.search_paths.clone(),
unit_extension: self.unit_extension.clone(),
}
}
pub fn add_search_path<P: AsRef<Path>>(&mut self, path: P) {
let path = path.as_ref().to_path_buf();
if !self.search_paths.contains(&path) {
self.search_paths.push(path);
}
}
pub fn set_unit_extension(&mut self, ext: String) {
self.unit_extension = ext;
}
pub fn find_unit_file(&self, unit_name: &str) -> ModuleResult<PathBuf> {
let filename = format!("{}.{}", unit_name.to_lowercase(), self.unit_extension);
for search_path in &self.search_paths {
let full_path = search_path.join(&filename);
if full_path.exists() {
return Ok(full_path);
}
}
Err(ModuleError::ModuleNotFound(unit_name.to_string()))
}
pub fn load_unit_source(&self, unit_name: &str) -> ModuleResult<(PathBuf, String)> {
let path = self.find_unit_file(unit_name)?;
let source = fs::read_to_string(&path)
.map_err(|e| ModuleError::LoadError(unit_name.to_string(), e.to_string()))?;
Ok((path, source))
}
pub fn is_cached(&self, unit_name: &str) -> bool {
self.cache.read().unwrap().contains_key(unit_name)
}
pub fn get_cached(&self, unit_name: &str) -> Option<Module> {
self.cache.read().unwrap().get(unit_name).cloned()
}
pub fn cache_module(&mut self, module: Module) {
self.cache
.write()
.unwrap()
.insert(module.name.clone(), module);
}
pub fn clear_cache(&mut self) {
self.cache.write().unwrap().clear();
}
pub fn cached_modules(&self) -> Vec<String> {
self.cache.read().unwrap().keys().cloned().collect()
}
pub fn unit_exists(&self, unit_name: &str) -> bool {
self.find_unit_file(unit_name).is_ok()
}
pub fn get_unit_mtime(&self, unit_name: &str) -> ModuleResult<std::time::SystemTime> {
let path = self.find_unit_file(unit_name)?;
let metadata = fs::metadata(&path)
.map_err(|e| ModuleError::LoadError(unit_name.to_string(), e.to_string()))?;
metadata
.modified()
.map_err(|e| ModuleError::LoadError(unit_name.to_string(), e.to_string()))
}
pub fn is_cache_valid(&self, unit_name: &str) -> bool {
if let Some(_cached) = self.cache.read().unwrap().get(unit_name) {
if let Ok(_file_time) = self.get_unit_mtime(unit_name) {
return true;
}
}
false
}
pub fn find_ppu_file(&self, unit_name: &str) -> ModuleResult<PathBuf> {
let filename = format!("{}.ppu", unit_name.to_lowercase());
for search_path in &self.search_paths {
let full_path = search_path.join(&filename);
if full_path.exists() {
return Ok(full_path);
}
}
Err(ModuleError::ModuleNotFound(unit_name.to_string()))
}
pub fn load_from_ppu(&self, unit_name: &str) -> ModuleResult<crate::ast::Unit> {
let ppu_path = self.find_ppu_file(unit_name)?;
let ppu = PpuFile::load(&ppu_path)?;
if !ppu.verify_checksum() {
return Err(ModuleError::LoadError(
unit_name.to_string(),
"PPU checksum verification failed".to_string(),
));
}
Ok(ppu.module.unit)
}
pub fn save_to_ppu(
&self,
unit: &crate::ast::Unit,
output_dir: Option<&Path>,
) -> ModuleResult<PathBuf> {
let module = Module {
name: unit.name.clone(),
unit: unit.clone(),
dependencies: unit.uses.clone(),
};
let ppu = PpuFile::new(module);
let dir = output_dir.unwrap_or_else(|| Path::new("."));
let filename = format!("{}.ppu", unit.name.to_lowercase());
let ppu_path = dir.join(filename);
ppu.save(&ppu_path)?;
Ok(ppu_path)
}
pub fn is_ppu_up_to_date(&self, unit_name: &str) -> bool {
if let (Ok(ppu_path), Ok(src_path)) = (
self.find_ppu_file(unit_name),
self.find_unit_file(unit_name),
) {
if let (Ok(ppu_meta), Ok(src_meta)) = (fs::metadata(&ppu_path), fs::metadata(&src_path))
{
if let (Ok(ppu_time), Ok(src_time)) = (ppu_meta.modified(), src_meta.modified()) {
return ppu_time >= src_time;
}
}
}
false
}
}
impl Default for ModuleLoader {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "tokio")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
pub trait AsyncModuleLoader: Send + Sync {
fn load_unit_source_async(
&self,
unit_name: &str,
) -> impl std::future::Future<Output = ModuleResult<(PathBuf, String)>> + Send;
}
#[cfg(feature = "tokio")]
impl AsyncModuleLoader for ModuleLoader {
async fn load_unit_source_async(
&self,
unit_name: &str,
) -> ModuleResult<(PathBuf, String)> {
let path = self.find_unit_file(unit_name)?;
let path_clone = path.clone();
let name = unit_name.to_string();
let source = tokio::task::spawn_blocking(move || {
std::fs::read_to_string(&path_clone)
.map_err(|e| ModuleError::LoadError(name, e.to_string()))
})
.await
.map_err(|e| ModuleError::LoadError(unit_name.to_string(), e.to_string()))??;
Ok((path, source))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
fn create_test_module(name: &str) -> Module {
Module {
name: name.to_string(),
unit: crate::ast::Unit {
name: name.to_string(),
uses: vec![],
interface: crate::ast::UnitInterface {
uses: vec![],
types: vec![],
constants: vec![],
variables: vec![],
procedures: vec![],
functions: vec![],
classes: vec![],
interfaces: vec![],
},
implementation: crate::ast::UnitImplementation {
uses: vec![],
types: vec![],
constants: vec![],
variables: vec![],
procedures: vec![],
functions: vec![],
classes: vec![],
interfaces: vec![],
initialization: None,
finalization: None,
},
},
dependencies: vec![],
}
}
#[test]
fn test_loader_creation() {
let loader = ModuleLoader::new();
assert_eq!(loader.search_paths.len(), 2);
assert_eq!(loader.unit_extension, "pas");
}
#[test]
fn test_add_search_path() {
let mut loader = ModuleLoader::new();
loader.add_search_path("/usr/lib/pascal");
assert_eq!(loader.search_paths.len(), 3);
loader.add_search_path("/usr/lib/pascal");
assert_eq!(loader.search_paths.len(), 3);
}
#[test]
fn test_cache_operations() {
let loader = ModuleLoader::new();
assert!(!loader.is_cached("TestUnit"));
assert_eq!(loader.cached_modules().len(), 0);
}
#[test]
fn test_clone_for_thread() {
let loader = ModuleLoader::new();
let cloned = loader.clone_for_thread();
assert_eq!(loader.cached_modules().len(), cloned.cached_modules().len());
}
#[test]
fn test_cache_module_and_retrieve() {
let mut loader = ModuleLoader::new();
let module = create_test_module("TestModule");
loader.cache_module(module.clone());
assert!(loader.is_cached("TestModule"));
let cached = loader.get_cached("TestModule");
assert!(cached.is_some());
assert_eq!(cached.unwrap().name, "TestModule");
}
#[test]
fn test_clear_cache() {
let mut loader = ModuleLoader::new();
let module = create_test_module("TestModule");
loader.cache_module(module);
assert!(loader.is_cached("TestModule"));
loader.clear_cache();
assert!(!loader.is_cached("TestModule"));
assert_eq!(loader.cached_modules().len(), 0);
}
#[test]
fn test_concurrent_cache_access() {
let loader = ModuleLoader::new();
let mut handles = vec![];
for i in 0..10 {
let loader_clone = loader.clone_for_thread();
let handle = thread::spawn(move || {
for _ in 0..100 {
let _ = loader_clone.is_cached(&format!("Module{}", i));
let _ = loader_clone.cached_modules();
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
#[test]
fn test_concurrent_cache_write() {
let mut loader = ModuleLoader::new();
let mut handles = vec![];
for i in 0..5 {
let mut loader_clone = loader.clone_for_thread();
let handle = thread::spawn(move || {
let module = create_test_module(&format!("Module{}", i));
loader_clone.cache_module(module);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(loader.cached_modules().len(), 5);
}
#[test]
fn test_cached_modules_list() {
let mut loader = ModuleLoader::new();
for i in 0..3 {
let module = create_test_module(&format!("Module{}", i));
loader.cache_module(module);
}
let modules = loader.cached_modules();
assert_eq!(modules.len(), 3);
assert!(modules.contains(&"Module0".to_string()));
assert!(modules.contains(&"Module1".to_string()));
assert!(modules.contains(&"Module2".to_string()));
}
}