cljrs_runtime/env/env.rs
1//! Lexical environment: local frames, global namespace table, and current Env.
2
3use std::collections::{HashMap, HashSet};
4use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
5use std::sync::{Arc, Condvar, Mutex, RwLock};
6
7use crate::env::async_hook::AsyncRuntime;
8
9use crate::env::error::EvalResult;
10use crate::mode::{ExecutionMode, TierState};
11use cljrs_gc::{GcConfig, GcPtr};
12use cljrs_reader::Form;
13use cljrs_value::{CljxFn, Namespace, ReferClojureFilter, Value, Var};
14// ── RequireSpec / RequireRefer ─────────────────────────────────────────────────
15
16/// How symbols should be referred into the requiring namespace.
17#[derive(Debug, Clone)]
18pub enum RequireRefer {
19 None,
20 All,
21 Named(Vec<Arc<str>>),
22}
23
24/// A parsed `require` specification.
25#[derive(Debug, Clone)]
26pub struct RequireSpec {
27 pub ns: Arc<str>,
28 /// Present when the namespace symbol carried a `@<hash>` version suffix.
29 pub version: Option<Arc<str>>,
30 pub alias: Option<Arc<str>>,
31 pub refer: RequireRefer,
32}
33
34// ── Frame ─────────────────────────────────────────────────────────────────────
35
36/// One stack frame of local bindings (a single `let*`, `fn`, or `loop*` scope).
37pub struct Frame {
38 pub bindings: Vec<(Arc<str>, Value)>,
39}
40
41impl Default for Frame {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl Frame {
48 pub fn new() -> Self {
49 Self {
50 bindings: Vec::new(),
51 }
52 }
53
54 pub fn bind(&mut self, name: Arc<str>, val: Value) {
55 // Shadow: push new binding; lookup searches from the end.
56 self.bindings.push((name, val));
57 }
58
59 pub fn lookup(&self, name: &str) -> Option<&Value> {
60 // Search in reverse order so later bindings shadow earlier ones.
61 tracing::trace!(target: "env", "lookup {}", name);
62 for (n, v) in self.bindings.iter().rev() {
63 if n.as_ref() == name {
64 return Some(v);
65 }
66 }
67 None
68 }
69}
70
71// ── GlobalEnv ─────────────────────────────────────────────────────────────────
72
73/// The global mutable store of all namespaces.
74pub struct GlobalEnv {
75 /// Process-unique identity of this runtime instance.
76 ///
77 /// Allocated from a counter, not derived from the `Arc`'s address: an
78 /// address is only unique while the allocation is live, so a dropped
79 /// runtime could hand its key to the next one and let it inherit stale
80 /// cross-defn IR. Used to scope per-instance registries.
81 id: u64,
82 pub namespaces: RwLock<HashMap<Arc<str>, GcPtr<Namespace>>>,
83 /// Directories to search when resolving namespace names to files.
84 pub source_paths: RwLock<Vec<std::path::PathBuf>>,
85 /// Namespaces that have been fully loaded from a file (idempotent guard).
86 pub loaded: Mutex<std::collections::HashSet<Arc<str>>>,
87 /// Namespaces currently being loaded, mapped to the thread loading them.
88 /// Used to detect true circular requires (same thread) vs concurrent loads
89 /// (different thread — those wait on `loading_done` instead of erroring).
90 pub loading: Mutex<HashMap<Arc<str>, std::thread::ThreadId>>,
91 /// Signalled whenever a namespace finishes loading (or fails).
92 pub loading_done: Condvar,
93 /// Built-in namespace sources embedded in the binary.
94 /// Checked by `load_ns` before falling back to source-path search.
95 pub builtin_sources: RwLock<HashMap<Arc<str>, &'static str>>,
96 /// GC configuration for automatic collection based on memory pressure.
97 pub gc_config: RwLock<Option<Arc<GcConfig>>>,
98 /// How this runtime executes function calls. Fixed when the runtime is
99 /// built; see [`crate::RuntimeBuilder::execution_mode`].
100 execution_mode: ExecutionMode,
101 /// Which tiers are live right now (see [`TierState`]). Starts at
102 /// [`TierState::TreeWalk`] — nothing can be lowered until `clojure.core`
103 /// exists — and is raised once to `execution_mode.target_tier()` when the
104 /// builder finishes bootstrapping.
105 tier_state: AtomicU8,
106 /// This runtime's Tier-1 and Tier-2 state: the lowered-IR cache, the JIT
107 /// counters and native-code tables, and the JIT backend attached to this
108 /// runtime. Instance state: two runtimes in one process never read,
109 /// evict, or invalidate each other's entries, and everything dies with
110 /// the runtime.
111 tiers: Arc<crate::tiered::tiers::Tiers>,
112 /// Optional async runtime registered by `cljrs-async`.
113 /// `None` when the library is not linked; `Some` after `cljrs_async::init`.
114 pub async_rt: RwLock<Option<Arc<dyn AsyncRuntime>>>,
115 /// Cache of values resolved at a specific commit.
116 /// Key format: `"<ns>/<name>@<commit>"` for individual vars,
117 /// or `"<ns>@<commit>"` for whole versioned namespaces.
118 pub version_cache: Mutex<HashMap<Arc<str>, Value>>,
119 /// Parsed `cljrs.edn` config, loaded once at startup.
120 pub deps_config: RwLock<Option<Arc<cljrs_project::config::DepsConfig>>>,
121 /// When true, every versioned-symbol or versioned-namespace resolution must
122 /// carry a valid commit signature (verified natively against `trusted_keys`)
123 /// before the historical code is executed. Off by default; enabled via
124 /// `--verify-commit-signatures` CLI flag or `:verify-commit-signatures true`
125 /// in `cljrs.edn`.
126 pub verify_commit_signatures: AtomicBool,
127 /// The git backend used by versioned resolution and signature checking, or
128 /// `None` in builds that carry no VCS implementation (wasm, or
129 /// `cljrs-runtime` without its default `deps` feature). With no provider,
130 /// source files are treated as living outside any repository and versioned
131 /// resolution can only use embedded (AOT) sources. See [`crate::env::vcs`].
132 vcs: RwLock<Option<Arc<dyn crate::env::vcs::VcsProvider>>>,
133 /// Session-scoped cache of commits that have already passed signature
134 /// verification this run, keyed by `(repo_root, commit_hash)`.
135 pub sig_verify_cache: Mutex<HashSet<(Arc<str>, Arc<str>)>>,
136 /// Pinned source texts fetched from git this session, keyed by
137 /// `"<ns>@<commit>"`. The AOT compiler embeds these in the produced
138 /// binary so versioned namespaces resolve without git at runtime.
139 pub versioned_sources: RwLock<HashMap<Arc<str>, Arc<str>>>,
140 /// When true (set by AOT harness main), versioned namespaces resolve
141 /// only from embedded builtin sources — never from git. A versioned
142 /// namespace that was not embedded at compile time fails with a clear
143 /// error instead of attempting a fetch.
144 pub versioned_offline: AtomicBool,
145 /// Provenance of native (Rust-backed) packages recorded at registration:
146 /// namespace → the git commit the package was built from. Consulted by
147 /// the versioned resolver's native HEAD fallback to detect pinned-commit
148 /// mismatches.
149 pub native_provenance: RwLock<HashMap<Arc<str>, Arc<str>>>,
150 /// When true, a pinned lookup of a native function whose recorded
151 /// provenance does not match the requested commit is an error instead of
152 /// a once-per-pin warning. CLI: `--enforce-native-versions`; cljrs.edn:
153 /// `:enforce-native-versions true`.
154 pub enforce_native_versions: AtomicBool,
155 /// Pinned-native mismatches already warned about this session
156 /// (key: `"<ns>@<commit>"`), so each pin warns at most once.
157 pub provenance_warned: Mutex<HashSet<Arc<str>>>,
158 /// Optional loader for **pinned native packages** (`:rust/load :dylib`),
159 /// installed by the CLI. Called by the versioned resolver with
160 /// `(globals, base_ns, commit)` before falling back to the HEAD native
161 /// binding; returns `Ok(true)` when it registered the package's pinned
162 /// implementations into the `"<base_ns>@<commit>"` namespace.
163 #[allow(clippy::type_complexity)]
164 pub pinned_native_loader: RwLock<Option<PinnedNativeLoader>>,
165 /// Optional loader for **native dependencies on the plain `require` path**
166 /// (`:rust/load :dylib`), installed by the CLI. Called by the
167 /// unversioned namespace loader with `(globals, ns)` when a `require`d
168 /// namespace has no Clojure source on the source path; returns `Ok(true)`
169 /// when it built the dep's crate at the pinned `:git/sha` and registered
170 /// the package's exports into the **unversioned** namespace, so a plain
171 /// `(require '[my.native.lib :as lib])` brings the native code in.
172 #[allow(clippy::type_complexity)]
173 pub native_require_loader: RwLock<Option<NativeRequireLoader>>,
174 /// Loaders for **AOT-compiled namespaces**, installed by the binary
175 /// produced by `cljrs compile`. Keyed by namespace name. When a plain
176 /// `require` resolves a namespace that has a registered loader, `load_ns`
177 /// invokes the loader instead of interpreting Clojure source: the loader
178 /// evaluates the namespace's small interpreted preamble (its `ns`/`require`
179 /// and macro definitions) and then calls the namespace's natively compiled
180 /// initializer, so the bulk of the namespace runs as machine code rather
181 /// than being tree-walked at startup.
182 #[allow(clippy::type_complexity)]
183 pub compiled_ns_loaders: RwLock<HashMap<Arc<str>, CompiledNsLoader>>,
184}
185
186/// Loader callback for an AOT-compiled namespace (see
187/// `GlobalEnv::compiled_ns_loaders`). Given the global env, it loads the
188/// namespace by running its interpreted preamble and its compiled initializer.
189pub type CompiledNsLoader = Arc<dyn Fn(&Arc<GlobalEnv>) -> EvalResult<()> + Send + Sync>;
190
191/// Loader callback for pinned native packages (see
192/// `GlobalEnv::pinned_native_loader`).
193pub type PinnedNativeLoader =
194 Arc<dyn Fn(&Arc<GlobalEnv>, &str, &str) -> EvalResult<bool> + Send + Sync>;
195
196/// Loader callback for native dependencies reached through a plain `require`
197/// (see `GlobalEnv::native_require_loader`).
198pub type NativeRequireLoader = Arc<dyn Fn(&Arc<GlobalEnv>, &str) -> EvalResult<bool> + Send + Sync>;
199
200/// Source of [`GlobalEnv::id`] values.
201static NEXT_GLOBAL_ENV_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
202
203impl std::fmt::Debug for GlobalEnv {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 write!(f, "GlobalEnv {{ ... }}")
206 }
207}
208
209impl GlobalEnv {
210 /// Create an empty global environment for the given execution mode.
211 ///
212 /// This is the *raw* constructor: no builtins, no bootstrap, no source
213 /// paths. Use [`crate::Runtime::builder`] unless you are the builder.
214 pub fn new(execution_mode: ExecutionMode) -> Arc<Self> {
215 let id = NEXT_GLOBAL_ENV_ID.fetch_add(1, Ordering::Relaxed);
216 Arc::new(Self {
217 id,
218 namespaces: RwLock::new(HashMap::new()),
219 source_paths: RwLock::new(Vec::new()),
220 loaded: Mutex::new(std::collections::HashSet::new()),
221 loading: Mutex::new(HashMap::new()),
222 loading_done: Condvar::new(),
223 builtin_sources: RwLock::new(HashMap::new()),
224 gc_config: RwLock::new(None),
225 execution_mode,
226 tier_state: AtomicU8::new(TierState::TreeWalk as u8),
227 tiers: crate::tiered::tiers::Tiers::new(id),
228 async_rt: RwLock::new(None),
229 version_cache: Mutex::new(HashMap::new()),
230 deps_config: RwLock::new(None),
231 verify_commit_signatures: AtomicBool::new(false),
232 vcs: RwLock::new(crate::env::vcs::default_provider()),
233 sig_verify_cache: Mutex::new(HashSet::new()),
234 versioned_sources: RwLock::new(HashMap::new()),
235 versioned_offline: AtomicBool::new(false),
236 native_provenance: RwLock::new(HashMap::new()),
237 enforce_native_versions: AtomicBool::new(false),
238 provenance_warned: Mutex::new(HashSet::new()),
239 pinned_native_loader: RwLock::new(None),
240 native_require_loader: RwLock::new(None),
241 compiled_ns_loaders: RwLock::new(HashMap::new()),
242 })
243 }
244
245 /// Replace the source path list.
246 pub fn set_source_paths(&self, paths: Vec<std::path::PathBuf>) {
247 *self.source_paths.write().unwrap() = paths;
248 }
249
250 /// Register an embedded namespace source (called by cljrs-stdlib at startup).
251 pub fn register_builtin_source(&self, ns: &str, src: &'static str) {
252 self.builtin_sources
253 .write()
254 .unwrap()
255 .insert(Arc::from(ns), src);
256 }
257
258 /// Look up an embedded source for a namespace, if one has been registered.
259 pub fn builtin_source(&self, ns: &str) -> Option<&'static str> {
260 self.builtin_sources.read().unwrap().get(ns).copied()
261 }
262
263 /// Register a loader for an AOT-compiled namespace (called by the harness
264 /// `main` of a binary produced by `cljrs compile`).
265 pub fn register_compiled_ns_loader(&self, ns: &str, loader: CompiledNsLoader) {
266 self.compiled_ns_loaders
267 .write()
268 .unwrap()
269 .insert(Arc::from(ns), loader);
270 }
271
272 /// Look up the loader for an AOT-compiled namespace, if one is registered.
273 pub fn compiled_ns_loader(&self, ns: &str) -> Option<CompiledNsLoader> {
274 self.compiled_ns_loaders.read().unwrap().get(ns).cloned()
275 }
276
277 /// Mark a namespace as fully loaded from a file.
278 pub fn mark_loaded(&self, ns: &str) {
279 self.loaded.lock().unwrap().insert(Arc::from(ns));
280 }
281
282 /// True if the namespace has already been loaded from a file.
283 pub fn is_loaded(&self, ns: &str) -> bool {
284 self.loaded.lock().unwrap().contains(ns)
285 }
286
287 /// Set the GC configuration for automatic memory pressure management.
288 pub fn set_gc_config(&self, config: Arc<GcConfig>) {
289 *self.gc_config.write().unwrap() = Some(config);
290 }
291
292 /// Get the GC configuration, if one has been set.
293 pub fn gc_config(&self) -> Option<Arc<GcConfig>> {
294 self.gc_config.read().unwrap().clone()
295 }
296
297 /// Resolve a short alias to a full namespace name in `current_ns`.
298 pub fn resolve_alias(&self, current_ns: &str, alias: &str) -> Option<Arc<str>> {
299 let map = self.namespaces.read().unwrap();
300 let ns = map.get(current_ns)?;
301 let aliases = ns.get().aliases.lock().unwrap();
302 aliases.get(alias).cloned()
303 }
304
305 /// Resolve an auto-resolved keyword name (the text after `::`) to its
306 /// fully-qualified `ns/name` form.
307 ///
308 /// `::kw` qualifies with `current_ns` directly; `::alias/kw` looks
309 /// `alias` up in `current_ns`'s alias table (populated by `(require
310 /// '[... :as alias])`) and qualifies with the resolved namespace.
311 pub fn resolve_auto_keyword(&self, current_ns: &str, name: &str) -> Result<String, String> {
312 match name.split_once('/') {
313 Some((alias, kw_name)) => match self.resolve_alias(current_ns, alias) {
314 Some(ns) => Ok(format!("{ns}/{kw_name}")),
315 None => Err(format!(
316 "invalid token: ::{name} (no such namespace alias: {alias})"
317 )),
318 },
319 None => Ok(format!("{current_ns}/{name}")),
320 }
321 }
322
323 /// Return the namespace with this name, creating it if it doesn't exist.
324 pub fn get_or_create_ns(&self, name: &str) -> GcPtr<Namespace> {
325 // Fast path: already exists.
326 {
327 let map = self.namespaces.read().unwrap();
328 if let Some(ns) = map.get(name) {
329 return ns.clone();
330 }
331 }
332 // Slow path: insert.
333 let mut map = self.namespaces.write().unwrap();
334 // Re-check after acquiring write lock.
335 if let Some(ns) = map.get(name) {
336 return ns.clone();
337 }
338 let ns = GcPtr::new(Namespace::new(name));
339 map.insert(Arc::from(name), ns.clone());
340 ns
341 }
342
343 /// Intern `name` with `val` in the given namespace, returning the Var.
344 pub fn intern(&self, ns_name: &str, name: Arc<str>, val: Value) -> GcPtr<Var> {
345 let ns = self.get_or_create_ns(ns_name);
346 let mut interns = ns.get().interns.lock().unwrap();
347 if let Some(var) = interns.get(&name) {
348 // Update existing var.
349 var.get().bind(val);
350 return var.clone();
351 }
352 let var = GcPtr::new(Var::new(ns_name, name.as_ref()));
353 var.get().bind(val);
354 interns.insert(name, var.clone());
355 var
356 }
357
358 /// Look up a Var in the named namespace (interns only).
359 pub fn lookup_var(&self, ns_name: &str, sym_name: &str) -> Option<GcPtr<Var>> {
360 let map = self.namespaces.read().unwrap();
361 let ns = map.get(ns_name)?;
362 let interns = ns.get().interns.lock().unwrap();
363 interns.get(sym_name).cloned()
364 }
365
366 /// Look up a value in `ns_name`: checks interns then refers.
367 /// Routes through the dynamic binding stack so `binding` overrides work.
368 pub fn lookup_in_ns(&self, ns_name: &str, sym_name: &str) -> Option<Value> {
369 let map = self.namespaces.read().unwrap();
370 let ns = map.get(ns_name)?;
371 let ns_ref = ns.get();
372 // Check interns first.
373 {
374 let interns = ns_ref.interns.lock().unwrap();
375 if let Some(var) = interns.get(sym_name) {
376 return crate::env::dynamics::deref_var(var);
377 }
378 }
379 // Then refers.
380 {
381 let refers = ns_ref.refers.lock().unwrap();
382 if let Some(var) = refers.get(sym_name) {
383 return crate::env::dynamics::deref_var(var);
384 }
385 }
386 None
387 }
388
389 /// Look up the raw Var (not its value) in `ns_name`: interns then refers.
390 pub fn lookup_var_in_ns(&self, ns_name: &str, sym_name: &str) -> Option<GcPtr<Var>> {
391 let map = self.namespaces.read().unwrap();
392 let ns = map.get(ns_name)?;
393 let ns_ref = ns.get();
394 {
395 let interns = ns_ref.interns.lock().unwrap();
396 if let Some(var) = interns.get(sym_name) {
397 return Some(var.clone());
398 }
399 }
400 {
401 let refers = ns_ref.refers.lock().unwrap();
402 if let Some(var) = refers.get(sym_name) {
403 return Some(var.clone());
404 }
405 }
406 None
407 }
408
409 /// Copy all interns from `src_ns` into `dst_ns` as refers.
410 ///
411 /// This is the *explicit* refer — `(:require [x :refer :all])` — so it is
412 /// never narrowed by `dst_ns`'s `(:refer-clojure ...)` filter, matching
413 /// `clojure.core/refer`: naming a namespace explicitly re-maps even names
414 /// an earlier `refer-clojure` left out. The automatic core refer every
415 /// namespace starts with goes through [`GlobalEnv::refer_core`] instead.
416 pub fn refer_all(&self, dst_ns: &str, src_ns: &str) {
417 let map = self.namespaces.read().unwrap();
418 let src = match map.get(src_ns) {
419 Some(ns) => ns.clone(),
420 None => return,
421 };
422 let dst = match map.get(dst_ns) {
423 Some(ns) => ns.clone(),
424 None => return,
425 };
426 let src_interns = src.get().interns.lock().unwrap();
427 let mut dst_refers = dst.get().refers.lock().unwrap();
428 for (name, var) in src_interns.iter() {
429 dst_refers.insert(name.clone(), var.clone());
430 }
431 }
432
433 /// Apply the automatic `clojure.core` refer that every namespace starts
434 /// with, narrowed by `dst_ns`'s `(:refer-clojure ...)` filter.
435 pub fn refer_core(&self, dst_ns: &str) {
436 self.refer_core_impl(dst_ns, false);
437 }
438
439 /// `replace`: drop the refers `dst_ns` already inherited from
440 /// `clojure.core` before re-referring, under the same lock — so installing
441 /// a filter after the namespace was pre-referred neither leaves stale
442 /// names behind nor exposes a window where core is only half-referred.
443 fn refer_core_impl(&self, dst_ns: &str, replace: bool) {
444 let map = self.namespaces.read().unwrap();
445 let src = match map.get("clojure.core") {
446 Some(ns) => ns.clone(),
447 None => return,
448 };
449 let dst = match map.get(dst_ns) {
450 Some(ns) => ns.clone(),
451 None => return,
452 };
453 // Lock order is filter → src interns → dst refers throughout.
454 let filter = dst.get().refer_clojure_filter.lock().unwrap();
455 let src_interns = src.get().interns.lock().unwrap();
456 let mut dst_refers = dst.get().refers.lock().unwrap();
457 if replace {
458 dst_refers.retain(|_, var| var.get().namespace.as_ref() != "clojure.core");
459 }
460 match filter.as_ref() {
461 Some(f) => {
462 for (name, var) in src_interns.iter() {
463 if let Some(local) = f.local_name(name) {
464 dst_refers.insert(local, var.clone());
465 }
466 }
467 }
468 None => {
469 for (name, var) in src_interns.iter() {
470 dst_refers.insert(name.clone(), var.clone());
471 }
472 }
473 }
474 }
475
476 /// Install `dst_ns`'s `(:refer-clojure ...)` filter (`None` removes any
477 /// previous one) and re-apply the automatic `clojure.core` refer under it.
478 ///
479 /// Refers already inherited from `clojure.core` are dropped first, so a
480 /// filter set after the namespace was pre-referred — the loader refers core
481 /// before it reads the file, and `ns` itself refers core before it reaches
482 /// the clause — still takes effect.
483 pub fn set_refer_clojure_filter(
484 &self,
485 dst_ns: &str,
486 filter: Option<ReferClojureFilter>,
487 ) -> Result<(), String> {
488 if let Some(f) = &filter {
489 self.validate_refer_clojure_filter(f)?;
490 }
491 let dst = self.get_or_create_ns(dst_ns);
492 {
493 let mut slot = dst.get().refer_clojure_filter.lock().unwrap();
494 // Nothing to install and nothing to undo: leave the refers alone.
495 if slot.is_none() && filter.is_none() {
496 return Ok(());
497 }
498 *slot = filter;
499 }
500 self.refer_core_impl(dst_ns, true);
501 Ok(())
502 }
503
504 /// Check a `(:refer-clojure ...)` filter against the names `clojure.core`
505 /// actually publishes, so a typo fails at the `ns` form rather than as an
506 /// unbound symbol somewhere further down the file.
507 ///
508 /// `:only` and `:rename` name specific vars and must resolve; `:exclude` is
509 /// subtractive and stays permissive (excluding a name core does not have is
510 /// harmless, and lets a file stay portable across core versions). Clojure
511 /// validates `:only` the same way but ignores an unresolvable `:rename`
512 /// key, since it only consults the rename map for names already in its
513 /// to-do list.
514 ///
515 /// Two names landing on the same local name is an error rather than a coin
516 /// flip: with `:rename {inc str}` both `inc` and core's own `str` want the
517 /// name `str`. Clojure warns and lets whichever one its intern table
518 /// yields last win; picking a winner by hash order here would make the
519 /// choice unstable from run to run.
520 fn validate_refer_clojure_filter(&self, filter: &ReferClojureFilter) -> Result<(), String> {
521 let map = self.namespaces.read().unwrap();
522 let Some(core) = map.get("clojure.core").cloned() else {
523 return Ok(());
524 };
525 drop(map);
526 let interns = core.get().interns.lock().unwrap();
527 // A runtime built without the core bootstrap has nothing to check
528 // against; do not fail every name.
529 if interns.is_empty() {
530 return Ok(());
531 }
532
533 for (opt, names) in [
534 ("only", filter.only.iter().flatten().collect::<Vec<_>>()),
535 ("rename", filter.rename.keys().collect::<Vec<_>>()),
536 ] {
537 let mut unknown: Vec<&str> = names
538 .into_iter()
539 .filter(|n| !interns.contains_key(*n))
540 .map(|n| n.as_ref())
541 .collect();
542 if !unknown.is_empty() {
543 unknown.sort_unstable();
544 return Err(format!(
545 ":refer-clojure :{opt} names {}, which clojure.core does not define",
546 unknown.join(", ")
547 ));
548 }
549 }
550
551 // local name → the core name referred under it.
552 let mut taken: HashMap<Arc<str>, Arc<str>> = HashMap::new();
553 let mut conflicts: Vec<(Arc<str>, Arc<str>, Arc<str>)> = Vec::new();
554 for name in interns.keys() {
555 let Some(local) = filter.local_name(name) else {
556 continue;
557 };
558 if let Some(prev) = taken.insert(local.clone(), name.clone()) {
559 let (a, b) = if prev.as_ref() <= name.as_ref() {
560 (prev, name.clone())
561 } else {
562 (name.clone(), prev)
563 };
564 conflicts.push((local, a, b));
565 }
566 }
567 if !conflicts.is_empty() {
568 conflicts.sort_unstable();
569 let (local, a, b) = &conflicts[0];
570 return Err(format!(
571 ":refer-clojure would refer both {a} and {b} as {local}; \
572 rename or exclude one of them"
573 ));
574 }
575 Ok(())
576 }
577
578 /// Copy selected interns from `src_ns` into `dst_ns` as refers.
579 pub fn refer_named(&self, dst_ns: &str, src_ns: &str, names: &[Arc<str>]) {
580 let map = self.namespaces.read().unwrap();
581 let src = match map.get(src_ns) {
582 Some(ns) => ns.clone(),
583 None => return,
584 };
585 let dst = match map.get(dst_ns) {
586 Some(ns) => ns.clone(),
587 None => return,
588 };
589 let src_interns = src.get().interns.lock().unwrap();
590 let mut dst_refers = dst.get().refers.lock().unwrap();
591 for name in names {
592 if let Some(var) = src_interns.get(name) {
593 // Use insert (not or_insert_with) so that an explicit
594 // `require :refer [name]` always overrides a previous refer
595 // (e.g. one inherited from clojure.core via refer-all).
596 // clojure.core.async's `into` intentionally shadows clojure.core/into;
597 // or_insert_with would silently drop the override.
598 dst_refers.insert(name.clone(), var.clone());
599 }
600 }
601 }
602
603 /// Register `alias` → `full_ns` in `current_ns`'s alias table.
604 pub fn add_alias(&self, current_ns: &str, alias: &str, full_ns: &str) {
605 let ns_ptr = self.get_or_create_ns(current_ns);
606 let mut aliases = ns_ptr.get().aliases.lock().unwrap();
607 aliases.insert(Arc::from(alias), Arc::from(full_ns));
608 }
609
610 /// Process-unique identity of this runtime instance.
611 #[inline(always)]
612 pub fn id(&self) -> u64 {
613 self.id
614 }
615
616 /// This runtime's Tier-1/Tier-2 state.
617 #[inline(always)]
618 pub fn tiers(&self) -> &Arc<crate::tiered::tiers::Tiers> {
619 &self.tiers
620 }
621
622 /// This runtime's cache of lowered IR.
623 #[inline(always)]
624 pub fn ir_cache(&self) -> &crate::tiered::ir_cache::IrCache {
625 self.tiers.ir_cache()
626 }
627
628 /// This runtime's JIT counters, profiles, and native-code tables.
629 #[inline(always)]
630 pub fn jit(&self) -> &crate::tiered::jit_state::JitState {
631 self.tiers.jit()
632 }
633
634 /// The JIT compiler attached to this runtime, if any.
635 ///
636 /// `None` when no JIT is linked or installed; callers then keep to the
637 /// interpreter tiers. Installed by `cljrs_compiler::jit::install`.
638 #[inline(always)]
639 pub fn jit_backend(&self) -> Option<Arc<dyn crate::tiered::backend::JitBackend>> {
640 self.tiers.jit().backend().cloned()
641 }
642
643 // ── Execution mode and tier state ────────────────────────────────────
644
645 /// How this runtime executes function calls.
646 #[inline(always)]
647 pub fn execution_mode(&self) -> ExecutionMode {
648 self.execution_mode
649 }
650
651 /// Which tiers are live right now.
652 #[inline(always)]
653 pub fn tier_state(&self) -> TierState {
654 TierState::from_u8(self.tier_state.load(Ordering::Acquire))
655 }
656
657 /// Raise the live tier state. Called once by the runtime builder after
658 /// the bootstrap completes; lowering the tier is not supported, so a
659 /// request below the current state is ignored.
660 pub fn set_tier_state(&self, tier: TierState) {
661 let _ = self.tier_state.fetch_max(tier as u8, Ordering::AcqRel);
662 }
663
664 /// True when IR may be lowered, cached, and interpreted. This is the
665 /// gate the old `compiler_ready` flag served.
666 #[inline(always)]
667 pub fn ir_enabled(&self) -> bool {
668 self.tier_state().ir_enabled()
669 }
670
671 // ── Evaluation entry points ──────────────────────────────────────────
672
673 /// Evaluate `form` in `env`.
674 #[inline(always)]
675 pub fn eval(&self, form: &Form, env: &mut Env) -> EvalResult {
676 crate::interp::eval::eval(form, env)
677 }
678
679 /// Call a Clojure function, taking the path this runtime's
680 /// [`ExecutionMode`] selects.
681 ///
682 /// This is the single function-call dispatch point: tree walk, tier-1 IR,
683 /// and JIT-native execution are all reached from here.
684 #[inline(always)]
685 pub fn call_cljrs_fn(&self, func: &CljxFn, args: &[Value], env: &mut Env) -> EvalResult {
686 match self.execution_mode {
687 ExecutionMode::TreeWalk => crate::interp::apply::call_cljrs_fn(func, args, env),
688 ExecutionMode::Tiered | ExecutionMode::TieredNoJit => {
689 crate::tiered::apply::call_cljrs_fn(func, args, env)
690 }
691 ExecutionMode::NoGcTransaction => crate::env::depth::call_cljrs_fn(func, args, env),
692 }
693 }
694
695 /// Notify the active tier that a new `fn*` was defined.
696 ///
697 /// In a tiered runtime with IR enabled this eagerly lowers the function
698 /// (when eager lowering is on); in every other mode it does nothing.
699 #[inline(always)]
700 pub fn on_fn_defined(&self, f: &CljxFn, env: &mut Env) {
701 if self.execution_mode.is_tiered() && self.ir_enabled() {
702 crate::tiered::ir_interp::eager_lower_fn(f, env);
703 }
704 }
705
706 /// Install an async runtime. Called once by `cljrs_async::init`.
707 /// Subsequent calls are silently ignored (first writer wins).
708 pub fn set_async_runtime(&self, rt: Arc<dyn AsyncRuntime>) {
709 let mut guard = self.async_rt.write().unwrap();
710 if guard.is_none() {
711 *guard = Some(rt);
712 }
713 }
714
715 /// Return the async runtime, if one has been registered.
716 pub fn async_runtime(&self) -> Option<Arc<dyn AsyncRuntime>> {
717 self.async_rt.read().unwrap().clone()
718 }
719
720 /// Return `(source_file, git_repo_root)` for the named namespace, if
721 /// both have been populated by the loader.
722 pub fn get_ns_git_context(&self, ns_name: &str) -> Option<(Arc<str>, Arc<str>)> {
723 let map = self.namespaces.read().unwrap();
724 let ns = map.get(ns_name)?;
725 let ns_ref = ns.get();
726 let file = ns_ref.source_file.lock().unwrap().clone()?;
727 let repo = ns_ref.git_repo_root.lock().unwrap().clone()?;
728 Some((file, repo))
729 }
730
731 /// Store a resolved versioned value in the cache.
732 /// Key: `"<ns>/<name>@<commit>"`.
733 pub fn cache_versioned(&self, ns: &str, name: &str, commit: &str, val: Value) {
734 let key: Arc<str> = Arc::from(format!("{ns}/{name}@{commit}"));
735 self.version_cache.lock().unwrap().insert(key, val);
736 }
737
738 /// Retrieve a previously resolved versioned value, if cached.
739 pub fn get_cached_versioned(&self, ns: &str, name: &str, commit: &str) -> Option<Value> {
740 let key = format!("{ns}/{name}@{commit}");
741 self.version_cache
742 .lock()
743 .unwrap()
744 .get(key.as_str())
745 .cloned()
746 }
747
748 /// Mark namespace `name@commit` as loaded in the standard loaded set.
749 pub fn cache_versioned_ns(&self, ns: &str, commit: &str) {
750 let key: Arc<str> = Arc::from(format!("{ns}@{commit}"));
751 self.version_cache.lock().unwrap().insert(key, Value::Nil);
752 }
753
754 /// Record the source text of a versioned namespace fetched from git.
755 /// Key: `"<ns>@<commit>"`. Consumed by the AOT compiler for embedding.
756 pub fn record_versioned_source(&self, versioned_ns: &str, src: &str) {
757 self.versioned_sources
758 .write()
759 .unwrap()
760 .insert(Arc::from(versioned_ns), Arc::from(src));
761 }
762
763 /// Snapshot of all versioned sources fetched this session, sorted by key.
764 pub fn versioned_sources_snapshot(&self) -> Vec<(Arc<str>, Arc<str>)> {
765 let map = self.versioned_sources.read().unwrap();
766 let mut entries: Vec<_> = map.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
767 entries.sort_by(|a, b| a.0.cmp(&b.0));
768 entries
769 }
770
771 /// Restrict versioned-namespace resolution to embedded builtin sources
772 /// (no git). Called by AOT harness binaries, which embed every pinned
773 /// source discovered at compile time.
774 pub fn set_versioned_offline(&self, offline: bool) {
775 self.versioned_offline.store(offline, Ordering::Relaxed);
776 }
777
778 /// True when versioned namespaces may only come from embedded sources.
779 pub fn versioned_offline(&self) -> bool {
780 self.versioned_offline.load(Ordering::Relaxed)
781 }
782
783 /// Record the git commit a native (Rust-backed) package was built from.
784 /// Called at registration time (`Registry::set_provenance` or the
785 /// `register_provenance!` inventory entry in cljrs-interop).
786 pub fn set_native_provenance(&self, ns: &str, commit: &str) {
787 self.native_provenance
788 .write()
789 .unwrap()
790 .insert(Arc::from(ns), Arc::from(commit));
791 }
792
793 /// The recorded provenance commit for a native package's namespace.
794 pub fn native_provenance_for(&self, ns: &str) -> Option<Arc<str>> {
795 self.native_provenance.read().unwrap().get(ns).cloned()
796 }
797
798 /// Make pinned-native provenance mismatches hard errors.
799 pub fn set_enforce_native_versions(&self, enforce: bool) {
800 self.enforce_native_versions
801 .store(enforce, Ordering::Relaxed);
802 }
803
804 /// True when pinned-native provenance mismatches are errors.
805 pub fn enforce_native_versions(&self) -> bool {
806 self.enforce_native_versions.load(Ordering::Relaxed)
807 }
808
809 /// Install the pinned-native package loader (called once by
810 /// `cljrs::native::pinned::install`; first writer wins).
811 pub fn set_pinned_native_loader(&self, loader: PinnedNativeLoader) {
812 let mut guard = self.pinned_native_loader.write().unwrap();
813 if guard.is_none() {
814 *guard = Some(loader);
815 }
816 }
817
818 /// Install the native-dependency `require` loader (called once by
819 /// `cljrs::native::pinned::install`; first writer wins).
820 pub fn set_native_require_loader(&self, loader: NativeRequireLoader) {
821 let mut guard = self.native_require_loader.write().unwrap();
822 if guard.is_none() {
823 *guard = Some(loader);
824 }
825 }
826
827 /// The installed VCS backend, or `None` when this build has none (see
828 /// [`crate::env::vcs`]). Callers must degrade gracefully: "no provider"
829 /// means "this source file is not in a git repository".
830 pub fn vcs(&self) -> Option<Arc<dyn crate::env::vcs::VcsProvider>> {
831 self.vcs.read().unwrap().clone()
832 }
833
834 /// Replace the VCS backend. Lets an embedder that built without the
835 /// `deps` feature supply its own git implementation, or a sandboxed host
836 /// remove the default one (`None`) so no versioned resolution can reach
837 /// the filesystem's git history.
838 ///
839 /// Drops every cached signature verdict: those were reached by the
840 /// outgoing provider, against its trust set and its view of the
841 /// repository, and say nothing about what the incoming one would decide
842 /// for the same `(repo, commit)`. Keeping them would let a permissive
843 /// provider launder an approval for source a later provider serves.
844 pub fn set_vcs_provider(&self, provider: Option<Arc<dyn crate::env::vcs::VcsProvider>>) {
845 // Neither this nor `check_commit_signature` ever holds the `vcs` and
846 // `sig_verify_cache` locks at the same time, and the two reach for
847 // them in opposite orders — keep it that way, or the pair becomes a
848 // lock-order inversion.
849 *self.vcs.write().unwrap() = provider;
850 self.invalidate_signature_cache();
851 }
852
853 /// Forget every cached signature verdict, so the next
854 /// [`check_commit_signature`](Self::check_commit_signature) re-asks the
855 /// current provider. Called whenever the thing that produced those
856 /// verdicts changes: the provider itself, or its trusted-key set.
857 pub fn invalidate_signature_cache(&self) {
858 self.sig_verify_cache.lock().unwrap().clear();
859 }
860
861 /// If `:verify-commit-signatures` is enabled, verify that `commit` inside
862 /// `repo_root` carries a valid GPG or SSH signature.
863 ///
864 /// Returns `Ok(())` immediately when the feature is off. On the happy
865 /// path the result is cached per `(repo_root, commit)` so each commit is
866 /// only verified once per session. On failure returns
867 /// `EvalError::CommitSignatureVerificationFailed`.
868 ///
869 /// If verification is demanded but this build has no VCS provider, the
870 /// check fails: silently accepting an unverifiable commit would defeat the
871 /// flag the user explicitly turned on.
872 pub fn check_commit_signature(&self, repo_root: &str, commit: &str) -> EvalResult<()> {
873 if !self.verify_commit_signatures.load(Ordering::Relaxed) {
874 return Ok(());
875 }
876 let key = (Arc::<str>::from(repo_root), Arc::<str>::from(commit));
877 if self.sig_verify_cache.lock().unwrap().contains(&key) {
878 return Ok(());
879 }
880 let Some(vcs) = self.vcs() else {
881 return Err(crate::env::error::EvalError::Runtime(format!(
882 "commit-signature verification is enabled, but this build has no VCS \
883 provider to verify commit {commit} with (cljrs-runtime built without \
884 the `deps` feature)"
885 )));
886 };
887 vcs.verify_commit_signature(std::path::Path::new(repo_root), commit)
888 .map_err(|e| match e {
889 crate::env::vcs::SignatureFailure::Untrusted { commit, reason } => {
890 crate::env::error::EvalError::CommitSignatureVerificationFailed {
891 commit,
892 reason,
893 }
894 }
895 crate::env::vcs::SignatureFailure::Error(msg) => {
896 crate::env::error::EvalError::Runtime(msg)
897 }
898 })?;
899 self.sig_verify_cache.lock().unwrap().insert(key);
900 Ok(())
901 }
902
903 /// Build the trusted-signer key set from a parsed `cljrs.edn` config and
904 /// install it, so subsequent `check_commit_signature` calls verify against
905 /// it. Inline keys are parsed directly; `File` entries are read from disk.
906 /// Returns the number of keys loaded; warns (to stderr) on any key that
907 /// fails to load rather than aborting. Returns 0 when this build has no
908 /// VCS provider, since there is nothing that could consume the keys.
909 ///
910 /// Replacing the trust set invalidates the signature cache for the same
911 /// reason replacing the provider does: a verdict reached under the old
912 /// keys is not a verdict under the new ones. (In the normal flow this
913 /// runs at session start, before anything has been verified.)
914 pub fn load_trusted_signers(&self, config: &cljrs_project::config::DepsConfig) -> usize {
915 let Some(vcs) = self.vcs() else {
916 return 0;
917 };
918 let loaded = vcs.load_trusted_signers(&config.trusted_signers);
919 self.invalidate_signature_cache();
920 loaded
921 }
922}
923
924// ── Env ───────────────────────────────────────────────────────────────────────
925
926/// The full execution environment: a stack of local frames plus the global env.
927pub struct Env {
928 pub frames: Vec<Frame>,
929 pub current_ns: Arc<str>,
930 pub globals: Arc<GlobalEnv>,
931 /// When set, unversioned same-namespace symbol lookups implicitly resolve
932 /// at this commit hash instead of HEAD. Set by the versioned resolver when
933 /// evaluating a function body fetched from git history.
934 pub versioned_eval_commit: Option<Arc<str>>,
935 /// True when evaluating the body of an `^:async` function.
936 /// Set by `cljrs-async`; allows the `await` special form to know whether
937 /// to yield (async context) or block the OS thread (sync context).
938 pub is_async: bool,
939}
940
941impl Env {
942 pub fn new(globals: Arc<GlobalEnv>, ns: &str) -> Self {
943 Self {
944 frames: Vec::new(),
945 current_ns: Arc::from(ns),
946 globals,
947 versioned_eval_commit: None,
948 is_async: false,
949 }
950 }
951
952 /// Create an Env for evaluating source at a specific commit.
953 pub fn new_versioned(globals: Arc<GlobalEnv>, ns: &str, commit: &str) -> Self {
954 Self {
955 versioned_eval_commit: Some(Arc::from(commit)),
956 ..Self::new(globals, ns)
957 }
958 }
959
960 /// Create an Env pre-loaded with a function's closed-over bindings.
961 pub fn with_closure(globals: Arc<GlobalEnv>, ns: &str, f: &CljxFn) -> Self {
962 let mut env = Self::new(globals, ns);
963 if !f.closed_over_names.is_empty() {
964 env.push_frame();
965 for (name, val) in f.closed_over_names.iter().zip(f.closed_over_vals.iter()) {
966 env.bind(name.clone(), val.clone());
967 }
968 }
969 env
970 }
971
972 pub fn push_frame(&mut self) {
973 self.frames.push(Frame::new());
974 }
975
976 pub fn pop_frame(&mut self) {
977 self.frames.pop();
978 }
979
980 /// Bind `name` to `val` in the top frame.
981 pub fn bind(&mut self, name: Arc<str>, val: Value) {
982 if let Some(frame) = self.frames.last_mut() {
983 frame.bind(name, val);
984 }
985 // If there are no frames, the binding is silently dropped.
986 // Callers must push a frame first.
987 }
988
989 /// Look up `name`: local frames (innermost first), then the current namespace.
990 pub fn lookup(&self, name: &str) -> Option<Value> {
991 tracing::trace!(target: "env", "lookup {} in {} frames", name, self.frames.len());
992 for frame in self.frames.iter().rev() {
993 if let Some(v) = frame.lookup(name) {
994 return Some(v.clone());
995 }
996 }
997 self.globals.lookup_in_ns(&self.current_ns, name)
998 }
999
1000 /// Look up `name` in local frames only — does **not** fall back to the
1001 /// global namespace. Used by the versioned resolver to check for local
1002 /// bindings before applying commit inheritance.
1003 pub fn lookup_local_frames(&self, name: &str) -> Option<Value> {
1004 for frame in self.frames.iter().rev() {
1005 if let Some(v) = frame.lookup(name) {
1006 return Some(v.clone());
1007 }
1008 }
1009 None
1010 }
1011
1012 /// Look up the Var object for `name` in the current namespace.
1013 pub fn lookup_var(&self, name: &str) -> Option<GcPtr<Var>> {
1014 self.globals.lookup_var_in_ns(&self.current_ns, name)
1015 }
1016
1017 /// Collect all current local bindings (all frames, innermost last).
1018 /// Used for closure capture.
1019 pub fn all_local_bindings(&self) -> (Vec<Arc<str>>, Vec<Value>) {
1020 let mut names = Vec::new();
1021 let mut vals = Vec::new();
1022 // Outermost first so inner frames override on lookup.
1023 for frame in &self.frames {
1024 for (n, v) in &frame.bindings {
1025 names.push(n.clone());
1026 vals.push(v.clone());
1027 }
1028 }
1029 (names, vals)
1030 }
1031
1032 /// Create a child Env for closure capture (same globals, same ns, captures locals).
1033 pub fn child(&self) -> Self {
1034 let (names, vals) = self.all_local_bindings();
1035 let mut child = Self::new(self.globals.clone(), &self.current_ns);
1036 child.is_async = self.is_async;
1037 if !names.is_empty() {
1038 child.push_frame();
1039 for (n, v) in names.into_iter().zip(vals) {
1040 child.bind(n, v);
1041 }
1042 }
1043 child
1044 }
1045
1046 #[inline(always)]
1047 pub fn eval(&mut self, form: &Form) -> EvalResult {
1048 let globals = self.globals.clone();
1049 globals.eval(form, self)
1050 }
1051
1052 #[inline(always)]
1053 pub fn call_cljrs_fn(&mut self, func: &CljxFn, args: &[Value]) -> EvalResult {
1054 let globals = self.globals.clone();
1055 globals.call_cljrs_fn(func, args, self)
1056 }
1057
1058 #[inline(always)]
1059 pub fn on_fn_defined(&mut self, func: &CljxFn) {
1060 let globals = self.globals.clone();
1061 globals.on_fn_defined(func, self);
1062 }
1063}