use std::collections::HashSet;
use std::path::{Path, PathBuf};
use crate::phonetic::llev::{self, LLevFile};
use super::ast::{ImportDirective, LLreFile, ResolvedImport, SymbolTable};
use super::error::{LLreError, LLreErrorKind, LLreResult};
use super::parser;
#[derive(Debug, Clone)]
pub struct LoaderConfig {
pub search_paths: Vec<PathBuf>,
pub max_import_depth: usize,
pub resolve_imports: bool,
}
impl Default for LoaderConfig {
fn default() -> Self {
Self {
search_paths: vec![PathBuf::from(".")],
max_import_depth: 10,
resolve_imports: true,
}
}
}
impl LoaderConfig {
pub fn with_search_paths(search_paths: Vec<PathBuf>) -> Self {
Self {
search_paths,
..Default::default()
}
}
pub fn add_search_path(&mut self, path: impl Into<PathBuf>) {
self.search_paths.push(path.into());
}
}
pub struct Loader {
config: LoaderConfig,
loading_stack: HashSet<PathBuf>,
}
impl Loader {
pub fn new() -> Self {
Self {
config: LoaderConfig::default(),
loading_stack: HashSet::new(),
}
}
pub fn with_config(config: LoaderConfig) -> Self {
Self {
config,
loading_stack: HashSet::new(),
}
}
pub fn load<P: AsRef<Path>>(&mut self, path: P) -> LLreResult<LLreFile> {
let path = path.as_ref();
let canonical = self.canonicalize_path(path)?;
if self.loading_stack.contains(&canonical) {
return Err(LLreError::new(LLreErrorKind::CircularImport(canonical)));
}
let content = std::fs::read_to_string(path).map_err(|e| {
LLreError::with_file(
match e.kind() {
std::io::ErrorKind::NotFound => {
LLreErrorKind::FileNotFound(path.display().to_string())
}
std::io::ErrorKind::PermissionDenied => {
LLreErrorKind::PermissionDenied(path.display().to_string())
}
_ => LLreErrorKind::IoError(e.to_string()),
},
path,
)
})?;
let mut file = parser::parse_str(&content)?;
file.source_file = Some(canonical.clone());
if self.config.resolve_imports && !file.imports.is_empty() {
self.loading_stack.insert(canonical.clone());
self.resolve_imports(&mut file, path.parent())?;
self.loading_stack.remove(&canonical);
}
Ok(file)
}
pub fn load_str(&self, content: &str) -> LLreResult<LLreFile> {
parser::parse_str(content)
}
fn resolve_imports(&mut self, file: &mut LLreFile, base_dir: Option<&Path>) -> LLreResult<()> {
let mut resolved_imports = Vec::new();
let mut symbol_table = SymbolTable::new();
for import in &file.imports {
let resolved = self.resolve_import(import, base_dir)?;
symbol_table.merge(&resolved.1);
resolved_imports.push(ResolvedImport {
directive: import.clone(),
resolved_path: resolved.0,
symbols: resolved.1.symbol_names(),
rules: Vec::new(), });
}
file.resolved_imports = resolved_imports;
file.symbol_table = symbol_table;
Ok(())
}
fn resolve_import(
&mut self,
import: &ImportDirective,
base_dir: Option<&Path>,
) -> LLreResult<(PathBuf, SymbolTable)> {
let mut search_paths = Vec::new();
if let Some(base) = base_dir {
search_paths.push(base.to_path_buf());
}
search_paths.extend(self.config.search_paths.clone());
let resolved_path = self.find_file(&import.path, &search_paths).ok_or_else(|| {
LLreError::with_position(
LLreErrorKind::ImportNotFound {
path: import.path.clone(),
search_paths: search_paths.clone(),
},
import.position,
)
})?;
if self.loading_stack.len() >= self.config.max_import_depth {
return Err(LLreError::new(LLreErrorKind::ImportDepthExceeded {
max: self.config.max_import_depth,
path: resolved_path,
}));
}
let llev_config = llev::LoaderConfig {
include_paths: search_paths.clone(),
max_include_depth: self.config.max_import_depth,
allow_missing_includes: false,
};
let llev_loader = llev::Loader::with_config(llev_config);
let llev_file = llev_loader
.load(&resolved_path)
.map_err(|e| LLreError::from_llev(&e))?;
let symbol_table = self.extract_symbols(&llev_file, import.alias.as_deref())?;
Ok((resolved_path, symbol_table))
}
fn find_file(&self, path: &str, search_paths: &[PathBuf]) -> Option<PathBuf> {
let path = Path::new(path);
if path.is_absolute() {
if path.exists() {
return Some(path.to_path_buf());
}
return None;
}
for base in search_paths {
let full_path = base.join(path);
if full_path.exists() {
return Some(full_path);
}
}
None
}
fn canonicalize_path(&self, path: &Path) -> LLreResult<PathBuf> {
std::fs::canonicalize(path).or_else(|_| {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
std::env::current_dir()
.map(|cwd| cwd.join(path))
.map_err(|e| LLreError::new(LLreErrorKind::IoError(e.to_string())))
}
})
}
fn extract_symbols(&self, file: &LLevFile, alias: Option<&str>) -> LLreResult<SymbolTable> {
let mut table = SymbolTable::new();
let source = file.source_file.clone();
for symbol in &file.symbols {
let name = if let Some(alias) = alias {
format!("{}_{}", alias, symbol.name)
} else {
symbol.name.clone()
};
match &symbol.value {
llev::Expression::CharClass { chars, .. } => {
table.add_char_class(name, chars.clone(), source.clone());
}
llev::Expression::Char(c) => {
table.add_char_class(name, vec![*c], source.clone());
}
_ => {
}
}
}
Ok(table)
}
}
impl Default for Loader {
fn default() -> Self {
Self::new()
}
}
pub fn load_file<P: AsRef<Path>>(path: P) -> LLreResult<LLreFile> {
let mut loader = Loader::new();
loader.load(path)
}
pub fn load_file_with_config<P: AsRef<Path>>(
path: P,
config: LoaderConfig,
) -> LLreResult<LLreFile> {
let mut loader = Loader::with_config(config);
loader.load(path)
}
pub fn parse_str(content: &str) -> LLreResult<LLreFile> {
parser::parse_str(content)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_loader_config_default() {
let config = LoaderConfig::default();
assert_eq!(config.search_paths, vec![PathBuf::from(".")]);
assert_eq!(config.max_import_depth, 10);
assert!(config.resolve_imports);
}
#[test]
fn test_load_simple_file() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = temp_dir.path().join("test.llre");
let content = r#"
@name "Test"
^hello$
"#;
std::fs::write(&file_path, content).expect("Failed to write file");
let file = load_file(&file_path).expect("Failed to load file");
assert_eq!(file.metadata.name, Some("Test".to_string()));
assert!(file.source_file.is_some());
}
#[test]
fn test_load_file_not_found() {
let result = load_file("/nonexistent/path/test.llre");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err.kind, LLreErrorKind::FileNotFound(_)));
}
#[test]
fn test_load_with_import() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let llev_path = temp_dir.path().join("symbols.llev");
let llev_content = r#"
@name "Symbols"
@define VOWEL = [aeiou]
"#;
std::fs::write(&llev_path, llev_content).expect("Failed to write llev file");
let llre_path = temp_dir.path().join("test.llre");
let llre_content = r#"
@import "symbols.llev"
^[a-z]+$
"#;
std::fs::write(&llre_path, llre_content).expect("Failed to write llre file");
let config = LoaderConfig {
search_paths: vec![temp_dir.path().to_path_buf()],
..Default::default()
};
let file = load_file_with_config(&llre_path, config).expect("Failed to load file");
assert_eq!(file.imports.len(), 1);
assert_eq!(file.resolved_imports.len(), 1);
assert!(file.symbol_table.contains("VOWEL"));
}
#[test]
fn test_load_with_aliased_import() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let llev_path = temp_dir.path().join("english.llev");
let llev_content = r#"
@name "English"
@define VOWEL = [aeiou]
"#;
std::fs::write(&llev_path, llev_content).expect("Failed to write llev file");
let llre_path = temp_dir.path().join("test.llre");
let llre_content = r#"
@import "english.llev" as en
^[a-z]+$
"#;
std::fs::write(&llre_path, llre_content).expect("Failed to write llre file");
let config = LoaderConfig {
search_paths: vec![temp_dir.path().to_path_buf()],
..Default::default()
};
let file = load_file_with_config(&llre_path, config).expect("Failed to load file");
assert!(file.symbol_table.contains("en_VOWEL"));
assert!(!file.symbol_table.contains("VOWEL"));
}
#[test]
fn test_import_not_found() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let llre_path = temp_dir.path().join("test.llre");
let llre_content = r#"
@import "nonexistent.llev"
^test$
"#;
std::fs::write(&llre_path, llre_content).expect("Failed to write llre file");
let result = load_file(&llre_path);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err.kind, LLreErrorKind::ImportNotFound { .. }));
}
#[test]
fn test_parse_str_no_imports() {
let content = r#"
@name "Test"
@import "symbols.llev" # This won't be resolved
^hello$
"#;
let file = parse_str(content).expect("Failed to parse");
assert_eq!(file.imports.len(), 1);
assert!(file.resolved_imports.is_empty());
}
}