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/// - Same-thread cycle detection: returns an error if the current thread is
13///   already loading `spec.ns` (true circular require).
14/// - Cross-thread coordination: if a *different* thread is loading `spec.ns`,
15///   waits for it to finish (via `GlobalEnv::loading_done`) instead of
16///   reporting a spurious "circular require" error.
17pub fn load_ns(globals: Arc<GlobalEnv>, spec: &RequireSpec, current_ns: &str) -> EvalResult<()> {
18    let ns_name = &spec.ns;
19
20    if !globals.is_loaded(ns_name) {
21        // Try to claim this namespace for loading, or wait if another thread
22        // is already loading it.
23        let should_load = claim_or_wait(&globals, ns_name)?;
24
25        if should_load {
26            let result = do_load(&globals, ns_name);
27
28            // Release the claim and notify any waiting threads.
29            globals.loading.lock().unwrap().remove(ns_name.as_ref());
30            if result.is_ok() {
31                globals.mark_loaded(ns_name);
32            }
33            globals.loading_done.notify_all();
34
35            result?;
36        }
37    }
38
39    // Apply alias.
40    if let Some(alias) = &spec.alias {
41        globals.add_alias(current_ns, alias, ns_name);
42    }
43
44    // Apply refer.
45    match &spec.refer {
46        RequireRefer::None => {}
47        RequireRefer::All => globals.refer_all(current_ns, ns_name),
48        RequireRefer::Named(names) => globals.refer_named(current_ns, ns_name, names),
49    }
50
51    Ok(())
52}
53
54/// Claim `ns_name` for loading by the current thread, or wait until another
55/// thread that claimed it finishes.
56///
57/// Returns `Ok(true)` if the caller claimed the namespace and must load it.
58/// Returns `Ok(false)` if another thread loaded it while we waited.
59/// Returns `Err` on a genuine circular require (same thread).
60fn claim_or_wait(globals: &Arc<GlobalEnv>, ns_name: &Arc<str>) -> EvalResult<bool> {
61    let tid = std::thread::current().id();
62    loop {
63        let mut loading = globals.loading.lock().unwrap();
64        match loading.get(ns_name.as_ref()) {
65            None => {
66                loading.insert(ns_name.clone(), tid);
67                return Ok(true);
68            }
69            Some(&owner) if owner == tid => {
70                return Err(EvalError::Runtime(format!("circular require: {ns_name}")));
71            }
72            Some(_) => {
73                // A different thread is loading this namespace.  Wait for it
74                // to finish (the Condvar releases `loading` while sleeping).
75                let _guard = globals.loading_done.wait(loading).unwrap();
76                // After waking, the namespace may now be fully loaded.
77                if globals.is_loaded(ns_name) {
78                    return Ok(false);
79                }
80                // Otherwise loop and try to claim again.
81            }
82        }
83    }
84}
85
86/// Evaluate the source file for `ns_name`, returning Ok(()) or an error.
87/// The caller is responsible for claiming/releasing the namespace in the
88/// `loading` map.
89fn do_load(globals: &Arc<GlobalEnv>, ns_name: &Arc<str>) -> EvalResult<()> {
90    // Resolve namespace name: check built-in registry first, then disk.
91    // Clojure convention: dots → path separators, hyphens → underscores.
92    let rel_path = ns_name.replace('.', "/").replace('-', "_");
93    let src_paths = globals.source_paths.read().unwrap().clone();
94    let (src, file_path): (String, String) = if let Some(builtin) = globals.builtin_source(ns_name)
95    {
96        (builtin.to_owned(), format!("<builtin:{ns_name}>"))
97    } else {
98        find_source_file(&rel_path, &src_paths).ok_or_else(|| {
99            EvalError::Runtime(format!("Could not find namespace {ns_name} on source path"))
100        })?
101    };
102
103    // Pre-refer clojure.core so code in the file can use core fns before (ns ...).
104    if ns_name.as_ref() != "clojure.core" {
105        globals.refer_all(ns_name, "clojure.core");
106    }
107
108    // Evaluate the file in a new Env rooted at the namespace being loaded.
109    // Save and restore *ns* so the caller's namespace is not disturbed.
110    let saved_ns = globals
111        .lookup_var("clojure.core", "*ns*")
112        .and_then(|v| crate::dynamics::deref_var(&v));
113    {
114        let mut env = Env::new(globals.clone(), ns_name);
115        let mut parser = cljrs_reader::Parser::new(src, file_path);
116        let forms = parser.parse_all().map_err(EvalError::Read)?;
117        for form in forms {
118            // Alloc frame per top-level form: all allocations during this
119            // form's evaluation are rooted.  Frame pops between forms,
120            // allowing GC to collect temporaries from previous forms.
121            let _alloc_frame = cljrs_gc::push_alloc_frame();
122            (*globals)
123                .eval(&form, &mut env)
124                .map_err(|e| annotate(e, ns_name))?;
125        }
126    }
127    // Restore *ns* to the caller's namespace.
128    if let Some(saved) = saved_ns
129        && let Some(var) = globals.lookup_var("clojure.core", "*ns*")
130    {
131        var.get().bind(saved);
132    }
133
134    Ok(())
135}
136
137fn find_source_file(rel: &str, src_paths: &[std::path::PathBuf]) -> Option<(String, String)> {
138    for dir in src_paths {
139        for ext in &[".cljrs", ".cljc"] {
140            let path = dir.join(format!("{rel}{ext}"));
141            if path.exists() {
142                let src = std::fs::read_to_string(&path).ok()?;
143                return Some((src, path.display().to_string()));
144            }
145        }
146    }
147    None
148}
149
150/// Wrap an EvalError with namespace context.  Read errors (which carry
151/// file/line/col in CljxError) are passed through unchanged so the CLI can
152/// render them with full location information.
153fn annotate(e: EvalError, ns_name: &Arc<str>) -> EvalError {
154    match e {
155        // Preserve read errors — they carry source location.
156        EvalError::Read(_) => e,
157        // Propagate recur unchanged (internal signal).
158        EvalError::Recur(_) => e,
159        // Annotate everything else with the namespace being loaded.
160        other => EvalError::Runtime(format!("in {ns_name}: {other}")),
161    }
162}