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