Skip to main content

cljrs_runtime/env/
loader.rs

1//! Namespace file loader: resolves `require` to source files and evaluates them.
2
3#[cfg(not(target_arch = "wasm32"))]
4use std::path::Path;
5use std::sync::Arc;
6
7use crate::env::env::{Env, GlobalEnv, RequireRefer, RequireSpec};
8use crate::env::error::{EvalError, EvalResult};
9
10/// Find, load, and wire up the source file for `spec.ns`.
11///
12/// - Idempotent: if already loaded, skips file evaluation but still applies
13///   alias/refer in the *current* namespace.
14/// - Same-thread cycle detection: returns an error if the current thread is
15///   already loading `spec.ns` (true circular require).
16/// - Cross-thread coordination: if a *different* thread is loading `spec.ns`,
17///   waits for it to finish (via `GlobalEnv::loading_done`) instead of
18///   reporting a spurious "circular require" error.
19/// - Versioned require: if `spec.version` is set, delegates to
20///   `load_versioned_ns` which fetches source at the given commit.
21pub fn load_ns(globals: Arc<GlobalEnv>, spec: &RequireSpec, current_ns: &str) -> EvalResult<()> {
22    // Versioned require: delegate entirely to the versioned loader.
23    #[cfg(not(target_arch = "wasm32"))]
24    if let Some(ref commit) = spec.version {
25        return load_versioned_ns(globals, spec, commit, current_ns);
26    }
27    #[cfg(target_arch = "wasm32")]
28    if spec.version.is_some() {
29        return Err(EvalError::Runtime(
30            "versioned require is not supported in WASM".to_string(),
31        ));
32    }
33
34    let ns_name = &spec.ns;
35
36    if !globals.is_loaded(ns_name) {
37        // Try to claim this namespace for loading, or wait if another thread
38        // is already loading it.
39        let should_load = claim_or_wait(&globals, ns_name)?;
40
41        if should_load {
42            let result = do_load(&globals, ns_name);
43
44            // Release the claim and notify any waiting threads.
45            globals.loading.lock().unwrap().remove(ns_name.as_ref());
46            if result.is_ok() {
47                globals.mark_loaded(ns_name);
48            }
49            globals.loading_done.notify_all();
50
51            result?;
52        }
53    }
54
55    // Apply alias.
56    if let Some(alias) = &spec.alias {
57        globals.add_alias(current_ns, alias, ns_name);
58    }
59
60    // Apply refer.
61    match &spec.refer {
62        RequireRefer::None => {}
63        RequireRefer::All => globals.refer_all(current_ns, ns_name),
64        RequireRefer::Named(names) => globals.refer_named(current_ns, ns_name, names),
65    }
66
67    Ok(())
68}
69
70/// Claim `ns_name` for loading by the current thread, or wait until another
71/// thread that claimed it finishes.
72///
73/// Returns `Ok(true)` if the caller claimed the namespace and must load it.
74/// Returns `Ok(false)` if another thread loaded it while we waited.
75/// Returns `Err` on a genuine circular require (same thread).
76pub(crate) fn claim_or_wait(globals: &Arc<GlobalEnv>, ns_name: &Arc<str>) -> EvalResult<bool> {
77    let tid = std::thread::current().id();
78    loop {
79        let mut loading = globals.loading.lock().unwrap();
80        match loading.get(ns_name.as_ref()) {
81            None => {
82                loading.insert(ns_name.clone(), tid);
83                return Ok(true);
84            }
85            Some(&owner) if owner == tid => {
86                return Err(EvalError::Runtime(format!("circular require: {ns_name}")));
87            }
88            Some(_) => {
89                // A different thread is loading this namespace.  Wait for it
90                // to finish (the Condvar releases `loading` while sleeping).
91                let _guard = globals.loading_done.wait(loading).unwrap();
92                // After waking, the namespace may now be fully loaded.
93                if globals.is_loaded(ns_name) {
94                    return Ok(false);
95                }
96                // Otherwise loop and try to claim again.
97            }
98        }
99    }
100}
101
102/// Evaluate the source file for `ns_name`, returning Ok(()) or an error.
103/// The caller is responsible for claiming/releasing the namespace in the
104/// `loading` map.
105fn do_load(globals: &Arc<GlobalEnv>, ns_name: &Arc<str>) -> EvalResult<()> {
106    // AOT-compiled namespace: a binary produced by `cljrs compile` registers a
107    // loader for each required namespace.  Run it instead of interpreting
108    // source — the loader evaluates a small interpreted preamble (ns/require,
109    // macros) and then calls the namespace's natively compiled initializer.
110    if let Some(loader) = globals.compiled_ns_loader(ns_name) {
111        // Ensure the namespace exists before its compiled `def`s run, then
112        // pre-refer clojure.core so the preamble can use core fns before its
113        // own `(ns ...)` form (mirrors the source-file path below).
114        globals.get_or_create_ns(ns_name);
115        if ns_name.as_ref() != "clojure.core" {
116            globals.refer_all(ns_name, "clojure.core");
117        }
118        // Save and restore *ns* so the caller's namespace is not disturbed by
119        // the `(ns ...)` form in the loaded namespace's preamble.
120        let saved_ns = globals
121            .lookup_var("clojure.core", "*ns*")
122            .and_then(|v| crate::env::dynamics::deref_var(&v));
123        let result = loader(globals);
124        if let Some(saved) = saved_ns
125            && let Some(var) = globals.lookup_var("clojure.core", "*ns*")
126        {
127            var.get().bind(saved);
128        }
129        return result;
130    }
131
132    // Resolve namespace name: check built-in registry first, then disk.
133    // Clojure convention: dots → path separators, hyphens → underscores.
134    let rel_path = ns_name.replace('.', "/").replace('-', "_");
135    let src_paths = globals.source_paths.read().unwrap().clone();
136    let (src, file_path): (String, String) = if let Some(builtin) = globals.builtin_source(ns_name)
137    {
138        (builtin.to_owned(), format!("<builtin:{ns_name}>"))
139    } else if let Some(found) = find_source_file(&rel_path, &src_paths) {
140        found
141    } else {
142        // No Clojure source on the path.  Before giving up, try loading the
143        // namespace from a native dependency declared in `cljrs.edn` with
144        // `:rust/load :dylib` (the hook is installed by the CLI).  A
145        // pure-native package has no `.cljrs`/`.cljc` file, so this is the
146        // only path that brings it in via a plain `require`.
147        if try_native_require(globals, ns_name)? {
148            return Ok(());
149        }
150        return Err(EvalError::Runtime(format!(
151            "Could not find namespace {ns_name} on source path"
152        )));
153    };
154
155    // Record source location on the namespace for versioned resolution.
156    // Only meaningful for real files (not builtins) and non-WASM targets.
157    #[cfg(not(target_arch = "wasm32"))]
158    if !file_path.starts_with("<builtin:") {
159        let repo_root = globals
160            .vcs()
161            .and_then(|vcs| vcs.find_repo_root(Path::new(&file_path)))
162            .map(|p| p.display().to_string());
163        let ns_ptr = globals.get_or_create_ns(ns_name);
164        ns_ptr
165            .get()
166            .set_source_location(&file_path, repo_root.as_deref());
167    }
168
169    // Pre-refer clojure.core so code in the file can use core fns before (ns ...).
170    if ns_name.as_ref() != "clojure.core" {
171        globals.refer_all(ns_name, "clojure.core");
172    }
173
174    // Evaluate the file in a new Env rooted at the namespace being loaded.
175    // Save and restore *ns* so the caller's namespace is not disturbed.
176    let saved_ns = globals
177        .lookup_var("clojure.core", "*ns*")
178        .and_then(|v| crate::env::dynamics::deref_var(&v));
179    {
180        let mut env = Env::new(globals.clone(), ns_name);
181        let mut parser = cljrs_reader::Parser::new(src, file_path);
182        let forms = parser.parse_all().map_err(EvalError::Read)?;
183        for form in forms {
184            // Alloc frame per top-level form: all allocations during this
185            // form's evaluation are rooted.  Frame pops between forms,
186            // allowing GC to collect temporaries from previous forms.
187            let _alloc_frame = cljrs_gc::push_alloc_frame();
188            (*globals)
189                .eval(&form, &mut env)
190                .map_err(|e| annotate(e, ns_name))?;
191        }
192    }
193    // Restore *ns* to the caller's namespace.
194    if let Some(saved) = saved_ns
195        && let Some(var) = globals.lookup_var("clojure.core", "*ns*")
196    {
197        var.get().bind(saved);
198    }
199
200    Ok(())
201}
202
203/// Consult the native-dependency `require` loader (installed by the CLI)
204/// for `ns_name`.  Returns `Ok(true)` when a `:rust/load :dylib` dep covering
205/// the namespace was built and its exports registered into the unversioned
206/// namespace; `Ok(false)` when no loader is installed or no dep covers the
207/// namespace; `Err` when a covering dep failed to build or load.
208fn try_native_require(globals: &Arc<GlobalEnv>, ns_name: &Arc<str>) -> EvalResult<bool> {
209    let loader = globals.native_require_loader.read().unwrap().clone();
210    match loader {
211        Some(loader) => loader(globals, ns_name),
212        None => Ok(false),
213    }
214}
215
216// ── Versioned namespace loading ───────────────────────────────────────────────
217
218/// Load `spec.ns` at `commit`, registering the result as the namespace
219/// `"<spec.ns>@<commit>"` in the global namespace table.
220///
221/// Idempotent: if the versioned namespace is already loaded, only applies the
222/// alias/refer from `spec` in `current_ns`.  The actual loading lives in
223/// `crate::env::versioned::ensure_versioned_ns_loaded`, shared with the per-symbol
224/// resolver used by all execution tiers.
225#[cfg(not(target_arch = "wasm32"))]
226pub fn load_versioned_ns(
227    globals: Arc<GlobalEnv>,
228    spec: &RequireSpec,
229    commit: &str,
230    current_ns: &str,
231) -> EvalResult<()> {
232    let versioned_ns_name =
233        crate::env::versioned::ensure_versioned_ns_loaded(&globals, &spec.ns, commit)?;
234    apply_alias_refer(&globals, &versioned_ns_name, current_ns, spec);
235    Ok(())
236}
237
238/// Apply the alias and refer clauses from `spec` into `current_ns`, using
239/// `effective_ns` as the source namespace (which may be `"base@commit"`).
240#[cfg(not(target_arch = "wasm32"))]
241fn apply_alias_refer(
242    globals: &GlobalEnv,
243    effective_ns: &Arc<str>,
244    current_ns: &str,
245    spec: &RequireSpec,
246) {
247    if let Some(alias) = &spec.alias {
248        globals.add_alias(current_ns, alias, effective_ns);
249    }
250    match &spec.refer {
251        RequireRefer::None => {}
252        RequireRefer::All => globals.refer_all(current_ns, effective_ns),
253        RequireRefer::Named(names) => globals.refer_named(current_ns, effective_ns, names),
254    }
255}
256
257pub(crate) fn find_source_file(
258    rel: &str,
259    src_paths: &[std::path::PathBuf],
260) -> Option<(String, String)> {
261    for dir in src_paths {
262        for ext in &[".cljrs", ".cljc"] {
263            let path = dir.join(format!("{rel}{ext}"));
264            if path.exists() {
265                let src = std::fs::read_to_string(&path).ok()?;
266                return Some((src, path.display().to_string()));
267            }
268        }
269    }
270    None
271}
272
273/// Wrap an EvalError with namespace context.  Read errors (which carry
274/// file/line/col in CljxError) are passed through unchanged so the CLI can
275/// render them with full location information.
276pub(crate) fn annotate(e: EvalError, ns_name: &Arc<str>) -> EvalError {
277    match e {
278        // Preserve read errors — they carry source location.
279        EvalError::Read(_) => e,
280        // Propagate recur unchanged (internal signal).
281        EvalError::Recur(_) => e,
282        // Annotate everything else with the namespace being loaded.
283        other => EvalError::Runtime(format!("in {ns_name}: {other}")),
284    }
285}