Skip to main content

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, 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    /// Public keys trusted to sign versioned dependency commits, built from
128    /// the `:trusted-signers` config.  Consulted by `check_commit_signature`
129    /// when `verify_commit_signatures` is on.  (Not built on wasm, where
130    /// `cljrs-project::vcs` is unavailable and signature checks are no-ops.)
131    #[cfg(not(target_arch = "wasm32"))]
132    pub trusted_keys: RwLock<Arc<cljrs_project::vcs::TrustedKeys>>,
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            #[cfg(not(target_arch = "wasm32"))]
233            trusted_keys: RwLock::new(Arc::new(cljrs_project::vcs::TrustedKeys::new())),
234            sig_verify_cache: Mutex::new(HashSet::new()),
235            versioned_sources: RwLock::new(HashMap::new()),
236            versioned_offline: AtomicBool::new(false),
237            native_provenance: RwLock::new(HashMap::new()),
238            enforce_native_versions: AtomicBool::new(false),
239            provenance_warned: Mutex::new(HashSet::new()),
240            pinned_native_loader: RwLock::new(None),
241            native_require_loader: RwLock::new(None),
242            compiled_ns_loaders: RwLock::new(HashMap::new()),
243        })
244    }
245
246    /// Replace the source path list.
247    pub fn set_source_paths(&self, paths: Vec<std::path::PathBuf>) {
248        *self.source_paths.write().unwrap() = paths;
249    }
250
251    /// Register an embedded namespace source (called by cljrs-stdlib at startup).
252    pub fn register_builtin_source(&self, ns: &str, src: &'static str) {
253        self.builtin_sources
254            .write()
255            .unwrap()
256            .insert(Arc::from(ns), src);
257    }
258
259    /// Look up an embedded source for a namespace, if one has been registered.
260    pub fn builtin_source(&self, ns: &str) -> Option<&'static str> {
261        self.builtin_sources.read().unwrap().get(ns).copied()
262    }
263
264    /// Register a loader for an AOT-compiled namespace (called by the harness
265    /// `main` of a binary produced by `cljrs compile`).
266    pub fn register_compiled_ns_loader(&self, ns: &str, loader: CompiledNsLoader) {
267        self.compiled_ns_loaders
268            .write()
269            .unwrap()
270            .insert(Arc::from(ns), loader);
271    }
272
273    /// Look up the loader for an AOT-compiled namespace, if one is registered.
274    pub fn compiled_ns_loader(&self, ns: &str) -> Option<CompiledNsLoader> {
275        self.compiled_ns_loaders.read().unwrap().get(ns).cloned()
276    }
277
278    /// Mark a namespace as fully loaded from a file.
279    pub fn mark_loaded(&self, ns: &str) {
280        self.loaded.lock().unwrap().insert(Arc::from(ns));
281    }
282
283    /// True if the namespace has already been loaded from a file.
284    pub fn is_loaded(&self, ns: &str) -> bool {
285        self.loaded.lock().unwrap().contains(ns)
286    }
287
288    /// Set the GC configuration for automatic memory pressure management.
289    pub fn set_gc_config(&self, config: Arc<GcConfig>) {
290        *self.gc_config.write().unwrap() = Some(config);
291    }
292
293    /// Get the GC configuration, if one has been set.
294    pub fn gc_config(&self) -> Option<Arc<GcConfig>> {
295        self.gc_config.read().unwrap().clone()
296    }
297
298    /// Resolve a short alias to a full namespace name in `current_ns`.
299    pub fn resolve_alias(&self, current_ns: &str, alias: &str) -> Option<Arc<str>> {
300        let map = self.namespaces.read().unwrap();
301        let ns = map.get(current_ns)?;
302        let aliases = ns.get().aliases.lock().unwrap();
303        aliases.get(alias).cloned()
304    }
305
306    /// Resolve an auto-resolved keyword name (the text after `::`) to its
307    /// fully-qualified `ns/name` form.
308    ///
309    /// `::kw` qualifies with `current_ns` directly; `::alias/kw` looks
310    /// `alias` up in `current_ns`'s alias table (populated by `(require
311    /// '[... :as alias])`) and qualifies with the resolved namespace.
312    pub fn resolve_auto_keyword(&self, current_ns: &str, name: &str) -> Result<String, String> {
313        match name.split_once('/') {
314            Some((alias, kw_name)) => match self.resolve_alias(current_ns, alias) {
315                Some(ns) => Ok(format!("{ns}/{kw_name}")),
316                None => Err(format!(
317                    "invalid token: ::{name} (no such namespace alias: {alias})"
318                )),
319            },
320            None => Ok(format!("{current_ns}/{name}")),
321        }
322    }
323
324    /// Return the namespace with this name, creating it if it doesn't exist.
325    pub fn get_or_create_ns(&self, name: &str) -> GcPtr<Namespace> {
326        // Fast path: already exists.
327        {
328            let map = self.namespaces.read().unwrap();
329            if let Some(ns) = map.get(name) {
330                return ns.clone();
331            }
332        }
333        // Slow path: insert.
334        let mut map = self.namespaces.write().unwrap();
335        // Re-check after acquiring write lock.
336        if let Some(ns) = map.get(name) {
337            return ns.clone();
338        }
339        let ns = GcPtr::new(Namespace::new(name));
340        map.insert(Arc::from(name), ns.clone());
341        ns
342    }
343
344    /// Intern `name` with `val` in the given namespace, returning the Var.
345    pub fn intern(&self, ns_name: &str, name: Arc<str>, val: Value) -> GcPtr<Var> {
346        let ns = self.get_or_create_ns(ns_name);
347        let mut interns = ns.get().interns.lock().unwrap();
348        if let Some(var) = interns.get(&name) {
349            // Update existing var.
350            var.get().bind(val);
351            return var.clone();
352        }
353        let var = GcPtr::new(Var::new(ns_name, name.as_ref()));
354        var.get().bind(val);
355        interns.insert(name, var.clone());
356        var
357    }
358
359    /// Look up a Var in the named namespace (interns only).
360    pub fn lookup_var(&self, ns_name: &str, sym_name: &str) -> Option<GcPtr<Var>> {
361        let map = self.namespaces.read().unwrap();
362        let ns = map.get(ns_name)?;
363        let interns = ns.get().interns.lock().unwrap();
364        interns.get(sym_name).cloned()
365    }
366
367    /// Look up a value in `ns_name`: checks interns then refers.
368    /// Routes through the dynamic binding stack so `binding` overrides work.
369    pub fn lookup_in_ns(&self, ns_name: &str, sym_name: &str) -> Option<Value> {
370        let map = self.namespaces.read().unwrap();
371        let ns = map.get(ns_name)?;
372        let ns_ref = ns.get();
373        // Check interns first.
374        {
375            let interns = ns_ref.interns.lock().unwrap();
376            if let Some(var) = interns.get(sym_name) {
377                return crate::env::dynamics::deref_var(var);
378            }
379        }
380        // Then refers.
381        {
382            let refers = ns_ref.refers.lock().unwrap();
383            if let Some(var) = refers.get(sym_name) {
384                return crate::env::dynamics::deref_var(var);
385            }
386        }
387        None
388    }
389
390    /// Look up the raw Var (not its value) in `ns_name`: interns then refers.
391    pub fn lookup_var_in_ns(&self, ns_name: &str, sym_name: &str) -> Option<GcPtr<Var>> {
392        let map = self.namespaces.read().unwrap();
393        let ns = map.get(ns_name)?;
394        let ns_ref = ns.get();
395        {
396            let interns = ns_ref.interns.lock().unwrap();
397            if let Some(var) = interns.get(sym_name) {
398                return Some(var.clone());
399            }
400        }
401        {
402            let refers = ns_ref.refers.lock().unwrap();
403            if let Some(var) = refers.get(sym_name) {
404                return Some(var.clone());
405            }
406        }
407        None
408    }
409
410    /// Copy all interns from `src_ns` into `dst_ns` as refers.
411    pub fn refer_all(&self, dst_ns: &str, src_ns: &str) {
412        let map = self.namespaces.read().unwrap();
413        let src = match map.get(src_ns) {
414            Some(ns) => ns.clone(),
415            None => return,
416        };
417        let dst = match map.get(dst_ns) {
418            Some(ns) => ns.clone(),
419            None => return,
420        };
421        let src_interns = src.get().interns.lock().unwrap();
422        let mut dst_refers = dst.get().refers.lock().unwrap();
423        for (name, var) in src_interns.iter() {
424            dst_refers.insert(name.clone(), var.clone());
425        }
426    }
427
428    /// Copy selected interns from `src_ns` into `dst_ns` as refers.
429    pub fn refer_named(&self, dst_ns: &str, src_ns: &str, names: &[Arc<str>]) {
430        let map = self.namespaces.read().unwrap();
431        let src = match map.get(src_ns) {
432            Some(ns) => ns.clone(),
433            None => return,
434        };
435        let dst = match map.get(dst_ns) {
436            Some(ns) => ns.clone(),
437            None => return,
438        };
439        let src_interns = src.get().interns.lock().unwrap();
440        let mut dst_refers = dst.get().refers.lock().unwrap();
441        for name in names {
442            if let Some(var) = src_interns.get(name) {
443                // Use insert (not or_insert_with) so that an explicit
444                // `require :refer [name]` always overrides a previous refer
445                // (e.g. one inherited from clojure.core via refer-all).
446                // clojure.core.async's `into` intentionally shadows clojure.core/into;
447                // or_insert_with would silently drop the override.
448                dst_refers.insert(name.clone(), var.clone());
449            }
450        }
451    }
452
453    /// Register `alias` → `full_ns` in `current_ns`'s alias table.
454    pub fn add_alias(&self, current_ns: &str, alias: &str, full_ns: &str) {
455        let ns_ptr = self.get_or_create_ns(current_ns);
456        let mut aliases = ns_ptr.get().aliases.lock().unwrap();
457        aliases.insert(Arc::from(alias), Arc::from(full_ns));
458    }
459
460    /// Process-unique identity of this runtime instance.
461    #[inline(always)]
462    pub fn id(&self) -> u64 {
463        self.id
464    }
465
466    /// This runtime's Tier-1/Tier-2 state.
467    #[inline(always)]
468    pub fn tiers(&self) -> &Arc<crate::tiered::tiers::Tiers> {
469        &self.tiers
470    }
471
472    /// This runtime's cache of lowered IR.
473    #[inline(always)]
474    pub fn ir_cache(&self) -> &crate::tiered::ir_cache::IrCache {
475        self.tiers.ir_cache()
476    }
477
478    /// This runtime's JIT counters, profiles, and native-code tables.
479    #[inline(always)]
480    pub fn jit(&self) -> &crate::tiered::jit_state::JitState {
481        self.tiers.jit()
482    }
483
484    /// The JIT compiler attached to this runtime, if any.
485    ///
486    /// `None` when no JIT is linked or installed; callers then keep to the
487    /// interpreter tiers.  Installed by `cljrs_compiler::jit::install`.
488    #[inline(always)]
489    pub fn jit_backend(&self) -> Option<Arc<dyn crate::tiered::backend::JitBackend>> {
490        self.tiers.jit().backend().cloned()
491    }
492
493    // ── Execution mode and tier state ────────────────────────────────────
494
495    /// How this runtime executes function calls.
496    #[inline(always)]
497    pub fn execution_mode(&self) -> ExecutionMode {
498        self.execution_mode
499    }
500
501    /// Which tiers are live right now.
502    #[inline(always)]
503    pub fn tier_state(&self) -> TierState {
504        TierState::from_u8(self.tier_state.load(Ordering::Acquire))
505    }
506
507    /// Raise the live tier state.  Called once by the runtime builder after
508    /// the bootstrap completes; lowering the tier is not supported, so a
509    /// request below the current state is ignored.
510    pub fn set_tier_state(&self, tier: TierState) {
511        let _ = self.tier_state.fetch_max(tier as u8, Ordering::AcqRel);
512    }
513
514    /// True when IR may be lowered, cached, and interpreted.  This is the
515    /// gate the old `compiler_ready` flag served.
516    #[inline(always)]
517    pub fn ir_enabled(&self) -> bool {
518        self.tier_state().ir_enabled()
519    }
520
521    // ── Evaluation entry points ──────────────────────────────────────────
522
523    /// Evaluate `form` in `env`.
524    #[inline(always)]
525    pub fn eval(&self, form: &Form, env: &mut Env) -> EvalResult {
526        crate::interp::eval::eval(form, env)
527    }
528
529    /// Call a Clojure function, taking the path this runtime's
530    /// [`ExecutionMode`] selects.
531    ///
532    /// This is the single function-call dispatch point: tree walk, tier-1 IR,
533    /// and JIT-native execution are all reached from here.
534    #[inline(always)]
535    pub fn call_cljrs_fn(&self, func: &CljxFn, args: &[Value], env: &mut Env) -> EvalResult {
536        match self.execution_mode {
537            ExecutionMode::TreeWalk => crate::interp::apply::call_cljrs_fn(func, args, env),
538            ExecutionMode::Tiered | ExecutionMode::TieredNoJit => {
539                crate::tiered::apply::call_cljrs_fn(func, args, env)
540            }
541            ExecutionMode::NoGcTransaction => crate::env::depth::call_cljrs_fn(func, args, env),
542        }
543    }
544
545    /// Notify the active tier that a new `fn*` was defined.
546    ///
547    /// In a tiered runtime with IR enabled this eagerly lowers the function
548    /// (when eager lowering is on); in every other mode it does nothing.
549    #[inline(always)]
550    pub fn on_fn_defined(&self, f: &CljxFn, env: &mut Env) {
551        if self.execution_mode.is_tiered() && self.ir_enabled() {
552            crate::tiered::ir_interp::eager_lower_fn(f, env);
553        }
554    }
555
556    /// Install an async runtime. Called once by `cljrs_async::init`.
557    /// Subsequent calls are silently ignored (first writer wins).
558    pub fn set_async_runtime(&self, rt: Arc<dyn AsyncRuntime>) {
559        let mut guard = self.async_rt.write().unwrap();
560        if guard.is_none() {
561            *guard = Some(rt);
562        }
563    }
564
565    /// Return the async runtime, if one has been registered.
566    pub fn async_runtime(&self) -> Option<Arc<dyn AsyncRuntime>> {
567        self.async_rt.read().unwrap().clone()
568    }
569
570    /// Return `(source_file, git_repo_root)` for the named namespace, if
571    /// both have been populated by the loader.
572    pub fn get_ns_git_context(&self, ns_name: &str) -> Option<(Arc<str>, Arc<str>)> {
573        let map = self.namespaces.read().unwrap();
574        let ns = map.get(ns_name)?;
575        let ns_ref = ns.get();
576        let file = ns_ref.source_file.lock().unwrap().clone()?;
577        let repo = ns_ref.git_repo_root.lock().unwrap().clone()?;
578        Some((file, repo))
579    }
580
581    /// Store a resolved versioned value in the cache.
582    /// Key: `"<ns>/<name>@<commit>"`.
583    pub fn cache_versioned(&self, ns: &str, name: &str, commit: &str, val: Value) {
584        let key: Arc<str> = Arc::from(format!("{ns}/{name}@{commit}"));
585        self.version_cache.lock().unwrap().insert(key, val);
586    }
587
588    /// Retrieve a previously resolved versioned value, if cached.
589    pub fn get_cached_versioned(&self, ns: &str, name: &str, commit: &str) -> Option<Value> {
590        let key = format!("{ns}/{name}@{commit}");
591        self.version_cache
592            .lock()
593            .unwrap()
594            .get(key.as_str())
595            .cloned()
596    }
597
598    /// Mark namespace `name@commit` as loaded in the standard loaded set.
599    pub fn cache_versioned_ns(&self, ns: &str, commit: &str) {
600        let key: Arc<str> = Arc::from(format!("{ns}@{commit}"));
601        self.version_cache.lock().unwrap().insert(key, Value::Nil);
602    }
603
604    /// Record the source text of a versioned namespace fetched from git.
605    /// Key: `"<ns>@<commit>"`.  Consumed by the AOT compiler for embedding.
606    pub fn record_versioned_source(&self, versioned_ns: &str, src: &str) {
607        self.versioned_sources
608            .write()
609            .unwrap()
610            .insert(Arc::from(versioned_ns), Arc::from(src));
611    }
612
613    /// Snapshot of all versioned sources fetched this session, sorted by key.
614    pub fn versioned_sources_snapshot(&self) -> Vec<(Arc<str>, Arc<str>)> {
615        let map = self.versioned_sources.read().unwrap();
616        let mut entries: Vec<_> = map.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
617        entries.sort_by(|a, b| a.0.cmp(&b.0));
618        entries
619    }
620
621    /// Restrict versioned-namespace resolution to embedded builtin sources
622    /// (no git).  Called by AOT harness binaries, which embed every pinned
623    /// source discovered at compile time.
624    pub fn set_versioned_offline(&self, offline: bool) {
625        self.versioned_offline.store(offline, Ordering::Relaxed);
626    }
627
628    /// True when versioned namespaces may only come from embedded sources.
629    pub fn versioned_offline(&self) -> bool {
630        self.versioned_offline.load(Ordering::Relaxed)
631    }
632
633    /// Record the git commit a native (Rust-backed) package was built from.
634    /// Called at registration time (`Registry::set_provenance` or the
635    /// `register_provenance!` inventory entry in cljrs-interop).
636    pub fn set_native_provenance(&self, ns: &str, commit: &str) {
637        self.native_provenance
638            .write()
639            .unwrap()
640            .insert(Arc::from(ns), Arc::from(commit));
641    }
642
643    /// The recorded provenance commit for a native package's namespace.
644    pub fn native_provenance_for(&self, ns: &str) -> Option<Arc<str>> {
645        self.native_provenance.read().unwrap().get(ns).cloned()
646    }
647
648    /// Make pinned-native provenance mismatches hard errors.
649    pub fn set_enforce_native_versions(&self, enforce: bool) {
650        self.enforce_native_versions
651            .store(enforce, Ordering::Relaxed);
652    }
653
654    /// True when pinned-native provenance mismatches are errors.
655    pub fn enforce_native_versions(&self) -> bool {
656        self.enforce_native_versions.load(Ordering::Relaxed)
657    }
658
659    /// Install the pinned-native package loader (called once by
660    /// `cljrs::native::pinned::install`; first writer wins).
661    pub fn set_pinned_native_loader(&self, loader: PinnedNativeLoader) {
662        let mut guard = self.pinned_native_loader.write().unwrap();
663        if guard.is_none() {
664            *guard = Some(loader);
665        }
666    }
667
668    /// Install the native-dependency `require` loader (called once by
669    /// `cljrs::native::pinned::install`; first writer wins).
670    pub fn set_native_require_loader(&self, loader: NativeRequireLoader) {
671        let mut guard = self.native_require_loader.write().unwrap();
672        if guard.is_none() {
673            *guard = Some(loader);
674        }
675    }
676
677    /// If `:verify-commit-signatures` is enabled, verify that `commit` inside
678    /// `repo_root` carries a valid GPG or SSH signature.
679    ///
680    /// Returns `Ok(())` immediately when the feature is off.  On the happy
681    /// path the result is cached per `(repo_root, commit)` so each commit is
682    /// only verified once per session.  On failure returns
683    /// `EvalError::CommitSignatureVerificationFailed`.
684    pub fn check_commit_signature(&self, repo_root: &str, commit: &str) -> EvalResult<()> {
685        #[cfg(not(target_arch = "wasm32"))]
686        {
687            if !self.verify_commit_signatures.load(Ordering::Relaxed) {
688                return Ok(());
689            }
690            let key = (Arc::<str>::from(repo_root), Arc::<str>::from(commit));
691            if self.sig_verify_cache.lock().unwrap().contains(&key) {
692                return Ok(());
693            }
694            let trusted = self.trusted_keys.read().unwrap().clone();
695            cljrs_project::vcs::verify_commit_signature(
696                std::path::Path::new(repo_root),
697                commit,
698                &trusted,
699            )
700            .map_err(|e| match e {
701                cljrs_project::vcs::VcsError::SignatureVerificationFailed { commit: c, reason } => {
702                    crate::env::error::EvalError::CommitSignatureVerificationFailed {
703                        commit: c,
704                        reason,
705                    }
706                }
707                other => crate::env::error::EvalError::Runtime(format!("{other}")),
708            })?;
709            self.sig_verify_cache.lock().unwrap().insert(key);
710        }
711        let _ = (repo_root, commit);
712        Ok(())
713    }
714
715    /// Build the trusted-signer key set from a parsed `cljrs.edn` config and
716    /// install it, so subsequent `check_commit_signature` calls verify against
717    /// it.  Inline keys are parsed directly; `File` entries are read from disk.
718    /// Returns the number of keys loaded; warns (to stderr) on any key that
719    /// fails to load rather than aborting.  (Not available on wasm.)
720    #[cfg(not(target_arch = "wasm32"))]
721    pub fn load_trusted_signers(&self, config: &cljrs_project::config::DepsConfig) -> usize {
722        let mut keys = cljrs_project::vcs::TrustedKeys::new();
723        let mut loaded = 0usize;
724        for signer in &config.trusted_signers {
725            let result = match signer {
726                cljrs_project::config::TrustedSigner::Inline(text) => keys.add_key_text(text),
727                cljrs_project::config::TrustedSigner::File(path) => {
728                    match std::fs::read_to_string(path) {
729                        Ok(text) => keys.add_key_text(&text),
730                        Err(e) => {
731                            eprintln!(
732                                "cljrs: warning: could not read trusted signer key {}: {e}",
733                                path.display()
734                            );
735                            continue;
736                        }
737                    }
738                }
739            };
740            match result {
741                Ok(()) => loaded += 1,
742                Err(e) => eprintln!("cljrs: warning: invalid trusted signer key: {e}"),
743            }
744        }
745        *self.trusted_keys.write().unwrap() = Arc::new(keys);
746        loaded
747    }
748}
749
750// ── Env ───────────────────────────────────────────────────────────────────────
751
752/// The full execution environment: a stack of local frames plus the global env.
753pub struct Env {
754    pub frames: Vec<Frame>,
755    pub current_ns: Arc<str>,
756    pub globals: Arc<GlobalEnv>,
757    /// When set, unversioned same-namespace symbol lookups implicitly resolve
758    /// at this commit hash instead of HEAD.  Set by the versioned resolver when
759    /// evaluating a function body fetched from git history.
760    pub versioned_eval_commit: Option<Arc<str>>,
761    /// True when evaluating the body of an `^:async` function.
762    /// Set by `cljrs-async`; allows the `await` special form to know whether
763    /// to yield (async context) or block the OS thread (sync context).
764    pub is_async: bool,
765}
766
767impl Env {
768    pub fn new(globals: Arc<GlobalEnv>, ns: &str) -> Self {
769        Self {
770            frames: Vec::new(),
771            current_ns: Arc::from(ns),
772            globals,
773            versioned_eval_commit: None,
774            is_async: false,
775        }
776    }
777
778    /// Create an Env for evaluating source at a specific commit.
779    pub fn new_versioned(globals: Arc<GlobalEnv>, ns: &str, commit: &str) -> Self {
780        Self {
781            versioned_eval_commit: Some(Arc::from(commit)),
782            ..Self::new(globals, ns)
783        }
784    }
785
786    /// Create an Env pre-loaded with a function's closed-over bindings.
787    pub fn with_closure(globals: Arc<GlobalEnv>, ns: &str, f: &CljxFn) -> Self {
788        let mut env = Self::new(globals, ns);
789        if !f.closed_over_names.is_empty() {
790            env.push_frame();
791            for (name, val) in f.closed_over_names.iter().zip(f.closed_over_vals.iter()) {
792                env.bind(name.clone(), val.clone());
793            }
794        }
795        env
796    }
797
798    pub fn push_frame(&mut self) {
799        self.frames.push(Frame::new());
800    }
801
802    pub fn pop_frame(&mut self) {
803        self.frames.pop();
804    }
805
806    /// Bind `name` to `val` in the top frame.
807    pub fn bind(&mut self, name: Arc<str>, val: Value) {
808        if let Some(frame) = self.frames.last_mut() {
809            frame.bind(name, val);
810        }
811        // If there are no frames, the binding is silently dropped.
812        // Callers must push a frame first.
813    }
814
815    /// Look up `name`: local frames (innermost first), then the current namespace.
816    pub fn lookup(&self, name: &str) -> Option<Value> {
817        tracing::trace!(target: "env", "lookup {} in {} frames", name, self.frames.len());
818        for frame in self.frames.iter().rev() {
819            if let Some(v) = frame.lookup(name) {
820                return Some(v.clone());
821            }
822        }
823        self.globals.lookup_in_ns(&self.current_ns, name)
824    }
825
826    /// Look up `name` in local frames only — does **not** fall back to the
827    /// global namespace.  Used by the versioned resolver to check for local
828    /// bindings before applying commit inheritance.
829    pub fn lookup_local_frames(&self, name: &str) -> Option<Value> {
830        for frame in self.frames.iter().rev() {
831            if let Some(v) = frame.lookup(name) {
832                return Some(v.clone());
833            }
834        }
835        None
836    }
837
838    /// Look up the Var object for `name` in the current namespace.
839    pub fn lookup_var(&self, name: &str) -> Option<GcPtr<Var>> {
840        self.globals.lookup_var_in_ns(&self.current_ns, name)
841    }
842
843    /// Collect all current local bindings (all frames, innermost last).
844    /// Used for closure capture.
845    pub fn all_local_bindings(&self) -> (Vec<Arc<str>>, Vec<Value>) {
846        let mut names = Vec::new();
847        let mut vals = Vec::new();
848        // Outermost first so inner frames override on lookup.
849        for frame in &self.frames {
850            for (n, v) in &frame.bindings {
851                names.push(n.clone());
852                vals.push(v.clone());
853            }
854        }
855        (names, vals)
856    }
857
858    /// Create a child Env for closure capture (same globals, same ns, captures locals).
859    pub fn child(&self) -> Self {
860        let (names, vals) = self.all_local_bindings();
861        let mut child = Self::new(self.globals.clone(), &self.current_ns);
862        child.is_async = self.is_async;
863        if !names.is_empty() {
864            child.push_frame();
865            for (n, v) in names.into_iter().zip(vals) {
866                child.bind(n, v);
867            }
868        }
869        child
870    }
871
872    #[inline(always)]
873    pub fn eval(&mut self, form: &Form) -> EvalResult {
874        let globals = self.globals.clone();
875        globals.eval(form, self)
876    }
877
878    #[inline(always)]
879    pub fn call_cljrs_fn(&mut self, func: &CljxFn, args: &[Value]) -> EvalResult {
880        let globals = self.globals.clone();
881        globals.call_cljrs_fn(func, args, self)
882    }
883
884    #[inline(always)]
885    pub fn on_fn_defined(&mut self, func: &CljxFn) {
886        let globals = self.globals.clone();
887        globals.on_fn_defined(func, self);
888    }
889}