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 = cljrs_project::vcs::find_repo_root(Path::new(&file_path))
160 .map(|p| p.display().to_string());
161 let ns_ptr = globals.get_or_create_ns(ns_name);
162 ns_ptr
163 .get()
164 .set_source_location(&file_path, repo_root.as_deref());
165 }
166
167 // Pre-refer clojure.core so code in the file can use core fns before (ns ...).
168 if ns_name.as_ref() != "clojure.core" {
169 globals.refer_all(ns_name, "clojure.core");
170 }
171
172 // Evaluate the file in a new Env rooted at the namespace being loaded.
173 // Save and restore *ns* so the caller's namespace is not disturbed.
174 let saved_ns = globals
175 .lookup_var("clojure.core", "*ns*")
176 .and_then(|v| crate::env::dynamics::deref_var(&v));
177 {
178 let mut env = Env::new(globals.clone(), ns_name);
179 let mut parser = cljrs_reader::Parser::new(src, file_path);
180 let forms = parser.parse_all().map_err(EvalError::Read)?;
181 for form in forms {
182 // Alloc frame per top-level form: all allocations during this
183 // form's evaluation are rooted. Frame pops between forms,
184 // allowing GC to collect temporaries from previous forms.
185 let _alloc_frame = cljrs_gc::push_alloc_frame();
186 (*globals)
187 .eval(&form, &mut env)
188 .map_err(|e| annotate(e, ns_name))?;
189 }
190 }
191 // Restore *ns* to the caller's namespace.
192 if let Some(saved) = saved_ns
193 && let Some(var) = globals.lookup_var("clojure.core", "*ns*")
194 {
195 var.get().bind(saved);
196 }
197
198 Ok(())
199}
200
201/// Consult the native-dependency `require` loader (installed by the CLI)
202/// for `ns_name`. Returns `Ok(true)` when a `:rust/load :dylib` dep covering
203/// the namespace was built and its exports registered into the unversioned
204/// namespace; `Ok(false)` when no loader is installed or no dep covers the
205/// namespace; `Err` when a covering dep failed to build or load.
206fn try_native_require(globals: &Arc<GlobalEnv>, ns_name: &Arc<str>) -> EvalResult<bool> {
207 let loader = globals.native_require_loader.read().unwrap().clone();
208 match loader {
209 Some(loader) => loader(globals, ns_name),
210 None => Ok(false),
211 }
212}
213
214// ── Versioned namespace loading ───────────────────────────────────────────────
215
216/// Load `spec.ns` at `commit`, registering the result as the namespace
217/// `"<spec.ns>@<commit>"` in the global namespace table.
218///
219/// Idempotent: if the versioned namespace is already loaded, only applies the
220/// alias/refer from `spec` in `current_ns`. The actual loading lives in
221/// `crate::env::versioned::ensure_versioned_ns_loaded`, shared with the per-symbol
222/// resolver used by all execution tiers.
223#[cfg(not(target_arch = "wasm32"))]
224pub fn load_versioned_ns(
225 globals: Arc<GlobalEnv>,
226 spec: &RequireSpec,
227 commit: &str,
228 current_ns: &str,
229) -> EvalResult<()> {
230 let versioned_ns_name =
231 crate::env::versioned::ensure_versioned_ns_loaded(&globals, &spec.ns, commit)?;
232 apply_alias_refer(&globals, &versioned_ns_name, current_ns, spec);
233 Ok(())
234}
235
236/// Apply the alias and refer clauses from `spec` into `current_ns`, using
237/// `effective_ns` as the source namespace (which may be `"base@commit"`).
238#[cfg(not(target_arch = "wasm32"))]
239fn apply_alias_refer(
240 globals: &GlobalEnv,
241 effective_ns: &Arc<str>,
242 current_ns: &str,
243 spec: &RequireSpec,
244) {
245 if let Some(alias) = &spec.alias {
246 globals.add_alias(current_ns, alias, effective_ns);
247 }
248 match &spec.refer {
249 RequireRefer::None => {}
250 RequireRefer::All => globals.refer_all(current_ns, effective_ns),
251 RequireRefer::Named(names) => globals.refer_named(current_ns, effective_ns, names),
252 }
253}
254
255pub(crate) fn find_source_file(
256 rel: &str,
257 src_paths: &[std::path::PathBuf],
258) -> Option<(String, String)> {
259 for dir in src_paths {
260 for ext in &[".cljrs", ".cljc"] {
261 let path = dir.join(format!("{rel}{ext}"));
262 if path.exists() {
263 let src = std::fs::read_to_string(&path).ok()?;
264 return Some((src, path.display().to_string()));
265 }
266 }
267 }
268 None
269}
270
271/// Wrap an EvalError with namespace context. Read errors (which carry
272/// file/line/col in CljxError) are passed through unchanged so the CLI can
273/// render them with full location information.
274pub(crate) fn annotate(e: EvalError, ns_name: &Arc<str>) -> EvalError {
275 match e {
276 // Preserve read errors — they carry source location.
277 EvalError::Read(_) => e,
278 // Propagate recur unchanged (internal signal).
279 EvalError::Recur(_) => e,
280 // Annotate everything else with the namespace being loaded.
281 other => EvalError::Runtime(format!("in {ns_name}: {other}")),
282 }
283}