fusabi_frontend/
loader.rs1use crate::ast::Program;
21use crate::lexer::{LexError, Lexer};
22use crate::parser::{ParseError, Parser};
23use std::collections::{HashMap, HashSet};
24use std::fmt;
25use std::path::{Path, PathBuf};
26
27#[derive(Debug, Clone)]
29pub enum LoadError {
30 FileNotFound(PathBuf),
32 CircularDependency(Vec<PathBuf>),
34 ParseError(PathBuf, ParseError),
36 LexError(PathBuf, LexError),
38 IoError(String),
40}
41
42impl fmt::Display for LoadError {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 LoadError::FileNotFound(path) => write!(f, "File not found: {}", path.display()),
46 LoadError::CircularDependency(paths) => {
47 write!(f, "Circular dependency detected: ")?;
48 for (i, path) in paths.iter().enumerate() {
49 if i > 0 {
50 write!(f, " -> ")?;
51 }
52 write!(f, "{}", path.display())?;
53 }
54 Ok(())
55 }
56 LoadError::ParseError(path, err) => {
57 write!(f, "Parse error in {}: {}", path.display(), err)
58 }
59 LoadError::LexError(path, err) => write!(f, "Lex error in {}: {}", path.display(), err),
60 LoadError::IoError(msg) => write!(f, "IO error: {}", msg),
61 }
62 }
63}
64
65impl std::error::Error for LoadError {}
66
67#[derive(Debug, Clone)]
69pub struct LoadedFile {
70 pub path: PathBuf,
72 pub program: Program,
74}
75
76pub struct FileLoader {
78 cache: HashMap<PathBuf, LoadedFile>,
80 loading: HashSet<PathBuf>,
82 base_dir: PathBuf,
84}
85
86impl FileLoader {
87 pub fn new(base_dir: PathBuf) -> Self {
89 Self {
90 cache: HashMap::new(),
91 loading: HashSet::new(),
92 base_dir,
93 }
94 }
95
96 pub fn load(&mut self, path: &str, from_file: &Path) -> Result<&LoadedFile, LoadError> {
105 let resolved = self.resolve_path(path, from_file)?;
106
107 if self.cache.contains_key(&resolved) {
109 return Ok(self.cache.get(&resolved).unwrap());
110 }
111
112 if self.loading.contains(&resolved) {
114 let mut cycle: Vec<PathBuf> = self.loading.iter().cloned().collect();
115 cycle.push(resolved.clone());
116 return Err(LoadError::CircularDependency(cycle));
117 }
118
119 self.loading.insert(resolved.clone());
121
122 let source = std::fs::read_to_string(&resolved)
124 .map_err(|e| LoadError::IoError(format!("{}: {}", resolved.display(), e)))?;
125
126 let mut lexer = Lexer::new(&source);
127 let tokens = lexer
128 .tokenize()
129 .map_err(|e| LoadError::LexError(resolved.clone(), e))?;
130
131 let mut parser = Parser::new(tokens);
132 let program = parser
133 .parse_program()
134 .map_err(|e| LoadError::ParseError(resolved.clone(), e))?;
135
136 for directive in &program.directives {
138 self.load(&directive.path, &resolved)?;
139 }
140
141 let loaded = LoadedFile {
143 path: resolved.clone(),
144 program,
145 };
146
147 self.loading.remove(&resolved);
149 self.cache.insert(resolved.clone(), loaded);
150
151 Ok(self.cache.get(&resolved).unwrap())
152 }
153
154 fn resolve_path(&self, path: &str, from_file: &Path) -> Result<PathBuf, LoadError> {
156 let resolved = if path.starts_with('/') {
157 PathBuf::from(path)
159 } else {
160 from_file
162 .parent()
163 .unwrap_or(&self.base_dir)
164 .join(path)
165 };
166
167 resolved
169 .canonicalize()
170 .map_err(|_| LoadError::FileNotFound(resolved.clone()))
171 }
172
173 pub fn get_cached(&self, path: &Path) -> Option<&LoadedFile> {
175 self.cache.get(path)
176 }
177
178 pub fn clear_cache(&mut self) {
180 self.cache.clear();
181 }
182
183 pub fn invalidate(&mut self, path: &Path) {
185 self.cache.remove(path);
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use std::fs;
193 use tempfile::TempDir;
194
195 #[test]
196 fn test_load_simple_file() {
197 let temp_dir = TempDir::new().unwrap();
198 let test_file = temp_dir.path().join("test.fsx");
199 let from_file = temp_dir.path().join("from.fsx");
200 fs::write(&test_file, "let x = 42").unwrap();
201
202 let mut loader = FileLoader::new(temp_dir.path().to_path_buf());
203 let loaded = loader.load("test.fsx", &from_file).unwrap();
204
205 assert_eq!(loaded.program.items.len(), 1);
206 }
207
208 #[test]
209 fn test_circular_dependency_detection() {
210 let temp_dir = TempDir::new().unwrap();
211 let a_file = temp_dir.path().join("a.fsx");
212 let b_file = temp_dir.path().join("b.fsx");
213
214 fs::write(&a_file, "#load \"b.fsx\"\nlet a = 1").unwrap();
215 fs::write(&b_file, "#load \"a.fsx\"\nlet b = 2").unwrap();
216
217 let mut loader = FileLoader::new(temp_dir.path().to_path_buf());
218 let result = loader.load("a.fsx", &a_file);
219
220 assert!(matches!(result, Err(LoadError::CircularDependency(_))));
221 }
222
223 #[test]
224 fn test_caching() {
225 let temp_dir = TempDir::new().unwrap();
226 let test_file = temp_dir.path().join("cached.fsx");
227 let from_file = temp_dir.path().join("from.fsx");
228 fs::write(&test_file, "let x = 42").unwrap();
229
230 let mut loader = FileLoader::new(temp_dir.path().to_path_buf());
231
232 loader.load("cached.fsx", &from_file).unwrap();
234 loader.load("cached.fsx", &from_file).unwrap();
235
236 assert_eq!(loader.cache.len(), 1);
238 }
239
240 #[test]
241 fn test_nested_loads() {
242 let temp_dir = TempDir::new().unwrap();
243 let utils_file = temp_dir.path().join("utils.fsx");
244 let main_file = temp_dir.path().join("main.fsx");
245
246 fs::write(&utils_file, "let helper x = x + 1").unwrap();
247 fs::write(&main_file, "#load \"utils.fsx\"\nlet result = helper 42").unwrap();
248
249 let mut loader = FileLoader::new(temp_dir.path().to_path_buf());
250 let loaded = loader.load("main.fsx", &main_file).unwrap();
251
252 assert_eq!(loaded.program.directives.len(), 1);
254 assert_eq!(loader.cache.len(), 2); }
256
257 #[test]
258 fn test_file_not_found() {
259 let temp_dir = TempDir::new().unwrap();
260 let from_file = temp_dir.path().join("from.fsx");
261 let mut loader = FileLoader::new(temp_dir.path().to_path_buf());
262 let result = loader.load("nonexistent.fsx", &from_file);
263
264 assert!(matches!(result, Err(LoadError::FileNotFound(_))));
265 }
266}