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