Skip to main content

cljrs_value/
types.rs

1//! Stub types for Phase 4/7 that are referenced by the Value enum.
2
3#![allow(unused)]
4
5use std::collections::HashMap;
6use std::mem;
7use std::sync::{Arc, Condvar, Mutex};
8
9use cljrs_gc::GcPtr;
10use cljrs_gc::mutator::TrackedMutex;
11use cljrs_reader::Form;
12
13use crate::TypeHint;
14use crate::Value;
15
16// ── No-GC debug provenance helper ────────────────────────────────────────────
17
18/// In `no-gc` debug builds: return `true` if the top-level `GcPtr` inside
19/// `value` (if any) was allocated by the global `StaticArena`.
20///
21/// Primitives (`Nil`, `Bool`, `Long`, `Double`, `Char`) contain no `GcPtr`
22/// and always return `true`.  `Resource` is Arc-managed and also returns
23/// `true`.  All other variants have a `GcPtr` that is checked against the
24/// static arena's chunk range.
25///
26/// This check is intentionally **shallow** (top-level pointer only).  If the
27/// value was produced inside a `StaticCtxGuard`, ALL allocations during its
28/// evaluation go to the static arena — so a static top-level pointer implies
29/// static contents.
30#[cfg(all(feature = "no-gc", debug_assertions))]
31pub(crate) fn value_gcptr_is_static(value: &Value) -> bool {
32    use crate::value::MapValue;
33    use crate::value::SetValue;
34    match value {
35        // Inline scalars — no GcPtr.
36        Value::Nil
37        | Value::Bool(_)
38        | Value::Long(_)
39        | Value::Double(_)
40        | Value::Char(_)
41        | Value::Uuid(_) => true,
42        // Arc-managed — not GcPtr; always considered static.
43        Value::Resource(_) | Value::SharedAtom(_) | Value::ByteBlob(_) => true,
44        // GcPtr variants.
45        Value::BigInt(p) => p.is_static_alloc(),
46        Value::BigDecimal(p) => p.is_static_alloc(),
47        Value::Ratio(p) => p.is_static_alloc(),
48        Value::Str(p) => p.is_static_alloc(),
49        Value::Pattern(p) => p.is_static_alloc(),
50        Value::Matcher(p) => p.is_static_alloc(),
51        Value::Symbol(p) => p.is_static_alloc(),
52        Value::Keyword(p) => p.is_static_alloc(),
53        Value::List(p) => p.is_static_alloc(),
54        Value::Vector(p) => p.is_static_alloc(),
55        Value::Queue(p) => p.is_static_alloc(),
56        Value::Map(m) => match m {
57            MapValue::Array(p) => p.is_static_alloc(),
58            MapValue::Hash(p) => p.is_static_alloc(),
59            MapValue::Sorted(p) => p.is_static_alloc(),
60        },
61        Value::Set(s) => match s {
62            SetValue::Hash(p) => p.is_static_alloc(),
63            SetValue::Sorted(p) => p.is_static_alloc(),
64        },
65        Value::NativeFunction(p) => p.is_static_alloc(),
66        Value::Fn(p) | Value::Macro(p) => p.is_static_alloc(),
67        Value::BoundFn(p) => p.is_static_alloc(),
68        Value::Var(p) => p.is_static_alloc(),
69        Value::Atom(p) => p.is_static_alloc(),
70        Value::Namespace(p) => p.is_static_alloc(),
71        Value::LazySeq(p) => p.is_static_alloc(),
72        Value::Cons(p) => p.is_static_alloc(),
73        Value::Protocol(p) => p.is_static_alloc(),
74        Value::ProtocolFn(p) => p.is_static_alloc(),
75        Value::MultiFn(p) => p.is_static_alloc(),
76        Value::Volatile(p) => p.is_static_alloc(),
77        Value::Delay(p) => p.is_static_alloc(),
78        Value::Promise(p) => p.is_static_alloc(),
79        Value::Future(p) => p.is_static_alloc(),
80        Value::Agent(p) => p.is_static_alloc(),
81        Value::TypeInstance(p) => p.is_static_alloc(),
82        Value::ObjectArray(p) => p.is_static_alloc(),
83        Value::NativeObject(p) => p.is_static_alloc(),
84        Value::Error(p) => p.is_static_alloc(),
85        Value::TransientMap(p) => p.is_static_alloc(),
86        Value::TransientVector(p) => p.is_static_alloc(),
87        Value::TransientSet(p) => p.is_static_alloc(),
88        // Primitive arrays — no meaningful pointer check needed.
89        Value::BooleanArray(_)
90        | Value::ByteArray(_)
91        | Value::ShortArray(_)
92        | Value::IntArray(_)
93        | Value::LongArray(_)
94        | Value::FloatArray(_)
95        | Value::DoubleArray(_)
96        | Value::CharArray(_) => true,
97        // Wrapper variants.
98        Value::Reduced(inner) | Value::WithMeta(inner, _) => value_gcptr_is_static(inner),
99    }
100}
101
102// ── Protocol ──────────────────────────────────────────────────────────────────
103
104/// Inner map type for protocol implementations: method_name → impl fn.
105pub type MethodMap = HashMap<Arc<str>, Value>;
106
107/// A Clojure protocol — an interface-like construct with named methods.
108#[derive(Debug)]
109pub struct Protocol {
110    pub name: Arc<str>,
111    pub ns: Arc<str>,
112    pub methods: Vec<ProtocolMethod>,
113    /// type_tag → { method_name → impl fn }
114    pub impls: TrackedMutex<HashMap<Arc<str>, MethodMap>>,
115    /// `(defprotocol Name :extend-via-metadata true ...)` — when set, protocol
116    /// dispatch consults the dispatch value's metadata (keyed by this
117    /// protocol's `ProtocolFn`s) before falling back to type-tag impls.
118    pub extend_via_metadata: bool,
119}
120
121impl Protocol {
122    pub fn new(
123        name: Arc<str>,
124        ns: Arc<str>,
125        methods: Vec<ProtocolMethod>,
126        extend_via_metadata: bool,
127    ) -> Self {
128        Self {
129            name,
130            ns,
131            methods,
132            impls: TrackedMutex::new(HashMap::new()),
133            extend_via_metadata,
134        }
135    }
136}
137
138/// Global protocol-extension generation, bumped on every `impls` mutation
139/// (`extend-type`, `extend-protocol`, `defrecord`/`reify` inline impls).
140///
141/// Inline caches for protocol dispatch (Phase 10.6, `rt_call_ic` in
142/// `cljrs-compiler`'s rt_abi) tag each cached `(dispatch type → impl fn)`
143/// entry with the generation observed at fill time; a later bump invalidates
144/// every cache entry at once, so re-extending a protocol mid-session is
145/// picked up on the next dispatch through any call site.
146static PROTOCOL_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
147
148/// Current protocol-extension generation (see [`bump_protocol_generation`]).
149pub fn protocol_generation() -> u64 {
150    PROTOCOL_GENERATION.load(std::sync::atomic::Ordering::Acquire)
151}
152
153/// Invalidate all protocol-dispatch inline caches.  Must be called after
154/// every mutation of any [`Protocol::impls`] map.
155pub fn bump_protocol_generation() {
156    PROTOCOL_GENERATION.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
157}
158
159impl cljrs_gc::Trace for Protocol {
160    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
161        {
162            let impls = self.impls.lock().unwrap();
163            for method_map in impls.values() {
164                for v in method_map.values() {
165                    v.trace(visitor);
166                }
167            }
168        }
169    }
170}
171
172/// One method signature declared in a `defprotocol`.
173#[derive(Debug, Clone)]
174pub struct ProtocolMethod {
175    pub name: Arc<str>,
176    pub min_arity: usize,
177    pub variadic: bool,
178}
179
180impl cljrs_gc::Trace for ProtocolMethod {
181    fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
182}
183
184// ── ProtocolFn ────────────────────────────────────────────────────────────────
185
186/// Callable that dispatches a single protocol method on the type of `args[0]`.
187#[derive(Debug)]
188pub struct ProtocolFn {
189    pub protocol: GcPtr<Protocol>,
190    pub method_name: Arc<str>,
191    pub min_arity: usize,
192    pub variadic: bool,
193}
194
195impl cljrs_gc::Trace for ProtocolFn {
196    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
197        use cljrs_gc::GcVisitor as _;
198        visitor.visit(&self.protocol);
199    }
200}
201
202// ── MultiFn ───────────────────────────────────────────────────────────────────
203
204/// A Clojure multimethod — arbitrary dispatch via a user-supplied function.
205#[derive(Debug)]
206pub struct MultiFn {
207    pub name: Arc<str>,
208    pub dispatch_fn: Value,
209    /// pr_str(dispatch-val) → implementation fn
210    pub methods: TrackedMutex<HashMap<String, Value>>,
211    /// pr_str(dispatch-val) → the dispatch value itself, for `isa?` lookups
212    pub dispatch_vals: TrackedMutex<HashMap<String, Value>>,
213    /// recorded preferences: pr_str(preferred) → pr_str(over)
214    pub prefers: TrackedMutex<HashMap<String, Vec<String>>>,
215    /// pr_str(preference dispatch value) → original value, including values
216    /// which do not currently have a method.
217    pub preference_vals: TrackedMutex<HashMap<String, Value>>,
218    /// normally ":default"
219    pub default_dispatch: String,
220    method_generation: std::sync::atomic::AtomicU64,
221    method_cache: TrackedMutex<MultiFnMethodCache>,
222}
223
224#[derive(Debug, Default)]
225struct MultiFnMethodCache {
226    hierarchy_generation: u64,
227    method_generation: u64,
228    methods: HashMap<String, String>,
229}
230
231impl MultiFn {
232    pub fn new(name: Arc<str>, dispatch_fn: Value, default_dispatch: String) -> Self {
233        Self {
234            name,
235            dispatch_fn,
236            methods: TrackedMutex::new(HashMap::new()),
237            dispatch_vals: TrackedMutex::new(HashMap::new()),
238            prefers: TrackedMutex::new(HashMap::new()),
239            preference_vals: TrackedMutex::new(HashMap::new()),
240            default_dispatch,
241            method_generation: std::sync::atomic::AtomicU64::new(0),
242            method_cache: TrackedMutex::new(MultiFnMethodCache::default()),
243        }
244    }
245
246    /// Current generation of this multimethod's method and preference tables.
247    pub fn method_generation(&self) -> u64 {
248        self.method_generation
249            .load(std::sync::atomic::Ordering::Acquire)
250    }
251
252    /// Invalidate this multimethod's cached inherited/default resolutions.
253    pub fn bump_method_generation(&self) {
254        self.method_generation
255            .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
256    }
257
258    /// Return a cached resolved method key when both source generations match.
259    /// Observing either new generation lazily discards stale entries at once.
260    pub fn cached_method(
261        &self,
262        dispatch_key: &str,
263        hierarchy_generation: u64,
264        method_generation: u64,
265    ) -> Option<String> {
266        let mut cache = self.method_cache.lock().unwrap();
267        if cache.hierarchy_generation != hierarchy_generation
268            || cache.method_generation != method_generation
269        {
270            cache.hierarchy_generation = hierarchy_generation;
271            cache.method_generation = method_generation;
272            cache.methods.clear();
273        }
274        cache.methods.get(dispatch_key).cloned()
275    }
276
277    /// Cache `dispatch_key -> method_key` under the source generations used to
278    /// resolve it. A concurrent mutation makes the entry stale, so the next
279    /// lookup discards it; the write itself never needs to be suppressed.
280    pub fn cache_method(
281        &self,
282        dispatch_key: String,
283        method_key: String,
284        hierarchy_generation: u64,
285        method_generation: u64,
286    ) {
287        let mut cache = self.method_cache.lock().unwrap();
288        if cache.hierarchy_generation != hierarchy_generation
289            || cache.method_generation != method_generation
290        {
291            cache.hierarchy_generation = hierarchy_generation;
292            cache.method_generation = method_generation;
293            cache.methods.clear();
294        }
295        cache.methods.insert(dispatch_key, method_key);
296    }
297
298    /// Number of entries in the current cache, exposed for regression tests.
299    pub fn cached_method_count(&self) -> usize {
300        self.method_cache.lock().unwrap().methods.len()
301    }
302}
303
304impl cljrs_gc::Trace for MultiFn {
305    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
306        self.dispatch_fn.trace(visitor);
307        {
308            let methods = self.methods.lock().unwrap();
309            for v in methods.values() {
310                v.trace(visitor);
311            }
312        }
313        {
314            let dispatch_vals = self.dispatch_vals.lock().unwrap();
315            for v in dispatch_vals.values() {
316                v.trace(visitor);
317            }
318        }
319        {
320            let preference_vals = self.preference_vals.lock().unwrap();
321            for v in preference_vals.values() {
322                v.trace(visitor);
323            }
324        }
325    }
326}
327
328// ── Var ───────────────────────────────────────────────────────────────────────
329
330/// A Clojure var — a namespace-interned mutable root binding.
331///
332/// ## Two-tier root (Phase B3, issue #171)
333///
334/// A var's *root* binding uses the same two-tier mechanism as `shared-atom`:
335///
336/// - **`value`** — the isolate-local, GC-backed fast path.  Every var deref,
337///   the IR tier, and the JIT/AOT `rt_*` ABI read this slot; promotion never
338///   touches it, so compiled inline caches and pointer-identity assumptions
339///   baked into native code stay valid.
340/// - **`shared_root`** — a `Send + Sync` cross-isolate mirror,
341///   `Arc<ArcSwap<Option<SharedValue>>>`, reusing
342///   [`crate::shared::SharedValue`].  `bind` (i.e. `def` / `alter-var-root` /
343///   `set!`) *promotes-on-write*: if the new root value is promotable the cell
344///   holds `Some(SharedValue)`, otherwise it is cleared to `None` (option (b)
345///   of the ADR — non-promotable roots, e.g. closures, stay isolate-local).
346///
347/// The shared cell is what crosses the structured-clone boundary: a var
348/// `def`'d in one isolate is observable *by value* from another, with keyword
349/// /symbol identity preserved through the intern table.  See
350/// `crate::clone` for the serialize/deserialize seam.
351///
352/// Dynamic `binding` is unchanged — it is already thread-local / per-isolate
353/// and lives on the binding stack, not in the var root.
354#[derive(Debug)]
355pub struct Var {
356    pub namespace: Arc<str>,
357    pub name: Arc<str>,
358    pub value: TrackedMutex<Option<Value>>,
359    /// Cross-isolate mirror of the root binding (Phase B3).  `None` when the
360    /// var is unbound or its current root is not promotable.
361    pub shared_root: Arc<arc_swap::ArcSwap<Option<crate::shared::SharedValue>>>,
362    pub is_macro: bool,
363    /// Metadata map (e.g. `{:dynamic true}`).
364    pub meta: TrackedMutex<Option<Value>>,
365    pub watches: TrackedMutex<Vec<(Value, Value)>>,
366    binding_generation: std::sync::atomic::AtomicU64,
367}
368
369impl Var {
370    pub fn new(namespace: impl Into<Arc<str>>, name: impl Into<Arc<str>>) -> Self {
371        Self {
372            namespace: namespace.into(),
373            name: name.into(),
374            value: TrackedMutex::new(None),
375            shared_root: Arc::new(arc_swap::ArcSwap::new(Arc::new(None))),
376            is_macro: false,
377            meta: TrackedMutex::new(None),
378            watches: TrackedMutex::new(Vec::new()),
379            binding_generation: std::sync::atomic::AtomicU64::new(0),
380        }
381    }
382
383    /// Reconstruct a var on the receiving side of an isolate boundary.
384    ///
385    /// The `shared_root` cell is the *same* `Arc` as the sending isolate's, so
386    /// both isolates share the cross-isolate root cell.  The local `value`
387    /// fast-path slot is seeded by demoting the current shared snapshot, so an
388    /// immediate `deref` observes the value the var carried at crossing time.
389    pub fn from_shared_root(
390        namespace: impl Into<Arc<str>>,
391        name: impl Into<Arc<str>>,
392        is_macro: bool,
393        shared_root: Arc<arc_swap::ArcSwap<Option<crate::shared::SharedValue>>>,
394    ) -> Self {
395        let local = shared_root
396            .load()
397            .as_ref()
398            .as_ref()
399            .map(crate::shared::demote);
400        Self {
401            namespace: namespace.into(),
402            name: name.into(),
403            value: TrackedMutex::new(local),
404            shared_root,
405            is_macro,
406            meta: TrackedMutex::new(None),
407            watches: TrackedMutex::new(Vec::new()),
408            binding_generation: std::sync::atomic::AtomicU64::new(0),
409        }
410    }
411
412    pub fn is_bound(&self) -> bool {
413        self.value.lock().unwrap().is_some()
414    }
415
416    pub fn deref(&self) -> Option<Value> {
417        self.value.lock().unwrap().clone()
418    }
419
420    /// Generation of the isolate-local root binding, incremented after each
421    /// completed [`Var::bind`].
422    pub fn binding_generation(&self) -> u64 {
423        self.binding_generation
424            .load(std::sync::atomic::Ordering::Acquire)
425    }
426
427    /// Read the cross-isolate root by demoting the shared cell, ignoring the
428    /// isolate-local fast path.  Returns `None` when the shared root is empty
429    /// (unbound or non-promotable).  Used to observe writes another isolate
430    /// made through the shared cell.
431    pub fn deref_shared(&self) -> Option<Value> {
432        self.shared_root
433            .load()
434            .as_ref()
435            .as_ref()
436            .map(crate::shared::demote)
437    }
438
439    pub fn bind(&self, v: Value) {
440        // In no-gc debug builds: assert the value being stored in this
441        // program-lifetime Var came from the StaticArena, not a scratch region.
442        // A region-local pointer would dangle after the function returns.
443        #[cfg(all(feature = "no-gc", debug_assertions))]
444        debug_assert!(
445            value_gcptr_is_static(&v),
446            "no-gc: Var::bind({}/{}) received a region-local value — store violations \
447             indicate a missing StaticCtxGuard around the value expression",
448            self.namespace,
449            self.name
450        );
451        // GC builds: heap-promotion fallback — a region-allocated value bound
452        // to a program-lifetime var is deep-copied to the heap (or the active
453        // regions are retired when it cannot be).  One depth check when no
454        // region is open.
455        let v = crate::publish::publish_value(v);
456        // Replace the binding, holding the lock only across the swap.  The
457        // previous value (if any) is handed to the JIT rebind hook so it can
458        // reclaim native code compiled for a now-superseded definition
459        // (Phase 10.2 — code unloading).  `v.clone()` is O(1) for the only
460        // values that carry compiled code (`Value::Fn`, a `GcPtr` clone).
461        let prev = {
462            let mut slot = self.value.lock().unwrap();
463            slot.replace(v.clone())
464        };
465        // Promote-on-`def` (Phase B3): mirror the new root into the
466        // cross-isolate cell when it is promotable, else clear the cell so it
467        // never advertises a stale or non-shareable root.  `def` is rare and
468        // global by nature, so this write-path cost is acceptable; the read
469        // path (and the JIT) never touch the shared cell.
470        let shared = crate::shared::promote(&v).ok();
471        self.shared_root.store(Arc::new(shared));
472        self.binding_generation
473            .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
474        if let Some(prev) = prev {
475            crate::jit_hooks::notify_var_rebind(&prev, &v);
476        }
477    }
478
479    pub fn get_meta(&self) -> Option<Value> {
480        self.meta.lock().unwrap().clone()
481    }
482
483    pub fn set_meta(&self, m: Value) {
484        *self.meta.lock().unwrap() = Some(m);
485    }
486
487    pub fn full_name(&self) -> String {
488        format!("{}/{}", self.namespace, self.name)
489    }
490}
491
492impl cljrs_gc::Trace for Var {
493    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
494        {
495            let value = self.value.lock().unwrap();
496            if let Some(v) = value.as_ref() {
497                v.trace(visitor);
498            }
499        }
500        {
501            let meta = self.meta.lock().unwrap();
502            if let Some(m) = meta.as_ref() {
503                m.trace(visitor);
504            }
505        }
506        {
507            let watches = self.watches.lock().unwrap();
508            for (key, f) in watches.iter() {
509                key.trace(visitor);
510                f.trace(visitor);
511            }
512        }
513    }
514}
515
516// ── Atom ──────────────────────────────────────────────────────────────────────
517
518/// A Clojure atom — a thread-safe mutable reference.
519#[derive(Debug)]
520pub struct Atom {
521    pub value: TrackedMutex<Value>,
522    pub meta: TrackedMutex<Option<Value>>,
523    pub validator: TrackedMutex<Option<Value>>,
524    pub watches: TrackedMutex<Vec<(Value, Value)>>,
525}
526
527impl Atom {
528    pub fn new(v: Value) -> Self {
529        // Heap-promotion fallback (GC builds): an atom is program-lifetime
530        // shared state, so its initial value must not be region-allocated.
531        let v = crate::publish::publish_value(v);
532        Self {
533            value: TrackedMutex::new(v),
534            meta: TrackedMutex::new(None),
535            validator: TrackedMutex::new(None),
536            watches: TrackedMutex::new(Vec::new()),
537        }
538    }
539
540    pub fn deref(&self) -> Value {
541        self.value.lock().unwrap().clone()
542    }
543
544    pub fn reset(&self, v: Value) -> Value {
545        // In no-gc debug builds: assert the new value came from the StaticArena.
546        #[cfg(all(feature = "no-gc", debug_assertions))]
547        debug_assert!(
548            value_gcptr_is_static(&v),
549            "no-gc: Atom::reset() received a region-local value — the new-value \
550             expression must be computed inside a StaticCtxGuard (i.e. inside \
551             the swap! / reset! call) so it is allocated in the static arena"
552        );
553        // GC builds: heap-promotion fallback (see `Var::bind`).
554        let v = crate::publish::publish_value(v);
555        let mut guard = self.value.lock().unwrap();
556        *guard = v.clone();
557        v
558    }
559
560    pub fn get_meta(&self) -> Option<Value> {
561        self.meta.lock().unwrap().clone()
562    }
563
564    pub fn set_meta(&self, m: Option<Value>) {
565        *self.meta.lock().unwrap() = m;
566    }
567
568    pub fn get_validator(&self) -> Option<Value> {
569        self.validator.lock().unwrap().clone()
570    }
571
572    pub fn set_validator(&self, vf: Option<Value>) {
573        *self.validator.lock().unwrap() = vf;
574    }
575}
576
577impl cljrs_gc::Trace for Atom {
578    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
579        {
580            let value = self.value.lock().unwrap();
581            value.trace(visitor);
582        }
583        {
584            let meta = self.meta.lock().unwrap();
585            if let Some(m) = meta.as_ref() {
586                m.trace(visitor);
587            }
588        }
589        {
590            let validator = self.validator.lock().unwrap();
591            if let Some(vf) = validator.as_ref() {
592                vf.trace(visitor);
593            }
594        }
595        {
596            let watches = self.watches.lock().unwrap();
597            for (key, f) in watches.iter() {
598                key.trace(visitor);
599                f.trace(visitor);
600            }
601        }
602    }
603}
604
605// ── Namespace ─────────────────────────────────────────────────────────────────
606
607/// Which `clojure.core` names a namespace auto-refers, as narrowed by an
608/// `(:refer-clojure ...)` clause in `ns`.  An absent filter (the default)
609/// refers every public core name.
610#[derive(Debug, Clone, Default)]
611pub struct ReferClojureFilter {
612    /// `:only` — when set, no core name outside this set is referred.
613    pub only: Option<std::collections::HashSet<Arc<str>>>,
614    /// `:exclude` — core names that are never referred.
615    pub exclude: std::collections::HashSet<Arc<str>>,
616    /// `:rename` — core name → the local name it is referred under.  A renamed
617    /// name is *not* also referred under its original name (matching
618    /// `clojure.core/refer`).
619    pub rename: HashMap<Arc<str>, Arc<str>>,
620}
621
622impl ReferClojureFilter {
623    /// The local name `name` is referred under, or `None` when this filter
624    /// drops it.
625    pub fn local_name(&self, name: &Arc<str>) -> Option<Arc<str>> {
626        if self.exclude.contains(name) {
627            return None;
628        }
629        if let Some(only) = &self.only
630            && !only.contains(name)
631        {
632            return None;
633        }
634        Some(
635            self.rename
636                .get(name)
637                .cloned()
638                .unwrap_or_else(|| name.clone()),
639        )
640    }
641}
642
643/// A Clojure namespace with intern table, refers, and aliases.
644#[derive(Debug)]
645pub struct Namespace {
646    pub name: Arc<str>,
647    /// Vars interned directly in this namespace.
648    pub interns: TrackedMutex<HashMap<Arc<str>, GcPtr<Var>>>,
649    /// Vars referred from other namespaces (e.g. clojure.core).
650    pub refers: TrackedMutex<HashMap<Arc<str>, GcPtr<Var>>>,
651    /// Namespace aliases: short-name → full namespace name.
652    pub aliases: TrackedMutex<HashMap<Arc<str>, Arc<str>>>,
653    /// Absolute path of the source file this namespace was loaded from,
654    /// populated by the loader.  Used by the versioned resolver to locate the
655    /// file for `git show`.
656    pub source_file: TrackedMutex<Option<Arc<str>>>,
657    /// Absolute path of the git repository root that contains `source_file`.
658    pub git_repo_root: TrackedMutex<Option<Arc<str>>>,
659    /// `true` for namespaces loaded from a specific commit (`name@hash`).
660    /// Versioned namespaces are immutable: `intern()` will refuse new bindings.
661    pub is_versioned: bool,
662    /// Metadata attached via `(ns ^{...} name ...)` or an `ns` attr-map.
663    pub meta: TrackedMutex<Option<Value>>,
664    /// Narrowing applied to the automatic `clojure.core` refer, set by an
665    /// `(:refer-clojure ...)` clause.  `None` refers all of core.
666    pub refer_clojure_filter: TrackedMutex<Option<ReferClojureFilter>>,
667}
668
669impl Namespace {
670    pub fn new(name: impl Into<Arc<str>>) -> Self {
671        Self {
672            name: name.into(),
673            interns: TrackedMutex::new(HashMap::new()),
674            refers: TrackedMutex::new(HashMap::new()),
675            aliases: TrackedMutex::new(HashMap::new()),
676            source_file: TrackedMutex::new(None),
677            git_repo_root: TrackedMutex::new(None),
678            is_versioned: false,
679            meta: TrackedMutex::new(None),
680            refer_clojure_filter: TrackedMutex::new(None),
681        }
682    }
683
684    /// Create a versioned (immutable) namespace for `name@commit`.
685    pub fn new_versioned(name: impl Into<Arc<str>>) -> Self {
686        Self {
687            is_versioned: true,
688            ..Self::new(name)
689        }
690    }
691
692    /// Record the source file path and its git repo root (if in a repo).
693    pub fn set_source_location(&self, file: &str, repo_root: Option<&str>) {
694        *self.source_file.lock().unwrap() = Some(Arc::from(file));
695        *self.git_repo_root.lock().unwrap() = repo_root.map(Arc::from);
696    }
697
698    pub fn get_meta(&self) -> Option<Value> {
699        self.meta.lock().unwrap().clone()
700    }
701
702    pub fn set_meta(&self, m: Value) {
703        *self.meta.lock().unwrap() = Some(m);
704    }
705}
706
707impl cljrs_gc::Trace for Namespace {
708    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
709        use cljrs_gc::GcVisitor as _;
710        {
711            let interns = self.interns.lock().unwrap();
712            for var in interns.values() {
713                visitor.visit(var);
714            }
715        }
716        {
717            let refers = self.refers.lock().unwrap();
718            for var in refers.values() {
719                visitor.visit(var);
720            }
721        }
722        {
723            let meta = self.meta.lock().unwrap();
724            if let Some(m) = meta.as_ref() {
725                m.trace(visitor);
726            }
727        }
728    }
729}
730
731// ── NativeFn ──────────────────────────────────────────────────────────────────
732
733/// A Rust function callable from Clojure.
734/// Legacy type alias kept for source compatibility. Bare `fn` pointers
735/// implement `Fn` and can be passed anywhere a `NativeFnFunc` is expected.
736pub type NativeFnPtr = fn(&[Value]) -> crate::error::ValueResult<Value>;
737
738/// The callable stored inside a `NativeFn`. Supports both bare function
739/// pointers and closures that capture state.
740pub type NativeFnFunc = Arc<dyn Fn(&[Value]) -> crate::error::ValueResult<Value>>;
741
742#[derive(Clone, Debug)]
743pub enum Arity {
744    Fixed(usize),
745    Variadic { min: usize },
746}
747
748pub struct NativeFn {
749    pub name: Arc<str>,
750    pub arity: Arity,
751    pub func: NativeFnFunc,
752}
753
754impl NativeFn {
755    /// Create from a bare function pointer (backwards-compatible).
756    pub fn new(name: impl Into<Arc<str>>, arity: Arity, func: NativeFnPtr) -> Self {
757        Self {
758            name: name.into(),
759            arity,
760            func: Arc::new(func),
761        }
762    }
763
764    /// Create from a closure or any `Fn(&[Value]) -> ValueResult<Value>`.
765    pub fn with_closure(
766        name: impl Into<Arc<str>>,
767        arity: Arity,
768        func: impl Fn(&[Value]) -> crate::error::ValueResult<Value> + 'static,
769    ) -> Self {
770        Self {
771            name: name.into(),
772            arity,
773            func: Arc::new(func),
774        }
775    }
776}
777
778impl std::fmt::Debug for NativeFn {
779    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
780        f.debug_struct("NativeFn")
781            .field("name", &self.name)
782            .field("arity", &self.arity)
783            .field("func", &"<fn>")
784            .finish()
785    }
786}
787
788impl cljrs_gc::Trace for NativeFn {
789    fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
790}
791
792// ── CljxFnArity ───────────────────────────────────────────────────────────────
793
794/// One arity branch of a Clojure function.
795#[derive(Debug, Clone)]
796pub struct CljxFnArity {
797    /// Simple parameter names (no `&`).
798    /// For destructured params, these are gensym'd names.
799    pub params: Vec<Arc<str>>,
800    /// The name after `&`, if any.
801    pub rest_param: Option<Arc<str>>,
802    /// The body forms for this arity.
803    pub body: Vec<Form>,
804    /// Destructuring patterns: (param_index, original_form).
805    /// After binding the gensym'd param, these patterns are applied
806    /// via `bind_pattern` to destructure the value.
807    pub destructure_params: Vec<(usize, Form)>,
808    /// If the rest param is destructured, the original form.
809    pub destructure_rest: Option<Form>,
810    /// Unique ID for IR cache lookup (assigned by the evaluator).
811    pub ir_arity_id: u64,
812    /// Optional primitive type hint per positional parameter (parallel to
813    /// `params`).  `^long x` → `Some(TypeHint::Long)`; an un-hinted or
814    /// non-primitive-tagged param → `None`.  Drives unboxed codegen.
815    pub param_hints: Vec<Option<TypeHint>>,
816    /// Primitive type hint on the rest param, if any (rarely useful, but parsed
817    /// for symmetry).
818    pub rest_hint: Option<TypeHint>,
819}
820
821impl CljxFnArity {
822    /// Heap bytes owned by this arity, not counting the `CljxFnArity` struct itself.
823    pub fn heap_size(&self) -> usize {
824        // params Vec buffer (Arc<str> pointers; the str data is shared, skip it)
825        self.params.capacity() * mem::size_of::<Arc<str>>()
826        // body: the dominant consumer — Form AST trees stored inline
827        + self.body.capacity() * mem::size_of::<Form>()
828        + self.body.iter().map(|f| f.heap_size()).sum::<usize>()
829        // destructure_params
830        + self.destructure_params.capacity() * mem::size_of::<(usize, Form)>()
831        + self.destructure_params.iter().map(|(_, f)| f.heap_size()).sum::<usize>()
832        // destructure_rest
833        + self.destructure_rest.as_ref()
834            .map_or(0, |f| mem::size_of::<Form>() + f.heap_size())
835        // param_hints (Copy elements, no nested heap)
836        + self.param_hints.capacity() * mem::size_of::<Option<TypeHint>>()
837    }
838}
839
840// ── CljxFn ────────────────────────────────────────────────────────────────────
841
842/// An interpreted Clojure closure with captured environment.
843#[derive(Debug, Clone)]
844pub struct CljxFn {
845    pub name: Option<Arc<str>>,
846    pub arities: Vec<CljxFnArity>,
847    /// Names of closed-over bindings (parallel to `closed_over_vals`).
848    pub closed_over_names: Vec<Arc<str>>,
849    /// Values of closed-over bindings (parallel to `closed_over_names`).
850    pub closed_over_vals: Vec<Value>,
851    /// True if this function was defined with `defmacro`.
852    pub is_macro: bool,
853    /// True if this function carries `^:async` metadata. When an async runtime
854    /// (`cljrs-async`) is registered, calling such a function spawns its body as
855    /// a task and returns a `Value::Future` immediately instead of running it
856    /// synchronously. Without a runtime it runs synchronously like any other fn.
857    pub is_async: bool,
858    /// Namespace in which this function was defined (for macro hygiene).
859    pub defining_ns: Arc<str>,
860    /// Back-pointer to the `GcPtr` that owns this `CljxFn`, set immediately
861    /// after allocation so that a named anonymous function's self-reference
862    /// (e.g. `(fn g [] g)`) returns the *identical* pointer to the caller,
863    /// preserving pointer-equality semantics (`(= f (f))` → `true`).
864    pub self_ptr: Option<GcPtr<CljxFn>>,
865}
866
867impl CljxFn {
868    pub fn new(
869        name: Option<Arc<str>>,
870        arities: Vec<CljxFnArity>,
871        closed_over_names: Vec<Arc<str>>,
872        closed_over_vals: Vec<Value>,
873        is_macro: bool,
874        defining_ns: Arc<str>,
875    ) -> Self {
876        Self {
877            name,
878            arities,
879            closed_over_names,
880            closed_over_vals,
881            is_macro,
882            is_async: false,
883            defining_ns,
884            self_ptr: None,
885        }
886    }
887}
888
889impl cljrs_gc::Trace for CljxFn {
890    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
891        use cljrs_gc::GcVisitor as _;
892        for v in &self.closed_over_vals {
893            v.trace(visitor);
894        }
895        if let Some(ref p) = self.self_ptr {
896            visitor.visit(p);
897        }
898    }
899
900    fn gc_size_extra(&self) -> usize {
901        // Vec<CljxFnArity> buffer + each arity's inline-owned heap
902        self.arities.capacity() * mem::size_of::<CljxFnArity>()
903            + self
904                .arities
905                .iter()
906                .map(CljxFnArity::heap_size)
907                .sum::<usize>()
908    }
909}
910
911// ── BoundFn ──────────────────────────────────────────────────────────────────
912
913/// A function wrapped with captured dynamic bindings.
914/// When called, the captured bindings are pushed as a frame before delegating
915/// to the wrapped function. This means captured bindings override the caller's
916/// for the same var, but vars not in the capture fall through normally.
917#[derive(Debug)]
918pub struct BoundFn {
919    /// The wrapped callable.
920    pub wrapped: Value,
921    /// Captured dynamic bindings (merged flat frame; opaque to cljrs-value).
922    pub captured_bindings: HashMap<usize, Value>,
923}
924
925impl cljrs_gc::Trace for BoundFn {
926    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
927        self.wrapped.trace(visitor);
928        for val in self.captured_bindings.values() {
929            val.trace(visitor);
930        }
931    }
932
933    fn gc_size_extra(&self) -> usize {
934        // HashMap<usize, Value>: hashbrown open-addressing, ~1 control byte + entry per slot.
935        self.captured_bindings.capacity() * (1 + mem::size_of::<usize>() + mem::size_of::<Value>())
936    }
937}
938
939// ── Thunk / LazySeq ───────────────────────────────────────────────────────────
940
941/// A deferred computation that produces a `Value` when forced.
942pub trait Thunk: std::fmt::Debug + cljrs_gc::Trace {
943    fn force(&self) -> Result<Value, String>;
944}
945
946/// Internal state of a lazy sequence cell.
947pub enum LazySeqState {
948    /// Thunk not yet evaluated.
949    Pending(Box<dyn Thunk>),
950    /// Result cached after first force.
951    Forced(Value),
952    /// Thunk evaluation failed; error message is cached.
953    Error(String),
954}
955
956/// A lazy sequence that forces its thunk exactly once and caches the result.
957pub struct LazySeq {
958    pub state: TrackedMutex<LazySeqState>,
959}
960
961impl LazySeq {
962    pub fn new(thunk: Box<dyn Thunk>) -> Self {
963        Self {
964            state: TrackedMutex::new(LazySeqState::Pending(thunk)),
965        }
966    }
967
968    /// Realize the sequence: force the thunk on first call, return cached value on subsequent calls.
969    /// On error, returns `Value::Nil` and caches the error (retrievable via `error()`).
970    pub fn realize(&self) -> Value {
971        let thunk = {
972            let mut guard = self.state.lock().unwrap();
973            match &*guard {
974                LazySeqState::Forced(v) => return v.clone(),
975                LazySeqState::Error(_) => return Value::Nil,
976                LazySeqState::Pending(_) => {}
977            }
978            // Replace the pending state with a temporary Forced(Nil), extract the thunk.
979            let prev = mem::replace(&mut *guard, LazySeqState::Forced(Value::Nil));
980            let LazySeqState::Pending(thunk) = prev else {
981                unreachable!("state was not Pending")
982            };
983            thunk
984            // guard dropped here — lock released before forcing
985        };
986        // Force the thunk WITHOUT holding the lock. This ensures GC's
987        // lock().unwrap() in LazySeq::trace() will not deadlock.
988        match thunk.force() {
989            Ok(result) => {
990                *self.state.lock().unwrap() = LazySeqState::Forced(result.clone());
991                result
992            }
993            Err(msg) => {
994                *self.state.lock().unwrap() = LazySeqState::Error(msg);
995                Value::Nil
996            }
997        }
998    }
999
1000    /// Return the cached error message, if the thunk failed.
1001    pub fn error(&self) -> Option<String> {
1002        let guard = self.state.lock().unwrap();
1003        if let LazySeqState::Error(e) = &*guard {
1004            Some(e.clone())
1005        } else {
1006            None
1007        }
1008    }
1009}
1010
1011impl std::fmt::Debug for LazySeq {
1012    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1013        write!(f, "LazySeq(...)")
1014    }
1015}
1016
1017impl cljrs_gc::Trace for LazySeq {
1018    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1019        // Safe to lock unconditionally: realize() drops the lock before entering
1020        // eval (thunk.force()), so the lock is never held across a GC safepoint.
1021        {
1022            let state = self.state.lock().unwrap();
1023            match &*state {
1024                LazySeqState::Pending(thunk) => thunk.trace(visitor),
1025                LazySeqState::Forced(v) => v.trace(visitor),
1026                LazySeqState::Error(_) => {}
1027            }
1028        }
1029    }
1030}
1031
1032// ── CljxCons ──────────────────────────────────────────────────────────────────
1033
1034/// A lazy cons cell: head element + tail (may be a `LazySeq`, `List`, or `Nil`).
1035///
1036/// Used when `cons` is called with a `LazySeq` or `Cons` tail, enabling lazy
1037/// sequences without eagerly realizing them.
1038#[derive(Debug, Clone)]
1039pub struct CljxCons {
1040    pub head: Value,
1041    pub tail: Value,
1042}
1043
1044impl cljrs_gc::Trace for CljxCons {
1045    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1046        self.head.trace(visitor);
1047        self.tail.trace(visitor);
1048    }
1049}
1050
1051// ── Volatile ──────────────────────────────────────────────────────────────────
1052
1053/// Non-atomic mutable cell (single-thread performance, no CAS).
1054pub struct Volatile {
1055    pub value: TrackedMutex<Value>,
1056}
1057
1058impl Volatile {
1059    pub fn new(v: Value) -> Self {
1060        // GC builds: heap-promotion fallback (see `Var::bind`).
1061        let v = crate::publish::publish_value(v);
1062        Self {
1063            value: TrackedMutex::new(v),
1064        }
1065    }
1066
1067    pub fn deref(&self) -> Value {
1068        self.value.lock().unwrap().clone()
1069    }
1070
1071    pub fn reset(&self, v: Value) -> Value {
1072        // In no-gc debug builds: assert the new value came from the StaticArena.
1073        #[cfg(all(feature = "no-gc", debug_assertions))]
1074        debug_assert!(
1075            value_gcptr_is_static(&v),
1076            "no-gc: Volatile::reset() received a region-local value — ensure the \
1077             new-value expression is inside a StaticCtxGuard (vreset! handles this)"
1078        );
1079        // GC builds: heap-promotion fallback (see `Var::bind`).
1080        let v = crate::publish::publish_value(v);
1081        *self.value.lock().unwrap() = v.clone();
1082        v
1083    }
1084}
1085
1086impl std::fmt::Debug for Volatile {
1087    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1088        write!(f, "Volatile")
1089    }
1090}
1091
1092impl cljrs_gc::Trace for Volatile {
1093    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1094        {
1095            let value = self.value.lock().unwrap();
1096            value.trace(visitor);
1097        }
1098    }
1099}
1100
1101// ── Delay ─────────────────────────────────────────────────────────────────────
1102
1103/// Internal state of a delay cell.
1104pub enum DelayState {
1105    Pending(Box<dyn Thunk>),
1106    Forced(Value),
1107}
1108
1109/// A lazy one-time computation (forced at most once, result cached).
1110pub struct Delay {
1111    pub state: TrackedMutex<DelayState>,
1112}
1113
1114impl Delay {
1115    pub fn new(thunk: Box<dyn Thunk>) -> Self {
1116        Self {
1117            state: TrackedMutex::new(DelayState::Pending(thunk)),
1118        }
1119    }
1120
1121    /// Force the delay and cache the result.
1122    /// Returns the value on success, or an error message on failure.
1123    pub fn force(&self) -> Result<Value, String> {
1124        let thunk = {
1125            let mut guard = self.state.lock().unwrap();
1126            if let DelayState::Forced(v) = &*guard {
1127                return Ok(v.clone());
1128            }
1129            let prev = mem::replace(&mut *guard, DelayState::Forced(Value::Nil));
1130            let DelayState::Pending(thunk) = prev else {
1131                unreachable!("state was not Pending")
1132            };
1133            thunk
1134            // guard dropped here — lock released before forcing
1135        };
1136        // Force the thunk WITHOUT holding the lock so GC's lock().unwrap() in
1137        // Delay::trace() will not deadlock.
1138        let result = thunk.force()?;
1139        *self.state.lock().unwrap() = DelayState::Forced(result.clone());
1140        Ok(result)
1141    }
1142
1143    /// True if the delay has already been forced.
1144    pub fn is_realized(&self) -> bool {
1145        matches!(&*self.state.lock().unwrap(), DelayState::Forced(_))
1146    }
1147}
1148
1149impl std::fmt::Debug for Delay {
1150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1151        write!(f, "Delay")
1152    }
1153}
1154
1155impl cljrs_gc::Trace for Delay {
1156    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1157        // Safe to lock unconditionally: force() drops the lock before entering
1158        // eval (thunk.force()), so the lock is never held across a GC safepoint.
1159        {
1160            let state = self.state.lock().unwrap();
1161            match &*state {
1162                DelayState::Pending(thunk) => thunk.trace(visitor),
1163                DelayState::Forced(v) => v.trace(visitor),
1164            }
1165        }
1166    }
1167}
1168
1169// ── CljxPromise ───────────────────────────────────────────────────────────────
1170
1171/// A one-shot rendezvous (promise).
1172pub struct CljxPromise {
1173    pub value: Mutex<Option<Value>>,
1174    pub cond: Condvar,
1175}
1176
1177impl CljxPromise {
1178    pub fn new() -> Self {
1179        Self {
1180            value: Mutex::new(None),
1181            cond: Condvar::new(),
1182        }
1183    }
1184
1185    /// Deliver a value (no-op if already delivered).
1186    pub fn deliver(&self, v: Value) {
1187        // GC builds: heap-promotion fallback — the promise may outlive (and be
1188        // read from outside) any region scope active at delivery time.
1189        let v = crate::publish::publish_value(v);
1190        let mut guard = self.value.lock().unwrap();
1191        if guard.is_none() {
1192            *guard = Some(v);
1193            self.cond.notify_all();
1194        }
1195    }
1196
1197    /// Block until a value is available, then return it.
1198    pub fn deref_blocking(&self) -> Value {
1199        let mut guard = self.value.lock().unwrap();
1200        while guard.is_none() {
1201            guard = self.cond.wait(guard).unwrap();
1202        }
1203        guard.as_ref().unwrap().clone()
1204    }
1205
1206    /// True if already delivered.
1207    pub fn is_realized(&self) -> bool {
1208        self.value.lock().unwrap().is_some()
1209    }
1210}
1211
1212impl Default for CljxPromise {
1213    fn default() -> Self {
1214        Self::new()
1215    }
1216}
1217
1218impl std::fmt::Debug for CljxPromise {
1219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1220        write!(f, "Promise")
1221    }
1222}
1223
1224impl cljrs_gc::Trace for CljxPromise {
1225    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1226        {
1227            let value = self.value.lock().unwrap();
1228            if let Some(v) = value.as_ref() {
1229                v.trace(visitor);
1230            }
1231        }
1232    }
1233}
1234
1235// ── CljxFuture ────────────────────────────────────────────────────────────────
1236
1237/// Thread-pool future state.
1238pub enum FutureState {
1239    Running,
1240    Done(Value),
1241    /// The future's body threw. Holds the thrown Clojure value (a
1242    /// `Value::Error`) so `await`/`deref` can re-throw it with its
1243    /// `ex-data`/`ex-cause` intact, rather than a stringified message.
1244    Failed(Value),
1245    /// The future exhausted the evaluation budget. Kept distinct from a
1246    /// thrown value so user-level catch clauses cannot intercept it.
1247    GasExhausted,
1248    /// The future's body failed with a managed-memory limit fault (strict
1249    /// governor mode). Kept distinct for the same reason as `GasExhausted`.
1250    MemoryLimitExceeded(cljrs_gc::MemoryLimitExceeded),
1251    Cancelled,
1252}
1253
1254/// The message carried by the error raised when reading a cancelled future.
1255pub const FUTURE_CANCELLED_MSG: &str = "future was cancelled";
1256
1257/// A future value computed asynchronously on another thread.
1258pub struct CljxFuture {
1259    pub state: Mutex<FutureState>,
1260    pub cond: Condvar,
1261    /// Set once a consumer has read the settled result (via `await`/`deref`).
1262    /// Used to warn about a `Failed` future that is discarded without anyone
1263    /// ever observing its error (the fire-and-forget footgun).
1264    observed: std::sync::atomic::AtomicBool,
1265}
1266
1267impl CljxFuture {
1268    pub fn new() -> Self {
1269        Self {
1270            state: Mutex::new(FutureState::Running),
1271            cond: Condvar::new(),
1272            observed: std::sync::atomic::AtomicBool::new(false),
1273        }
1274    }
1275
1276    /// True if done, failed, or cancelled (not still running).
1277    pub fn is_done(&self) -> bool {
1278        !matches!(&*self.state.lock().unwrap(), FutureState::Running)
1279    }
1280
1281    /// True if explicitly cancelled.
1282    pub fn is_cancelled(&self) -> bool {
1283        matches!(&*self.state.lock().unwrap(), FutureState::Cancelled)
1284    }
1285
1286    /// Mark a still-running future cancelled, waking anyone waiting on it.
1287    ///
1288    /// Returns false when it had already settled. The task itself is not
1289    /// interrupted; what is cancelled is the result.
1290    pub fn cancel(&self) -> bool {
1291        let mut state = self.state.lock().unwrap();
1292        if matches!(&*state, FutureState::Running) {
1293            *state = FutureState::Cancelled;
1294            self.cond.notify_all();
1295            true
1296        } else {
1297            false
1298        }
1299    }
1300
1301    /// The catchable value every reader of a cancelled future raises.
1302    ///
1303    /// One shape for every path — the sync tree-walker's `await`/`deref`, the
1304    /// async tree-walker, and the compiled state machine all hand user code
1305    /// the same `ex-info`-like error, so a `catch` behaves the same whether or
1306    /// not the enclosing fn happened to be async-JIT-compiled.
1307    pub fn cancelled_error() -> Value {
1308        Value::Error(GcPtr::new(crate::ExceptionInfo::new(
1309            crate::ValueError::Other(FUTURE_CANCELLED_MSG.to_string()),
1310            FUTURE_CANCELLED_MSG.to_string(),
1311            None,
1312            None,
1313        )))
1314    }
1315
1316    /// Mark this future's result as observed. Call when a consumer reads the
1317    /// settled value (`await`/`deref`), so a later drop doesn't warn about an
1318    /// unobserved error.
1319    pub fn mark_observed(&self) {
1320        self.observed
1321            .store(true, std::sync::atomic::Ordering::Relaxed);
1322    }
1323}
1324
1325impl Drop for CljxFuture {
1326    fn drop(&mut self) {
1327        // Warn if a future failed but nobody ever observed the error — the
1328        // fire-and-forget case where a thrown error would otherwise vanish.
1329        // Tied to GC sweep timing: only fires once the future is unreachable,
1330        // so a not-yet-awaited (still reachable) failed future won't warn.
1331        //
1332        // SAFETY: this Drop can run during GC sweep. The thrown value held in
1333        // `Failed(v)` is itself a GC value whose backing box may be freed in
1334        // the *same* sweep, so we must NOT dereference it here (no `{v}`). We
1335        // only inspect the state discriminant, which is inline in our own
1336        // (still-valid) allocation.
1337        if !self.observed.load(std::sync::atomic::Ordering::Relaxed)
1338            && let Ok(state) = self.state.lock()
1339            && matches!(&*state, FutureState::Failed(_))
1340        {
1341            eprintln!(
1342                "[clojurust warning] a failed future was discarded without its error \
1343                 being observed (no await/deref); the thrown exception was lost"
1344            );
1345        }
1346    }
1347}
1348
1349impl Default for CljxFuture {
1350    fn default() -> Self {
1351        Self::new()
1352    }
1353}
1354
1355impl std::fmt::Debug for CljxFuture {
1356    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1357        write!(f, "Future")
1358    }
1359}
1360
1361impl cljrs_gc::Trace for CljxFuture {
1362    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1363        {
1364            let state = self.state.lock().unwrap();
1365            // Both Done and Failed hold a Value (the result or the thrown
1366            // error); trace either so the GC keeps it alive until observed.
1367            if let FutureState::Done(v) | FutureState::Failed(v) = &*state {
1368                v.trace(visitor);
1369            }
1370        }
1371    }
1372}
1373
1374// ── Agent ─────────────────────────────────────────────────────────────────────
1375
1376/// A Clojure agent — asynchronous state update queue (stub: not yet implemented).
1377pub struct Agent {
1378    /// Current state.
1379    pub state: Arc<Mutex<Value>>,
1380    /// Last error.
1381    pub error: Arc<Mutex<Option<Value>>>,
1382    pub watches: TrackedMutex<Vec<(Value, Value)>>,
1383}
1384
1385impl Agent {
1386    pub fn get_state(&self) -> Value {
1387        self.state.lock().unwrap().clone()
1388    }
1389
1390    pub fn get_error(&self) -> Option<Value> {
1391        self.error.lock().unwrap().clone()
1392    }
1393
1394    pub fn clear_error(&self) {
1395        *self.error.lock().unwrap() = None;
1396    }
1397}
1398
1399impl std::fmt::Debug for Agent {
1400    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1401        write!(f, "Agent")
1402    }
1403}
1404
1405impl cljrs_gc::Trace for Agent {
1406    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1407        {
1408            let state = self.state.lock().unwrap();
1409            state.trace(visitor);
1410        }
1411        {
1412            let error = self.error.lock().unwrap();
1413            if let Some(e) = error.as_ref() {
1414                e.trace(visitor);
1415            }
1416        }
1417        {
1418            let watches = self.watches.lock().unwrap();
1419            for (key, f) in watches.iter() {
1420                key.trace(visitor);
1421                f.trace(visitor);
1422            }
1423        }
1424    }
1425}
1426
1427// ── Tests ─────────────────────────────────────────────────────────────────────
1428
1429#[cfg(test)]
1430mod var_tests {
1431    use super::*;
1432    use crate::shared::SharedValue;
1433
1434    #[test]
1435    fn bind_promotable_mirrors_shared_root() {
1436        let var = Var::new("user", "x");
1437        assert!(var.shared_root.load().is_none());
1438        var.bind(Value::Long(7));
1439        assert!(matches!(
1440            var.shared_root.load().as_ref().as_ref(),
1441            Some(SharedValue::Long(7))
1442        ));
1443        assert_eq!(var.deref(), Some(Value::Long(7)));
1444        assert_eq!(var.deref_shared(), Some(Value::Long(7)));
1445    }
1446
1447    #[test]
1448    fn bind_nonpromotable_clears_shared_root() {
1449        let var = Var::new("user", "f");
1450        var.bind(Value::Long(1));
1451        assert!(var.shared_root.load().is_some());
1452        // Rebinding to a non-promotable value clears the mirror, but the
1453        // isolate-local fast path still holds it.
1454        let f = Value::NativeFunction(GcPtr::new(NativeFn::new("f", Arity::Fixed(0), |_| {
1455            Ok(Value::Nil)
1456        })));
1457        var.bind(f);
1458        assert!(var.shared_root.load().is_none());
1459        assert!(var.is_bound());
1460        assert_eq!(var.deref_shared(), None);
1461    }
1462
1463    #[test]
1464    fn from_shared_root_seeds_local_slot() {
1465        let src = Var::new("user", "y");
1466        src.bind(Value::Long(99));
1467        let recv = Var::from_shared_root("user", "y", false, src.shared_root.clone());
1468        assert_eq!(recv.deref(), Some(Value::Long(99)));
1469        // Same underlying cell.
1470        src.bind(Value::Long(100));
1471        assert_eq!(recv.deref_shared(), Some(Value::Long(100)));
1472    }
1473}