1use std::collections::{HashMap, HashSet};
9use std::path::{Path, PathBuf};
10use tl_errors::{RuntimeError, TlError};
11
12#[derive(Debug, Clone)]
14pub struct ExportedItem {
15 pub name: String,
16 pub is_public: bool,
17}
18
19#[derive(Debug, Clone)]
21pub struct ModuleExports {
22 pub items: HashMap<String, ExportedItem>,
23 pub file_path: PathBuf,
24}
25
26#[derive(Debug, Clone)]
28pub struct ResolvedModule {
29 pub file_path: PathBuf,
31 pub item_name: Option<String>,
33}
34
35pub struct ModuleResolver {
37 root: PathBuf,
39 current_file: Option<PathBuf>,
41 module_cache: HashMap<PathBuf, ModuleExports>,
43 importing: HashSet<PathBuf>,
45}
46
47impl ModuleResolver {
48 pub fn new(root: PathBuf) -> Self {
49 Self {
50 root,
51 current_file: None,
52 module_cache: HashMap::new(),
53 importing: HashSet::new(),
54 }
55 }
56
57 pub fn set_current_file(&mut self, path: Option<PathBuf>) {
58 self.current_file = path;
59 }
60
61 pub fn root(&self) -> &Path {
62 &self.root
63 }
64
65 pub fn resolve_path(&self, segments: &[String]) -> Result<ResolvedModule, TlError> {
73 let base = self.base_dir();
74
75 if segments.is_empty() {
76 return Err(module_err("Empty module path".to_string()));
77 }
78
79 let rel_path: PathBuf = segments.iter().collect();
81
82 let file_path = base.join(&rel_path).with_extension("tl");
84 if file_path.exists() {
85 return Ok(ResolvedModule {
86 file_path,
87 item_name: None,
88 });
89 }
90
91 let dir_path = base.join(&rel_path).join("mod.tl");
93 if dir_path.exists() {
94 return Ok(ResolvedModule {
95 file_path: dir_path,
96 item_name: None,
97 });
98 }
99
100 if segments.len() > 1 {
102 let (parent_segs, item_name) = segments.split_at(segments.len() - 1);
103 let parent_path: PathBuf = parent_segs.iter().collect();
104
105 let parent_file = base.join(&parent_path).with_extension("tl");
107 if parent_file.exists() {
108 return Ok(ResolvedModule {
109 file_path: parent_file,
110 item_name: Some(item_name[0].clone()),
111 });
112 }
113
114 let parent_dir = base.join(&parent_path).join("mod.tl");
116 if parent_dir.exists() {
117 return Ok(ResolvedModule {
118 file_path: parent_dir,
119 item_name: Some(item_name[0].clone()),
120 });
121 }
122 }
123
124 Err(module_err(format!(
125 "Module not found: `{}`. Searched in: {}",
126 segments.join("."),
127 base.display()
128 )))
129 }
130
131 pub fn resolve_prefix(&self, segments: &[String]) -> Result<PathBuf, TlError> {
134 let base = self.base_dir();
135
136 if segments.is_empty() {
137 return Err(module_err("Empty module path".to_string()));
138 }
139
140 let rel_path: PathBuf = segments.iter().collect();
141
142 let file_path = base.join(&rel_path).with_extension("tl");
144 if file_path.exists() {
145 return Ok(file_path);
146 }
147
148 let dir_path = base.join(&rel_path).join("mod.tl");
150 if dir_path.exists() {
151 return Ok(dir_path);
152 }
153
154 Err(module_err(format!(
155 "Module not found: `{}`",
156 segments.join(".")
157 )))
158 }
159
160 pub fn begin_import(&mut self, path: &Path) -> Result<(), TlError> {
162 let canonical = self.canonicalize(path);
163 if self.importing.contains(&canonical) {
164 return Err(module_err(format!(
165 "Circular import detected: {}",
166 canonical.display()
167 )));
168 }
169 self.importing.insert(canonical);
170 Ok(())
171 }
172
173 pub fn end_import(&mut self, path: &Path) {
175 let canonical = self.canonicalize(path);
176 self.importing.remove(&canonical);
177 }
178
179 pub fn get_cached(&self, path: &Path) -> Option<&ModuleExports> {
181 let canonical = self.canonicalize(path);
182 self.module_cache.get(&canonical)
183 }
184
185 pub fn cache_module(&mut self, path: &Path, exports: ModuleExports) {
187 let canonical = self.canonicalize(path);
188 self.module_cache.insert(canonical, exports);
189 }
190
191 fn base_dir(&self) -> PathBuf {
192 if let Some(ref current) = self.current_file {
193 current.parent().unwrap_or(Path::new(".")).to_path_buf()
194 } else {
195 self.root.clone()
196 }
197 }
198
199 fn canonicalize(&self, path: &Path) -> PathBuf {
200 path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
201 }
202
203 pub fn resolve_package_path(
206 &self,
207 segments: &[String],
208 package_roots: &HashMap<String, PathBuf>,
209 ) -> Option<ResolvedModule> {
210 if segments.is_empty() {
211 return None;
212 }
213
214 let pkg_name = &segments[0];
215 let pkg_name_hyphen = pkg_name.replace('_', "-");
216 let pkg_root = package_roots
217 .get(pkg_name.as_str())
218 .or_else(|| package_roots.get(&pkg_name_hyphen))?;
219
220 let remaining = &segments[1..];
221
222 if remaining.is_empty() {
223 let src = pkg_root.join("src");
225 for entry in &["lib.tl", "mod.tl", "main.tl"] {
226 let p = src.join(entry);
227 if p.exists() {
228 return Some(ResolvedModule {
229 file_path: p,
230 item_name: None,
231 });
232 }
233 }
234 for entry in &["mod.tl", "lib.tl"] {
235 let p = pkg_root.join(entry);
236 if p.exists() {
237 return Some(ResolvedModule {
238 file_path: p,
239 item_name: None,
240 });
241 }
242 }
243 return None;
244 }
245
246 let rel: PathBuf = remaining.iter().collect();
247 let src = pkg_root.join("src");
248
249 let file_path = src.join(&rel).with_extension("tl");
251 if file_path.exists() {
252 return Some(ResolvedModule {
253 file_path,
254 item_name: None,
255 });
256 }
257
258 let dir_path = src.join(&rel).join("mod.tl");
260 if dir_path.exists() {
261 return Some(ResolvedModule {
262 file_path: dir_path,
263 item_name: None,
264 });
265 }
266
267 let file_path = pkg_root.join(&rel).with_extension("tl");
269 if file_path.exists() {
270 return Some(ResolvedModule {
271 file_path,
272 item_name: None,
273 });
274 }
275
276 if remaining.len() > 1 {
278 let parent: PathBuf = remaining[..remaining.len() - 1].iter().collect();
279 let item = remaining.last().unwrap().clone();
280 let parent_file = src.join(&parent).with_extension("tl");
281 if parent_file.exists() {
282 return Some(ResolvedModule {
283 file_path: parent_file,
284 item_name: Some(item),
285 });
286 }
287 }
288
289 None
290 }
291}
292
293fn module_err(message: String) -> TlError {
294 TlError::Runtime(RuntimeError {
295 message,
296 span: None,
297 stack_trace: vec![],
298 })
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use std::fs;
305
306 fn setup_test_dir() -> tempfile::TempDir {
307 let dir = tempfile::tempdir().unwrap();
308
309 let src = dir.path();
311 fs::write(src.join("math.tl"), "pub fn add(a, b) { a + b }").unwrap();
312 fs::create_dir_all(src.join("data")).unwrap();
313 fs::write(src.join("data/transforms.tl"), "pub fn clean(x) { x }").unwrap();
314 fs::create_dir_all(src.join("utils")).unwrap();
315 fs::write(src.join("utils/mod.tl"), "pub fn helper() { 1 }").unwrap();
316 fs::create_dir_all(src.join("nested/deep")).unwrap();
317 fs::write(src.join("nested/deep/mod.tl"), "pub fn deep_fn() { 42 }").unwrap();
318
319 dir
320 }
321
322 #[test]
323 fn test_resolve_file_module() {
324 let dir = setup_test_dir();
325 let resolver = ModuleResolver::new(dir.path().to_path_buf());
326
327 let result = resolver.resolve_path(&["math".into()]).unwrap();
328 assert_eq!(result.file_path, dir.path().join("math.tl"));
329 assert!(result.item_name.is_none());
330 }
331
332 #[test]
333 fn test_resolve_nested_file_module() {
334 let dir = setup_test_dir();
335 let resolver = ModuleResolver::new(dir.path().to_path_buf());
336
337 let result = resolver
338 .resolve_path(&["data".into(), "transforms".into()])
339 .unwrap();
340 assert_eq!(result.file_path, dir.path().join("data/transforms.tl"));
341 assert!(result.item_name.is_none());
342 }
343
344 #[test]
345 fn test_resolve_directory_module() {
346 let dir = setup_test_dir();
347 let resolver = ModuleResolver::new(dir.path().to_path_buf());
348
349 let result = resolver.resolve_path(&["utils".into()]).unwrap();
350 assert_eq!(result.file_path, dir.path().join("utils/mod.tl"));
351 assert!(result.item_name.is_none());
352 }
353
354 #[test]
355 fn test_resolve_item_within_module() {
356 let dir = setup_test_dir();
357 let resolver = ModuleResolver::new(dir.path().to_path_buf());
358
359 let result = resolver
361 .resolve_path(&["math".into(), "add".into()])
362 .unwrap();
363 assert_eq!(result.file_path, dir.path().join("math.tl"));
364 assert_eq!(result.item_name, Some("add".into()));
365 }
366
367 #[test]
368 fn test_circular_detection() {
369 let dir = setup_test_dir();
370 let mut resolver = ModuleResolver::new(dir.path().to_path_buf());
371
372 let path = dir.path().join("math.tl");
373 resolver.begin_import(&path).unwrap();
374 let result = resolver.begin_import(&path);
375 assert!(result.is_err());
376 assert!(format!("{:?}", result).contains("Circular import"));
377 }
378
379 #[test]
380 fn test_module_not_found() {
381 let dir = setup_test_dir();
382 let resolver = ModuleResolver::new(dir.path().to_path_buf());
383
384 let result = resolver.resolve_path(&["nonexistent".into()]);
385 assert!(result.is_err());
386 }
387}