Skip to main content

cljrs_env/
loader.rs

1//! Namespace file loader: resolves `require` to source files and evaluates them.
2
3use std::sync::Arc;
4
5use crate::env::{Env, GlobalEnv, RequireRefer, RequireSpec};
6use crate::error::{EvalError, EvalResult};
7
8/// Find, load, and wire up the source file for `spec.ns`.
9///
10/// - Idempotent: if already loaded, skips file evaluation but still applies
11///   alias/refer in the *current* namespace.
12/// - Cycle detection: returns an error if `spec.ns` is currently being loaded.
13pub fn load_ns(globals: Arc<GlobalEnv>, spec: &RequireSpec, current_ns: &str) -> EvalResult<()> {
14    let ns_name = &spec.ns;
15
16    // Skip file loading if already done, but still apply alias/refer.
17    let already_loaded = globals.is_loaded(ns_name);
18    if !already_loaded {
19        // Cycle detection.
20        {
21            let mut loading = globals.loading.lock().unwrap();
22            if loading.contains(ns_name.as_ref()) {
23                return Err(EvalError::Runtime(format!("circular require: {ns_name}")));
24            }
25            loading.insert(ns_name.clone());
26        }
27
28        // Resolve namespace name: check built-in registry first, then disk.
29        // Clojure convention: dots → path separators, hyphens → underscores.
30        let rel_path = ns_name.replace('.', "/").replace('-', "_");
31        let src_paths = globals.source_paths.read().unwrap().clone();
32        let (src, file_path): (String, String) =
33            if let Some(builtin) = globals.builtin_source(ns_name) {
34                (builtin.to_owned(), format!("<builtin:{ns_name}>"))
35            } else {
36                find_source_file(&rel_path, &src_paths).ok_or_else(|| {
37                    EvalError::Runtime(format!("Could not find namespace {ns_name} on source path"))
38                })?
39            };
40
41        // Pre-refer clojure.core so code in the file can use core fns before (ns ...).
42        if ns_name.as_ref() != "clojure.core" {
43            globals.refer_all(ns_name, "clojure.core");
44        }
45
46        // Evaluate the file in a new Env rooted at the namespace being loaded.
47        // Save and restore *ns* so the caller's namespace is not disturbed.
48        let saved_ns = globals
49            .lookup_var("clojure.core", "*ns*")
50            .and_then(|v| crate::dynamics::deref_var(&v));
51        {
52            let mut env = Env::new(globals.clone(), ns_name);
53            let mut parser = cljrs_reader::Parser::new(src, file_path);
54            let forms = parser.parse_all().map_err(EvalError::Read)?;
55            for form in forms {
56                // Alloc frame per top-level form: all allocations during this
57                // form's evaluation are rooted.  Frame pops between forms,
58                // allowing GC to collect temporaries from previous forms.
59                let _alloc_frame = cljrs_gc::push_alloc_frame();
60                (*globals)
61                    .eval(&form, &mut env)
62                    .map_err(|e| annotate(e, ns_name))?;
63            }
64        }
65        // Restore *ns* to the caller's namespace.
66        if let Some(saved) = saved_ns
67            && let Some(var) = globals.lookup_var("clojure.core", "*ns*")
68        {
69            var.get().bind(saved);
70        }
71
72        // Mark loaded and remove from in-progress set.
73        globals.loading.lock().unwrap().remove(ns_name.as_ref());
74        globals.mark_loaded(ns_name);
75    }
76
77    // Apply alias.
78    if let Some(alias) = &spec.alias {
79        globals.add_alias(current_ns, alias, ns_name);
80    }
81
82    // Apply refer.
83    match &spec.refer {
84        RequireRefer::None => {}
85        RequireRefer::All => globals.refer_all(current_ns, ns_name),
86        RequireRefer::Named(names) => globals.refer_named(current_ns, ns_name, names),
87    }
88
89    Ok(())
90}
91
92fn find_source_file(rel: &str, src_paths: &[std::path::PathBuf]) -> Option<(String, String)> {
93    for dir in src_paths {
94        for ext in &[".cljrs", ".cljc"] {
95            let path = dir.join(format!("{rel}{ext}"));
96            if path.exists() {
97                let src = std::fs::read_to_string(&path).ok()?;
98                return Some((src, path.display().to_string()));
99            }
100        }
101    }
102    None
103}
104
105/// Wrap an EvalError with namespace context.  Read errors (which carry
106/// file/line/col in CljxError) are passed through unchanged so the CLI can
107/// render them with full location information.
108fn annotate(e: EvalError, ns_name: &Arc<str>) -> EvalError {
109    match e {
110        // Preserve read errors — they carry source location.
111        EvalError::Read(_) => e,
112        // Propagate recur unchanged (internal signal).
113        EvalError::Recur(_) => e,
114        // Annotate everything else with the namespace being loaded.
115        other => EvalError::Runtime(format!("in {ns_name}: {other}")),
116    }
117}