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}