1use std::collections::{HashMap, HashSet};
20use std::sync::{Arc, Mutex};
21
22use thiserror::Error;
23
24use crate::value::Value;
25
26#[derive(Debug, Clone, Default)]
30pub struct Module {
31 pub path: Arc<str>,
32 pub exports: HashSet<Arc<str>>,
33 pub bindings: HashMap<Arc<str>, Value>,
34}
35
36impl Module {
37 pub fn new(path: impl Into<Arc<str>>) -> Self {
38 Self {
39 path: path.into(),
40 exports: HashSet::new(),
41 bindings: HashMap::new(),
42 }
43 }
44
45 pub fn get_export(&self, name: &str) -> Option<Value> {
48 if self.exports.contains(name) {
49 self.bindings.get(name).cloned()
50 } else {
51 None
52 }
53 }
54
55 pub fn add_export(&mut self, name: impl Into<Arc<str>>) {
57 self.exports.insert(name.into());
58 }
59
60 pub fn define(&mut self, name: impl Into<Arc<str>>, value: Value) {
63 self.bindings.insert(name.into(), value);
64 }
65}
66
67pub trait Loader: Send + Sync {
72 fn load(&self, path: &str) -> Result<String, ModuleError>;
73}
74
75#[derive(Default, Debug, Clone)]
78pub struct MapLoader {
79 pub modules: HashMap<String, String>,
80}
81
82impl MapLoader {
83 pub fn new() -> Self {
84 Self::default()
85 }
86
87 pub fn insert(&mut self, path: impl Into<String>, source: impl Into<String>) -> &mut Self {
88 self.modules.insert(path.into(), source.into());
89 self
90 }
91}
92
93impl Loader for MapLoader {
94 fn load(&self, path: &str) -> Result<String, ModuleError> {
95 self.modules
96 .get(path)
97 .cloned()
98 .ok_or_else(|| ModuleError::NotFound(path.to_string()))
99 }
100}
101
102#[derive(Debug, Default, Clone)]
106pub struct NoLoader;
107
108impl Loader for NoLoader {
109 fn load(&self, path: &str) -> Result<String, ModuleError> {
110 Err(ModuleError::NotFound(path.to_string()))
111 }
112}
113
114#[derive(Debug, Clone)]
129pub struct FilesystemLoader {
130 pub base_dir: std::path::PathBuf,
131 pub extra_search_paths: Vec<std::path::PathBuf>,
132}
133
134impl FilesystemLoader {
135 pub fn new(base_dir: impl Into<std::path::PathBuf>) -> Self {
136 Self {
137 base_dir: base_dir.into(),
138 extra_search_paths: Vec::new(),
139 }
140 }
141
142 pub fn with_search_paths(
143 mut self,
144 paths: impl IntoIterator<Item = std::path::PathBuf>,
145 ) -> Self {
146 self.extra_search_paths.extend(paths);
147 self
148 }
149
150 fn candidates(&self, path: &str) -> Vec<std::path::PathBuf> {
151 let p = std::path::Path::new(path);
152 let has_ext = p
153 .extension()
154 .is_some_and(|e| matches!(e.to_str(), Some("tlisp" | "lisp")));
155 let mut bases: Vec<std::path::PathBuf> = Vec::new();
156 if p.is_absolute() {
157 bases.push(p.to_path_buf());
158 } else {
159 bases.push(self.base_dir.join(p));
160 for extra in &self.extra_search_paths {
161 bases.push(extra.join(p));
162 }
163 }
164 let mut out = Vec::with_capacity(bases.len() * 4);
165 for base in bases {
166 if has_ext {
167 out.push(base);
168 } else {
169 out.push(base.with_extension("tlisp"));
170 out.push(base.with_extension("lisp"));
171 out.push(base.join("init.tlisp"));
172 out.push(base.join("init.lisp"));
173 }
174 }
175 out
176 }
177}
178
179impl Loader for FilesystemLoader {
180 fn load(&self, path: &str) -> Result<String, ModuleError> {
181 for candidate in self.candidates(path) {
182 if let Ok(s) = std::fs::read_to_string(&candidate) {
183 return Ok(s);
184 }
185 }
186 Err(ModuleError::NotFound(path.to_string()))
187 }
188}
189
190#[derive(Debug, Error, Clone)]
193pub enum ModuleError {
194 #[error("module not found: {0}")]
195 NotFound(String),
196 #[error("circular require: {path} (load stack: {stack})")]
197 Circular { path: String, stack: String },
198 #[error("name not exported: {1} from module {0}")]
199 NotExported(String, String),
200}
201
202#[derive(Debug, Default, Clone)]
207pub struct ModuleRegistry {
208 inner: Arc<Mutex<RegistryInner>>,
209}
210
211#[derive(Debug, Default)]
212pub(crate) struct RegistryInner {
213 pub(crate) modules: HashMap<Arc<str>, Module>,
214 pub(crate) loading: Vec<String>,
216 pub(crate) exports_staging: HashMap<String, HashSet<Arc<str>>>,
220}
221
222impl ModuleRegistry {
223 pub fn new() -> Self {
224 Self::default()
225 }
226
227 pub fn has(&self, path: &str) -> bool {
229 let g = self.inner.lock().unwrap();
230 g.modules.contains_key(path)
231 }
232
233 pub fn get(&self, path: &str) -> Option<Module> {
235 let g = self.inner.lock().unwrap();
236 g.modules.get(path).cloned()
237 }
238
239 pub fn begin_load(&self, path: &str) -> Result<(), ModuleError> {
242 let mut g = self.inner.lock().unwrap();
243 if g.loading.iter().any(|p| p == path) {
244 return Err(ModuleError::Circular {
245 path: path.to_string(),
246 stack: g.loading.join(" → "),
247 });
248 }
249 g.loading.push(path.to_string());
250 Ok(())
251 }
252
253 pub fn finish_load(&self, module: Module) {
256 let mut g = self.inner.lock().unwrap();
257 g.loading.retain(|p| **p != *module.path);
258 g.modules.insert(module.path.clone(), module);
259 }
260
261 pub fn abort_load(&self, path: &str) {
264 let mut g = self.inner.lock().unwrap();
265 g.loading.retain(|p| p != path);
266 }
267
268 pub fn len(&self) -> usize {
270 self.inner.lock().unwrap().modules.len()
271 }
272
273 pub fn is_empty(&self) -> bool {
274 self.len() == 0
275 }
276
277 pub(crate) fn inner_lock(&self) -> std::sync::MutexGuard<'_, RegistryInner> {
280 self.inner.lock().unwrap()
281 }
282}
283
284pub fn split_qualified(name: &str) -> Option<(&str, &str)> {
294 let idx = name.rfind('/')?;
295 if idx == 0 || idx == name.len() - 1 {
298 return None;
299 }
300 Some((&name[..idx], &name[idx + 1..]))
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn split_qualified_works() {
309 assert_eq!(split_qualified("foo/bar"), Some(("foo", "bar")));
310 assert_eq!(
311 split_qualified("lib/auth/validate"),
312 Some(("lib/auth", "validate"))
313 );
314 assert_eq!(split_qualified("plain"), None);
315 assert_eq!(split_qualified("/leading"), None);
316 assert_eq!(split_qualified("trailing/"), None);
317 }
318
319 #[test]
320 fn map_loader_round_trips() {
321 let mut l = MapLoader::new();
322 l.insert("lib/auth", "(define x 42)");
323 assert_eq!(l.load("lib/auth").unwrap(), "(define x 42)");
324 assert!(matches!(l.load("missing"), Err(ModuleError::NotFound(_))));
325 }
326
327 #[test]
328 fn registry_cycle_detection() {
329 let r = ModuleRegistry::new();
330 r.begin_load("a").unwrap();
331 r.begin_load("b").unwrap();
332 let err = r.begin_load("a").unwrap_err();
333 assert!(matches!(err, ModuleError::Circular { .. }));
334 }
335
336 #[test]
337 fn registry_finish_load_makes_module_visible() {
338 let r = ModuleRegistry::new();
339 r.begin_load("foo").unwrap();
340 let mut m = Module::new("foo");
341 m.define("x", Value::Int(42));
342 m.add_export("x");
343 r.finish_load(m);
344 assert!(r.has("foo"));
345 let exported = r.get("foo").unwrap().get_export("x");
346 assert!(matches!(exported, Some(Value::Int(42))));
347 }
348
349 #[test]
350 fn registry_finish_load_removes_from_loading() {
351 let r = ModuleRegistry::new();
352 r.begin_load("foo").unwrap();
353 r.finish_load(Module::new("foo"));
354 r.begin_load("foo").unwrap();
356 r.abort_load("foo");
357 }
358
359 #[test]
360 fn filesystem_loader_resolves_with_extensions() {
361 use std::io::Write;
362 let dir = tempfile_dir();
363 let lib = dir.join("lib");
365 std::fs::create_dir_all(&lib).unwrap();
366 let mut f = std::fs::File::create(lib.join("util.tlisp")).unwrap();
367 writeln!(f, "(define x 42)").unwrap();
368
369 let loader = FilesystemLoader::new(&dir);
370 let src = loader.load("lib/util").unwrap();
372 assert!(src.contains("define x 42"));
373
374 let src2 = loader.load("lib/util.tlisp").unwrap();
376 assert_eq!(src, src2);
377
378 assert!(matches!(
380 loader.load("missing/whatever"),
381 Err(ModuleError::NotFound(_))
382 ));
383
384 let _ = std::fs::remove_dir_all(&dir);
385 }
386
387 fn tempfile_dir() -> std::path::PathBuf {
388 use std::time::{SystemTime, UNIX_EPOCH};
389 let nanos = SystemTime::now()
390 .duration_since(UNIX_EPOCH)
391 .unwrap()
392 .as_nanos();
393 let mut tmp = std::env::temp_dir();
394 tmp.push(format!("tatara-loader-test-{nanos}"));
395 std::fs::create_dir_all(&tmp).unwrap();
396 tmp
397 }
398
399 #[test]
400 fn module_get_export_respects_export_set() {
401 let mut m = Module::new("test");
402 m.define("public", Value::Int(1));
403 m.define("private", Value::Int(2));
404 m.add_export("public");
405 assert!(matches!(m.get_export("public"), Some(Value::Int(1))));
406 assert!(matches!(m.get_export("private"), None));
408 }
409}