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 crate::parking::Event;
6use std::collections::HashMap;
7use std::mem;
8use std::rc::Rc;
9use std::sync::{Arc, Condvar, Mutex};
10use std::time::{Duration, Instant};
11
12use cljrs_gc::GcPtr;
13use cljrs_gc::mutator::TrackedMutex;
14use cljrs_reader::Form;
15
16use crate::TypeHint;
17use crate::Value;
18
19// ── Protocol ──────────────────────────────────────────────────────────────────
20
21/// Inner map type for protocol implementations: method_name → impl fn.
22pub type MethodMap = HashMap<Arc<str>, Value>;
23
24#[cfg(feature = "no-gc")]
25type ImplementationOwnership = HashMap<Arc<str>, HashMap<Arc<str>, crate::publish::ValueLease>>;
26
27/// A Clojure protocol — an interface-like construct with named methods.
28#[derive(Debug)]
29pub struct Protocol {
30    pub name: Arc<str>,
31    pub ns: Arc<str>,
32    pub methods: Vec<ProtocolMethod>,
33    /// type_tag → { method_name → impl fn }
34    pub impls: TrackedMutex<HashMap<Arc<str>, MethodMap>>,
35    #[cfg(feature = "no-gc")]
36    impl_ownership: std::cell::RefCell<ImplementationOwnership>,
37    /// `(defprotocol Name :extend-via-metadata true ...)` — when set, protocol
38    /// dispatch consults the dispatch value's metadata (keyed by this
39    /// protocol's `ProtocolFn`s) before falling back to type-tag impls.
40    pub extend_via_metadata: bool,
41}
42
43impl Protocol {
44    pub fn new(
45        name: Arc<str>,
46        ns: Arc<str>,
47        methods: Vec<ProtocolMethod>,
48        extend_via_metadata: bool,
49    ) -> Self {
50        Self {
51            name,
52            ns,
53            methods,
54            impls: TrackedMutex::new(HashMap::new()),
55            #[cfg(feature = "no-gc")]
56            impl_ownership: Default::default(),
57            extend_via_metadata,
58        }
59    }
60}
61
62impl Protocol {
63    /// Install an implementation and its replaceable ownership cache.
64    pub fn set_implementation(&self, tag: Arc<str>, method: Arc<str>, value: Value) {
65        #[cfg(feature = "no-gc")]
66        let (value, lease) = crate::publish::prepare_for_owner(
67            value,
68            &cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize),
69        );
70        #[cfg(not(feature = "no-gc"))]
71        let value = crate::publish::publish_value(value);
72        self.impls
73            .lock()
74            .unwrap()
75            .entry(tag.clone())
76            .or_default()
77            .insert(method.clone(), value);
78        #[cfg(feature = "no-gc")]
79        {
80            let previous = self
81                .impl_ownership
82                .borrow_mut()
83                .entry(tag)
84                .or_default()
85                .insert(method, lease);
86            drop(previous);
87        }
88    }
89    /// Borrow a cached implementation without allocating a composite key.
90    pub fn protect_implementation(&self, tag: &str, method: &str) {
91        #[cfg(feature = "no-gc")]
92        if let Some(lease) = self
93            .impl_ownership
94            .borrow()
95            .get(tag)
96            .and_then(|methods| methods.get(method))
97        {
98            lease.protect_read();
99        }
100        #[cfg(not(feature = "no-gc"))]
101        let _ = (tag, method);
102    }
103}
104
105/// Global protocol-extension generation, bumped on every `impls` mutation
106/// (`extend-type`, `extend-protocol`, `defrecord`/`reify` inline impls).
107///
108/// Inline caches for protocol dispatch (Phase 10.6, `rt_call_ic` in
109/// `cljrs-compiler`'s rt_abi) tag each cached `(dispatch type → impl fn)`
110/// entry with the generation observed at fill time; a later bump invalidates
111/// every cache entry at once, so re-extending a protocol mid-session is
112/// picked up on the next dispatch through any call site.
113static PROTOCOL_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
114
115/// Current protocol-extension generation (see [`bump_protocol_generation`]).
116pub fn protocol_generation() -> u64 {
117    PROTOCOL_GENERATION.load(std::sync::atomic::Ordering::Acquire)
118}
119
120/// Invalidate all protocol-dispatch inline caches.  Must be called after
121/// every mutation of any [`Protocol::impls`] map.
122pub fn bump_protocol_generation() {
123    PROTOCOL_GENERATION.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
124}
125
126impl Drop for Protocol {
127    fn drop(&mut self) {
128        crate::publish::clear_slots(self as *const _ as usize);
129    }
130}
131
132impl cljrs_gc::Trace for Protocol {
133    #[cfg(feature = "no-gc")]
134    fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {
135        // Mutable contents own their destination leases independently.
136    }
137    #[cfg(not(feature = "no-gc"))]
138    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
139        {
140            let impls = self.impls.lock().unwrap();
141            for method_map in impls.values() {
142                for v in method_map.values() {
143                    v.trace(visitor);
144                }
145            }
146        }
147    }
148}
149
150/// One method signature declared in a `defprotocol`.
151#[derive(Debug, Clone)]
152pub struct ProtocolMethod {
153    pub name: Arc<str>,
154    pub min_arity: usize,
155    pub variadic: bool,
156}
157
158impl cljrs_gc::Trace for ProtocolMethod {
159    fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
160}
161
162// ── ProtocolFn ────────────────────────────────────────────────────────────────
163
164/// Callable that dispatches a single protocol method on the type of `args[0]`.
165#[derive(Debug)]
166pub struct ProtocolFn {
167    pub protocol: GcPtr<Protocol>,
168    pub method_name: Arc<str>,
169    pub min_arity: usize,
170    pub variadic: bool,
171}
172
173impl cljrs_gc::Trace for ProtocolFn {
174    #[cfg(feature = "no-gc")]
175    fn immutable_children(&self) -> bool {
176        true
177    }
178    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
179        use cljrs_gc::GcVisitor as _;
180        visitor.visit(&self.protocol);
181    }
182}
183
184// ── MultiFn ───────────────────────────────────────────────────────────────────
185
186/// A Clojure multimethod — arbitrary dispatch via a user-supplied function.
187#[derive(Debug)]
188pub struct MultiFn {
189    pub name: Arc<str>,
190    pub dispatch_fn: Value,
191    /// pr_str(dispatch-val) → implementation fn
192    pub methods: TrackedMutex<HashMap<String, Value>>,
193    /// pr_str(dispatch-val) → the dispatch value itself, for `isa?` lookups
194    pub dispatch_vals: TrackedMutex<HashMap<String, Value>>,
195    /// recorded preferences: pr_str(preferred) → pr_str(over)
196    pub prefers: TrackedMutex<HashMap<String, Vec<String>>>,
197    /// pr_str(preference dispatch value) → original value, including values
198    /// which do not currently have a method.
199    pub preference_vals: TrackedMutex<HashMap<String, Value>>,
200    /// normally ":default"
201    pub default_dispatch: String,
202    method_generation: std::sync::atomic::AtomicU64,
203    method_cache: TrackedMutex<MultiFnMethodCache>,
204    #[cfg(feature = "no-gc")]
205    method_ownership: std::cell::RefCell<HashMap<String, crate::publish::ValueLease>>,
206}
207
208#[derive(Debug, Default)]
209struct MultiFnMethodCache {
210    hierarchy_generation: u64,
211    method_generation: u64,
212    methods: HashMap<String, String>,
213}
214
215impl MultiFn {
216    pub fn new(name: Arc<str>, dispatch_fn: Value, default_dispatch: String) -> Self {
217        Self {
218            name,
219            dispatch_fn,
220            methods: TrackedMutex::new(HashMap::new()),
221            dispatch_vals: TrackedMutex::new(HashMap::new()),
222            prefers: TrackedMutex::new(HashMap::new()),
223            preference_vals: TrackedMutex::new(HashMap::new()),
224            default_dispatch,
225            method_generation: std::sync::atomic::AtomicU64::new(0),
226            method_cache: TrackedMutex::new(MultiFnMethodCache::default()),
227            #[cfg(feature = "no-gc")]
228            method_ownership: Default::default(),
229        }
230    }
231
232    /// Read a method while borrowing its independently owned backing scopes.
233    pub fn method(&self, key: &str) -> Option<Value> {
234        let value = self.methods.lock().unwrap().get(key).cloned();
235        #[cfg(feature = "no-gc")]
236        if let Some(lease) = self.method_ownership.borrow().get(key) {
237            lease.protect_read();
238        }
239        value
240    }
241
242    /// Replace one method and its cached backing scopes.
243    pub fn set_method(&self, key: String, value: Value) {
244        #[cfg(feature = "no-gc")]
245        let (value, lease) = crate::publish::prepare_for_owner(
246            value,
247            &cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize),
248        );
249        #[cfg(not(feature = "no-gc"))]
250        let value = crate::publish::publish_value(value);
251        self.methods.lock().unwrap().insert(key.clone(), value);
252        #[cfg(feature = "no-gc")]
253        {
254            let previous = self.method_ownership.borrow_mut().insert(key, lease);
255            drop(previous);
256        }
257    }
258    /// Remove a method, releasing its cache after existing readers finish.
259    pub fn remove_method(&self, key: &str) -> bool {
260        let removed = self.methods.lock().unwrap().remove(key).is_some();
261        #[cfg(feature = "no-gc")]
262        {
263            let previous = self.method_ownership.borrow_mut().remove(key);
264            drop(previous);
265        }
266        removed
267    }
268
269    /// Current generation of this multimethod's method and preference tables.
270    pub fn method_generation(&self) -> u64 {
271        self.method_generation
272            .load(std::sync::atomic::Ordering::Acquire)
273    }
274
275    /// Invalidate this multimethod's cached inherited/default resolutions.
276    pub fn bump_method_generation(&self) {
277        self.method_generation
278            .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
279    }
280
281    /// Return a cached resolved method key when both source generations match.
282    /// Observing either new generation lazily discards stale entries at once.
283    pub fn cached_method(
284        &self,
285        dispatch_key: &str,
286        hierarchy_generation: u64,
287        method_generation: u64,
288    ) -> Option<String> {
289        let mut cache = self.method_cache.lock().unwrap();
290        if cache.hierarchy_generation != hierarchy_generation
291            || cache.method_generation != method_generation
292        {
293            cache.hierarchy_generation = hierarchy_generation;
294            cache.method_generation = method_generation;
295            cache.methods.clear();
296        }
297        cache.methods.get(dispatch_key).cloned()
298    }
299
300    /// Cache `dispatch_key -> method_key` under the source generations used to
301    /// resolve it. A concurrent mutation makes the entry stale, so the next
302    /// lookup discards it; the write itself never needs to be suppressed.
303    pub fn cache_method(
304        &self,
305        dispatch_key: String,
306        method_key: String,
307        hierarchy_generation: u64,
308        method_generation: u64,
309    ) {
310        let mut cache = self.method_cache.lock().unwrap();
311        if cache.hierarchy_generation != hierarchy_generation
312            || cache.method_generation != method_generation
313        {
314            cache.hierarchy_generation = hierarchy_generation;
315            cache.method_generation = method_generation;
316            cache.methods.clear();
317        }
318        cache.methods.insert(dispatch_key, method_key);
319    }
320
321    /// Number of entries in the current cache, exposed for regression tests.
322    pub fn cached_method_count(&self) -> usize {
323        self.method_cache.lock().unwrap().methods.len()
324    }
325}
326
327impl Drop for MultiFn {
328    fn drop(&mut self) {
329        crate::publish::clear_slots(self as *const _ as usize);
330    }
331}
332
333impl cljrs_gc::Trace for MultiFn {
334    #[cfg(feature = "no-gc")]
335    fn init_ownership(&self) {
336        let methods = self.methods.lock().unwrap().clone();
337        for (key, value) in methods {
338            self.set_method(key, value);
339        }
340        for (prefix, table) in [
341            ("dispatch", &self.dispatch_vals),
342            ("prefer", &self.preference_vals),
343        ] {
344            for (key, value) in table.lock().unwrap().iter() {
345                let lease = cljrs_gc::region::RegionLease::for_value(value);
346                cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize).set_slot(
347                    self as *const _ as usize,
348                    format!("{prefix}:{key}"),
349                    &lease,
350                );
351            }
352        }
353    }
354    #[cfg(feature = "no-gc")]
355    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
356        self.dispatch_fn.trace(visitor);
357    }
358    #[cfg(not(feature = "no-gc"))]
359    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
360        self.dispatch_fn.trace(visitor);
361        {
362            let methods = self.methods.lock().unwrap();
363            for v in methods.values() {
364                v.trace(visitor);
365            }
366        }
367        {
368            let dispatch_vals = self.dispatch_vals.lock().unwrap();
369            for v in dispatch_vals.values() {
370                v.trace(visitor);
371            }
372        }
373        {
374            let preference_vals = self.preference_vals.lock().unwrap();
375            for v in preference_vals.values() {
376                v.trace(visitor);
377            }
378        }
379    }
380}
381
382#[cfg(feature = "no-gc")]
383fn prepare_optional(
384    value: Option<Value>,
385    address: usize,
386) -> (Option<Value>, crate::publish::ValueLease) {
387    match value {
388        Some(value) => {
389            let (value, lease) = crate::publish::prepare_for_owner(
390                value,
391                &cljrs_gc::region::AllocationOwner::for_address(address),
392            );
393            (Some(value), lease)
394        }
395        None => (None, Default::default()),
396    }
397}
398fn edit_watches(
399    watches: &TrackedMutex<Vec<(Value, Value)>>,
400    added: Option<(Value, Value)>,
401    removed: Option<&Value>,
402) {
403    let mut watches = watches.lock().unwrap();
404    if let Some((key, callback)) = added {
405        if let Some(entry) = watches.iter_mut().find(|(k, _)| k == &key) {
406            entry.1 = callback;
407        } else {
408            watches.push((key, callback));
409        }
410    }
411    if let Some(key) = removed {
412        watches.retain(|(k, _)| k != key);
413    }
414}
415
416#[cfg(feature = "no-gc")]
417fn refresh_watch_lease(
418    watches: &TrackedMutex<Vec<(Value, Value)>>,
419    cached: &std::cell::RefCell<crate::publish::ValueLease>,
420    address: usize,
421) {
422    let values: Vec<Value> = watches
423        .lock()
424        .unwrap()
425        .iter()
426        .flat_map(|(key, callback)| [key.clone(), callback.clone()])
427        .collect();
428    let lease = crate::publish::ValueLease::for_values(
429        &values,
430        &cljrs_gc::region::AllocationOwner::for_address(address),
431    );
432    *cached.borrow_mut() = lease;
433}
434
435// ── Var ───────────────────────────────────────────────────────────────────────
436
437/// A Clojure var — a namespace-interned mutable root binding.
438///
439/// ## Two-tier root (Phase B3, issue #171)
440///
441/// A var's *root* binding uses the same two-tier mechanism as `shared-atom`:
442///
443/// - **`value`** — the isolate-local, GC-backed fast path.  Every var deref,
444///   the IR tier, and the JIT/AOT `rt_*` ABI read this slot; promotion never
445///   touches it, so compiled inline caches and pointer-identity assumptions
446///   baked into native code stay valid.
447/// - **`shared_root`** — a `Send + Sync` cross-isolate mirror,
448///   `Arc<ArcSwap<Option<SharedValue>>>`, reusing
449///   [`crate::shared::SharedValue`].  `bind` (i.e. `def` / `alter-var-root` /
450///   `set!`) *promotes-on-write*: if the new root value is promotable the cell
451///   holds `Some(SharedValue)`, otherwise it is cleared to `None` (option (b)
452///   of the ADR — non-promotable roots, e.g. closures, stay isolate-local).
453///
454/// The shared cell is what crosses the structured-clone boundary: a var
455/// `def`'d in one isolate is observable *by value* from another, with keyword
456/// /symbol identity preserved through the intern table.  See
457/// `crate::clone` for the serialize/deserialize seam.
458///
459/// Dynamic `binding` is unchanged — it is already thread-local / per-isolate
460/// and lives on the binding stack, not in the var root.
461#[derive(Debug)]
462pub struct Var {
463    #[cfg(feature = "no-gc")]
464    read_epoch: std::cell::Cell<u64>,
465    pub namespace: Arc<str>,
466    pub name: Arc<str>,
467    pub value: TrackedMutex<Option<Value>>,
468    #[cfg(feature = "no-gc")]
469    value_lease: std::cell::RefCell<crate::publish::ValueLease>,
470    /// Cross-isolate mirror of the root binding (Phase B3).  `None` when the
471    /// var is unbound or its current root is not promotable.
472    pub shared_root: Arc<arc_swap::ArcSwap<Option<crate::shared::SharedValue>>>,
473    pub is_macro: bool,
474    /// Metadata map (e.g. `{:dynamic true}`).
475    pub meta: TrackedMutex<Option<Value>>,
476    #[cfg(feature = "no-gc")]
477    meta_lease: std::cell::RefCell<crate::publish::ValueLease>,
478    #[cfg(feature = "no-gc")]
479    watch_lease: std::cell::RefCell<crate::publish::ValueLease>,
480    pub watches: TrackedMutex<Vec<(Value, Value)>>,
481    binding_generation: std::sync::atomic::AtomicU64,
482}
483
484impl Var {
485    pub fn new(namespace: impl Into<Arc<str>>, name: impl Into<Arc<str>>) -> Self {
486        Self {
487            namespace: namespace.into(),
488            name: name.into(),
489            value: TrackedMutex::new(None),
490            #[cfg(feature = "no-gc")]
491            read_epoch: Default::default(),
492            #[cfg(feature = "no-gc")]
493            value_lease: Default::default(),
494            shared_root: Arc::new(arc_swap::ArcSwap::new(Arc::new(None))),
495            is_macro: false,
496            meta: TrackedMutex::new(None),
497            #[cfg(feature = "no-gc")]
498            meta_lease: Default::default(),
499            #[cfg(feature = "no-gc")]
500            watch_lease: Default::default(),
501            watches: TrackedMutex::new(Vec::new()),
502            binding_generation: std::sync::atomic::AtomicU64::new(0),
503        }
504    }
505
506    /// Reconstruct a var on the receiving side of an isolate boundary.
507    ///
508    /// The `shared_root` cell is the *same* `Arc` as the sending isolate's, so
509    /// both isolates share the cross-isolate root cell.  The local `value`
510    /// fast-path slot is seeded by demoting the current shared snapshot, so an
511    /// immediate `deref` observes the value the var carried at crossing time.
512    pub fn from_shared_root(
513        namespace: impl Into<Arc<str>>,
514        name: impl Into<Arc<str>>,
515        is_macro: bool,
516        shared_root: Arc<arc_swap::ArcSwap<Option<crate::shared::SharedValue>>>,
517    ) -> Self {
518        let local = shared_root
519            .load()
520            .as_ref()
521            .as_ref()
522            .map(crate::shared::demote);
523        #[cfg(feature = "no-gc")]
524        let (local, lease) = match local {
525            Some(value) => {
526                let (value, lease) = crate::publish::prepare_definition(
527                    value,
528                    &cljrs_gc::region::AllocationOwner::current(),
529                );
530                (Some(value), lease)
531            }
532            None => (None, Default::default()),
533        };
534        Self {
535            namespace: namespace.into(),
536            name: name.into(),
537            value: TrackedMutex::new(local),
538            #[cfg(feature = "no-gc")]
539            read_epoch: Default::default(),
540            #[cfg(feature = "no-gc")]
541            value_lease: std::cell::RefCell::new(lease),
542            shared_root,
543            is_macro,
544            meta: TrackedMutex::new(None),
545            #[cfg(feature = "no-gc")]
546            meta_lease: Default::default(),
547            #[cfg(feature = "no-gc")]
548            watch_lease: Default::default(),
549            watches: TrackedMutex::new(Vec::new()),
550            binding_generation: std::sync::atomic::AtomicU64::new(0),
551        }
552    }
553
554    pub fn is_bound(&self) -> bool {
555        self.value.lock().unwrap().is_some()
556    }
557
558    pub fn snapshot(&self) -> Option<crate::publish::ValueSnapshot> {
559        self.value
560            .lock()
561            .unwrap()
562            .clone()
563            .map(|value| crate::publish::ValueSnapshot {
564                value,
565                #[cfg(feature = "no-gc")]
566                lease: self.value_lease.borrow().snapshot(),
567            })
568    }
569    pub fn deref(&self) -> Option<Value> {
570        let value = self.value.lock().unwrap().clone();
571        #[cfg(feature = "no-gc")]
572        {
573            let epoch = cljrs_gc::region::ReadScope::epoch();
574            if epoch == 0 || self.read_epoch.replace(epoch) != epoch {
575                self.value_lease.borrow().protect_read();
576            }
577        }
578        value
579    }
580
581    /// Generation of the isolate-local root binding, incremented after each
582    /// completed [`Var::bind`].
583    pub fn binding_generation(&self) -> u64 {
584        self.binding_generation
585            .load(std::sync::atomic::Ordering::Acquire)
586    }
587
588    /// Read the cross-isolate root by demoting the shared cell, ignoring the
589    /// isolate-local fast path.  Returns `None` when the shared root is empty
590    /// (unbound or non-promotable).  Used to observe writes another isolate
591    /// made through the shared cell.
592    pub fn deref_shared(&self) -> Option<Value> {
593        self.shared_root
594            .load()
595            .as_ref()
596            .as_ref()
597            .map(crate::shared::demote)
598    }
599
600    pub fn bind(&self, v: Value) {
601        #[cfg(feature = "no-gc")]
602        self.read_epoch.set(0);
603        #[cfg(not(feature = "no-gc"))]
604        let v = crate::publish::publish_value(v);
605        #[cfg(feature = "no-gc")]
606        let previous_lease = self.value_lease.borrow().snapshot();
607        #[cfg(feature = "no-gc")]
608        let (v, lease) = crate::publish::prepare_definition(
609            v,
610            &cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize),
611        );
612        // Replace the binding, holding the lock only across the swap.  The
613        // previous value (if any) is handed to the JIT rebind hook so it can
614        // reclaim native code compiled for a now-superseded definition
615        // (Phase 10.2 — code unloading).  `v.clone()` is O(1) for the only
616        // values that carry compiled code (`Value::Fn`, a `GcPtr` clone).
617        let prev = {
618            let mut slot = self.value.lock().unwrap();
619            slot.replace(v.clone())
620        };
621        #[cfg(feature = "no-gc")]
622        {
623            *self.value_lease.borrow_mut() = lease;
624        }
625        // Promote-on-`def` (Phase B3): mirror the new root into the
626        // cross-isolate cell when it is promotable, else clear the cell so it
627        // never advertises a stale or non-shareable root.  `def` is rare and
628        // global by nature, so this write-path cost is acceptable; the read
629        // path (and the JIT) never touch the shared cell.
630        let shared = crate::shared::promote(&v).ok();
631        self.shared_root.store(Arc::new(shared));
632        self.binding_generation
633            .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
634        if let Some(prev) = prev {
635            crate::jit_hooks::notify_var_rebind(&prev, &v);
636        }
637        #[cfg(feature = "no-gc")]
638        {
639            drop(previous_lease);
640        }
641    }
642
643    pub fn add_watch(&self, key: Value, callback: Value) {
644        edit_watches(&self.watches, Some((key, callback)), None);
645        #[cfg(feature = "no-gc")]
646        self.refresh_watch_lease();
647    }
648    pub fn remove_watch(&self, key: &Value) {
649        edit_watches(&self.watches, None, Some(key));
650        #[cfg(feature = "no-gc")]
651        self.refresh_watch_lease();
652    }
653    #[cfg(feature = "no-gc")]
654    fn refresh_watch_lease(&self) {
655        refresh_watch_lease(&self.watches, &self.watch_lease, self as *const _ as usize);
656    }
657    pub fn protect_watches(&self) {
658        #[cfg(feature = "no-gc")]
659        self.watch_lease.borrow().protect_read();
660    }
661
662    pub fn get_meta(&self) -> Option<Value> {
663        #[cfg(feature = "no-gc")]
664        self.meta_lease.borrow().protect_read();
665        self.meta.lock().unwrap().clone()
666    }
667
668    pub fn set_meta(&self, m: Value) {
669        #[cfg(not(feature = "no-gc"))]
670        let m = crate::publish::publish_for_owner(m, self as *const _ as usize);
671        #[cfg(feature = "no-gc")]
672        let (m, lease) = crate::publish::prepare_definition(
673            m,
674            &cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize),
675        );
676        *self.meta.lock().unwrap() = Some(m);
677        #[cfg(feature = "no-gc")]
678        {
679            *self.meta_lease.borrow_mut() = lease;
680        }
681    }
682
683    pub fn full_name(&self) -> String {
684        format!("{}/{}", self.namespace, self.name)
685    }
686}
687
688impl cljrs_gc::Trace for Var {
689    #[cfg(feature = "no-gc")]
690    fn init_ownership(&self) {
691        let owner = cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize);
692        let lease = self.value_lease.borrow().reparent(&owner);
693        let old = self.value_lease.replace(lease);
694        drop(old);
695    }
696
697    #[cfg(feature = "no-gc")]
698    fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
699    #[cfg(not(feature = "no-gc"))]
700    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
701        #[cfg(not(feature = "no-gc"))]
702        {
703            let value = self.value.lock().unwrap();
704            if let Some(v) = value.as_ref() {
705                v.trace(visitor);
706            }
707        }
708        {
709            let meta = self.meta.lock().unwrap();
710            if let Some(m) = meta.as_ref() {
711                m.trace(visitor);
712            }
713        }
714        {
715            let watches = self.watches.lock().unwrap();
716            for (key, f) in watches.iter() {
717                key.trace(visitor);
718                f.trace(visitor);
719            }
720        }
721    }
722}
723
724// ── Atom ──────────────────────────────────────────────────────────────────────
725
726/// A Clojure atom — a thread-safe mutable reference.
727#[derive(Debug)]
728pub struct Atom {
729    pub value: TrackedMutex<Value>,
730    #[cfg(feature = "no-gc")]
731    value_lease: std::cell::RefCell<crate::publish::ValueLease>,
732    pub meta: TrackedMutex<Option<Value>>,
733    #[cfg(feature = "no-gc")]
734    meta_lease: std::cell::RefCell<crate::publish::ValueLease>,
735    #[cfg(feature = "no-gc")]
736    watch_lease: std::cell::RefCell<crate::publish::ValueLease>,
737    pub validator: TrackedMutex<Option<Value>>,
738    #[cfg(feature = "no-gc")]
739    validator_lease: std::cell::RefCell<crate::publish::ValueLease>,
740    pub watches: TrackedMutex<Vec<(Value, Value)>>,
741}
742
743impl Atom {
744    pub fn new(v: Value) -> Self {
745        // Heap-promotion fallback (GC builds): an atom is program-lifetime
746        // shared state, so its initial value must not be region-allocated.
747        #[cfg(feature = "no-gc")]
748        let (v, lease) =
749            crate::publish::prepare_definition(v, &cljrs_gc::region::AllocationOwner::current());
750        #[cfg(not(feature = "no-gc"))]
751        let v = crate::publish::publish_value(v);
752        Self {
753            value: TrackedMutex::new(v),
754            #[cfg(feature = "no-gc")]
755            value_lease: std::cell::RefCell::new(lease),
756            meta: TrackedMutex::new(None),
757            #[cfg(feature = "no-gc")]
758            meta_lease: Default::default(),
759            #[cfg(feature = "no-gc")]
760            watch_lease: Default::default(),
761            validator: TrackedMutex::new(None),
762            #[cfg(feature = "no-gc")]
763            validator_lease: Default::default(),
764            watches: TrackedMutex::new(Vec::new()),
765        }
766    }
767
768    pub fn snapshot(&self) -> crate::publish::ValueSnapshot {
769        crate::publish::ValueSnapshot {
770            value: self.value.lock().unwrap().clone(),
771            #[cfg(feature = "no-gc")]
772            lease: self.value_lease.borrow().snapshot(),
773        }
774    }
775
776    pub fn deref(&self) -> Value {
777        let snapshot = self.snapshot();
778        snapshot.protect_current();
779        snapshot.value
780    }
781
782    pub fn reset(&self, v: Value) -> Value {
783        #[cfg(not(feature = "no-gc"))]
784        let v = crate::publish::publish_value(v);
785        #[cfg(feature = "no-gc")]
786        let (v, lease) = crate::publish::prepare_for_owner(
787            v,
788            &cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize),
789        );
790        #[cfg(feature = "no-gc")]
791        lease.publish_return();
792        *self.value.lock().unwrap() = v.clone();
793        #[cfg(feature = "no-gc")]
794        {
795            *self.value_lease.borrow_mut() = lease;
796        }
797        v
798    }
799
800    /// Replace the value only if it still equals `expected`.
801    /// Failed attempts do not publish or lease the candidate.
802    pub fn compare_and_set(&self, expected: &Value, value: Value) -> bool {
803        // The common vector update compares the exact snapshot just read.
804        // Avoid walking its shared elements for each accumulator update.
805        let matches_expected = |current: &Value| {
806            matches!((current, expected), (Value::Vector(a), Value::Vector(b)) if GcPtr::ptr_eq(a, b))
807                || current == expected
808        };
809        if !matches_expected(&self.value.lock().unwrap()) {
810            return false;
811        }
812        #[cfg(not(feature = "no-gc"))]
813        let value = crate::publish::publish_value(value);
814        #[cfg(feature = "no-gc")]
815        let (value, lease) = crate::publish::prepare_for_owner(
816            value,
817            &cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize),
818        );
819        let mut guard = self.value.lock().unwrap();
820        if !matches_expected(&guard) {
821            return false;
822        }
823        *guard = value;
824        drop(guard);
825        #[cfg(feature = "no-gc")]
826        {
827            *self.value_lease.borrow_mut() = lease;
828        }
829        true
830    }
831
832    pub fn add_watch(&self, key: Value, callback: Value) {
833        edit_watches(&self.watches, Some((key, callback)), None);
834        #[cfg(feature = "no-gc")]
835        self.refresh_watch_lease();
836    }
837    pub fn remove_watch(&self, key: &Value) {
838        edit_watches(&self.watches, None, Some(key));
839        #[cfg(feature = "no-gc")]
840        self.refresh_watch_lease();
841    }
842    #[cfg(feature = "no-gc")]
843    fn refresh_watch_lease(&self) {
844        refresh_watch_lease(&self.watches, &self.watch_lease, self as *const _ as usize);
845    }
846    pub fn protect_watches(&self) {
847        #[cfg(feature = "no-gc")]
848        self.watch_lease.borrow().protect_read();
849    }
850
851    pub fn get_meta(&self) -> Option<Value> {
852        #[cfg(feature = "no-gc")]
853        self.meta_lease.borrow().protect_read();
854        self.meta.lock().unwrap().clone()
855    }
856
857    pub fn set_meta(&self, m: Option<Value>) {
858        #[cfg(feature = "no-gc")]
859        let (m, lease) = prepare_optional(m, self as *const _ as usize);
860        *self.meta.lock().unwrap() = m;
861        #[cfg(feature = "no-gc")]
862        {
863            *self.meta_lease.borrow_mut() = lease;
864        }
865    }
866
867    pub fn get_validator(&self) -> Option<Value> {
868        #[cfg(feature = "no-gc")]
869        self.validator_lease.borrow().protect_read();
870        self.validator.lock().unwrap().clone()
871    }
872
873    pub fn set_validator(&self, vf: Option<Value>) {
874        #[cfg(feature = "no-gc")]
875        let (vf, lease) = prepare_optional(vf, self as *const _ as usize);
876        *self.validator.lock().unwrap() = vf;
877        #[cfg(feature = "no-gc")]
878        {
879            *self.validator_lease.borrow_mut() = lease;
880        }
881    }
882}
883
884impl cljrs_gc::Trace for Atom {
885    #[cfg(feature = "no-gc")]
886    fn init_ownership(&self) {
887        let owner = cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize);
888        let lease = self.value_lease.borrow().reparent(&owner);
889        let old = self.value_lease.replace(lease);
890        drop(old);
891    }
892    #[cfg(feature = "no-gc")]
893    fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
894    #[cfg(not(feature = "no-gc"))]
895    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
896        #[cfg(not(feature = "no-gc"))]
897        {
898            let value = self.value.lock().unwrap();
899            value.trace(visitor);
900        }
901        {
902            let meta = self.meta.lock().unwrap();
903            if let Some(m) = meta.as_ref() {
904                m.trace(visitor);
905            }
906        }
907        {
908            let validator = self.validator.lock().unwrap();
909            if let Some(vf) = validator.as_ref() {
910                vf.trace(visitor);
911            }
912        }
913        {
914            let watches = self.watches.lock().unwrap();
915            for (key, f) in watches.iter() {
916                key.trace(visitor);
917                f.trace(visitor);
918            }
919        }
920    }
921}
922
923// ── Namespace ─────────────────────────────────────────────────────────────────
924
925/// Which `clojure.core` names a namespace auto-refers, as narrowed by an
926/// `(:refer-clojure ...)` clause in `ns`.  An absent filter (the default)
927/// refers every public core name.
928#[derive(Debug, Clone, Default)]
929pub struct ReferClojureFilter {
930    /// `:only` — when set, no core name outside this set is referred.
931    pub only: Option<std::collections::HashSet<Arc<str>>>,
932    /// `:exclude` — core names that are never referred.
933    pub exclude: std::collections::HashSet<Arc<str>>,
934    /// `:rename` — core name → the local name it is referred under.  A renamed
935    /// name is *not* also referred under its original name (matching
936    /// `clojure.core/refer`).
937    pub rename: HashMap<Arc<str>, Arc<str>>,
938}
939
940impl ReferClojureFilter {
941    /// The local name `name` is referred under, or `None` when this filter
942    /// drops it.
943    pub fn local_name(&self, name: &Arc<str>) -> Option<Arc<str>> {
944        if self.exclude.contains(name) {
945            return None;
946        }
947        if let Some(only) = &self.only
948            && !only.contains(name)
949        {
950            return None;
951        }
952        Some(
953            self.rename
954                .get(name)
955                .cloned()
956                .unwrap_or_else(|| name.clone()),
957        )
958    }
959}
960
961/// A Clojure namespace with intern table, refers, and aliases.
962#[derive(Debug)]
963pub struct Namespace {
964    pub name: Arc<str>,
965    /// Vars interned directly in this namespace.
966    pub interns: TrackedMutex<HashMap<Arc<str>, GcPtr<Var>>>,
967    /// Vars referred from other namespaces (e.g. clojure.core).
968    pub refers: TrackedMutex<HashMap<Arc<str>, GcPtr<Var>>>,
969    /// Namespace aliases: short-name → full namespace name.
970    pub aliases: TrackedMutex<HashMap<Arc<str>, Arc<str>>>,
971    /// Absolute path of the source file this namespace was loaded from,
972    /// populated by the loader.  Used by the versioned resolver to locate the
973    /// file for `git show`.
974    pub source_file: TrackedMutex<Option<Arc<str>>>,
975    /// Absolute path of the git repository root that contains `source_file`.
976    pub git_repo_root: TrackedMutex<Option<Arc<str>>>,
977    /// `true` for namespaces loaded from a specific commit (`name@hash`).
978    /// Versioned namespaces are immutable: `intern()` will refuse new bindings.
979    pub is_versioned: bool,
980    /// Metadata attached via `(ns ^{...} name ...)` or an `ns` attr-map.
981    pub meta: TrackedMutex<Option<Value>>,
982    /// Narrowing applied to the automatic `clojure.core` refer, set by an
983    /// `(:refer-clojure ...)` clause.  `None` refers all of core.
984    pub refer_clojure_filter: TrackedMutex<Option<ReferClojureFilter>>,
985}
986
987impl Namespace {
988    pub fn new(name: impl Into<Arc<str>>) -> Self {
989        Self {
990            name: name.into(),
991            interns: TrackedMutex::new(HashMap::new()),
992            refers: TrackedMutex::new(HashMap::new()),
993            aliases: TrackedMutex::new(HashMap::new()),
994            source_file: TrackedMutex::new(None),
995            git_repo_root: TrackedMutex::new(None),
996            is_versioned: false,
997            meta: TrackedMutex::new(None),
998            refer_clojure_filter: TrackedMutex::new(None),
999        }
1000    }
1001
1002    /// Create a versioned (immutable) namespace for `name@commit`.
1003    pub fn new_versioned(name: impl Into<Arc<str>>) -> Self {
1004        let mut namespace = Self::new(name);
1005        namespace.is_versioned = true;
1006        namespace
1007    }
1008
1009    /// Record the source file path and its git repo root (if in a repo).
1010    pub fn set_source_location(&self, file: &str, repo_root: Option<&str>) {
1011        *self.source_file.lock().unwrap() = Some(Arc::from(file));
1012        *self.git_repo_root.lock().unwrap() = repo_root.map(Arc::from);
1013    }
1014
1015    pub fn get_meta(&self) -> Option<Value> {
1016        crate::publish::read_slot(self as *const _ as usize, 0);
1017        self.meta.lock().unwrap().clone()
1018    }
1019
1020    pub fn set_meta(&self, m: Value) {
1021        let m = crate::publish::publish_for_owner(m, self as *const _ as usize);
1022        *self.meta.lock().unwrap() = Some(m);
1023    }
1024}
1025
1026impl Drop for Namespace {
1027    fn drop(&mut self) {
1028        crate::publish::clear_slots(self as *const _ as usize);
1029    }
1030}
1031
1032impl cljrs_gc::Trace for Namespace {
1033    #[cfg(feature = "no-gc")]
1034    fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {
1035        // Mutable contents own their destination leases independently.
1036    }
1037    #[cfg(not(feature = "no-gc"))]
1038    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1039        use cljrs_gc::GcVisitor as _;
1040        {
1041            let interns = self.interns.lock().unwrap();
1042            for var in interns.values() {
1043                visitor.visit(var);
1044            }
1045        }
1046        {
1047            let refers = self.refers.lock().unwrap();
1048            for var in refers.values() {
1049                visitor.visit(var);
1050            }
1051        }
1052        {
1053            let meta = self.meta.lock().unwrap();
1054            if let Some(m) = meta.as_ref() {
1055                m.trace(visitor);
1056            }
1057        }
1058    }
1059}
1060
1061// ── NativeFn ──────────────────────────────────────────────────────────────────
1062
1063/// A Rust function callable from Clojure.
1064/// Legacy type alias kept for source compatibility. Bare `fn` pointers
1065/// implement `Fn` and can be passed anywhere a `NativeFnFunc` is expected.
1066pub type NativeFnPtr = fn(&[Value]) -> crate::error::ValueResult<Value>;
1067
1068/// The callable stored inside a `NativeFn`. Supports both bare function
1069/// pointers and closures that capture state.
1070pub type NativeFnFunc = Arc<dyn Fn(&[Value]) -> crate::error::ValueResult<Value>>;
1071
1072#[derive(Clone, Debug)]
1073pub enum Arity {
1074    Fixed(usize),
1075    Variadic { min: usize },
1076}
1077
1078pub struct NativeFn {
1079    pub name: Arc<str>,
1080    /// Lazily assigned watchdog name id (`Default::default()` when building).
1081    pub diagnostic_id: cljrs_gc::fiber::NativeDiagnosticId,
1082    pub arity: Arity,
1083    pub func: NativeFnFunc,
1084    /// Managed values retained by the opaque callable closure.
1085    pub captures: Arc<[Value]>,
1086    /// Optional adapter that marshals owned inputs and offloads native work.
1087    pub blocking_func: Option<NativeFnFunc>,
1088}
1089
1090impl NativeFn {
1091    /// Create from a bare function pointer (backwards-compatible).
1092    pub fn new(name: impl Into<Arc<str>>, arity: Arity, func: NativeFnPtr) -> Self {
1093        Self {
1094            name: name.into(),
1095            arity,
1096            blocking_func: None,
1097            captures: Default::default(),
1098            diagnostic_id: Default::default(),
1099            func: Arc::new(move |args| {
1100                crate::parking::catch_cancelled(|| {
1101                    crate::realization::catch(|| func(args))
1102                        .unwrap_or_else(|message| Err(crate::ValueError::Other(message)))
1103                })?
1104            }),
1105        }
1106    }
1107
1108    /// Create a Rust callable with no managed captures. For closures that
1109    /// retain Values, use with_captures so the collector can visit them.
1110    pub fn with_closure(
1111        name: impl Into<Arc<str>>,
1112        arity: Arity,
1113        func: impl Fn(&[Value]) -> crate::error::ValueResult<Value> + 'static,
1114    ) -> Self {
1115        Self {
1116            name: name.into(),
1117            arity,
1118            blocking_func: None,
1119            captures: Default::default(),
1120            diagnostic_id: Default::default(),
1121            func: Arc::new(move |args| {
1122                crate::parking::catch_cancelled(|| {
1123                    crate::realization::catch(|| func(args))
1124                        .unwrap_or_else(|message| Err(crate::ValueError::Other(message)))
1125                })?
1126            }),
1127        }
1128    }
1129
1130    /// Create a callable with explicit managed captures for precise tracing.
1131    /// The callback and tracer share one immutable capture slice.
1132    /// Callers must not replace the public `captures` handle after construction.
1133    pub fn with_captures(
1134        name: impl Into<Arc<str>>,
1135        arity: Arity,
1136        captures: Vec<Value>,
1137        func: impl Fn(&[Value], &[Value]) -> crate::ValueResult<Value> + 'static,
1138    ) -> Self {
1139        let captures: Arc<[Value]> = captures.into();
1140        let shared = captures.clone();
1141        let mut native = Self::with_closure(name, arity, move |args| func(&shared, args));
1142        native.captures = captures;
1143        native
1144    }
1145
1146    /// Create a native function with a separate owned-work blocking adapter.
1147    /// The adapter must keep managed conversions on the caller's isolate.
1148    pub fn with_blocking(
1149        name: impl Into<Arc<str>>,
1150        arity: Arity,
1151        func: impl Fn(&[Value]) -> crate::ValueResult<Value> + 'static,
1152        blocking_func: impl Fn(&[Value]) -> crate::ValueResult<Value> + 'static,
1153    ) -> Self {
1154        let mut native = Self::with_closure(name, arity, func);
1155        native.blocking_func = Some(Arc::new(move |args| {
1156            crate::parking::catch_cancelled(|| {
1157                crate::realization::catch(|| blocking_func(args))
1158                    .unwrap_or_else(|message| Err(crate::ValueError::Other(message)))
1159            })?
1160        }));
1161        native
1162    }
1163
1164    /// Call the blocking adapter. Reject unsupported functions before execution.
1165    pub fn call_blocking(&self, args: &[Value]) -> crate::ValueResult<Value> {
1166        let expected = match self.arity {
1167            Arity::Fixed(n) if args.len() != n => Some(n.to_string()),
1168            Arity::Variadic { min } if args.len() < min => Some(format!("{min}+")),
1169            _ => None,
1170        };
1171        if let Some(expected) = expected {
1172            return Err(crate::ValueError::ArityError {
1173                name: self.name.to_string(),
1174                expected,
1175                got: args.len(),
1176            });
1177        }
1178        let adapter = self.blocking_func.as_ref().ok_or_else(|| {
1179            crate::ValueError::Other(format!(
1180                "blocking cannot offload {}: this function has no owned-work adapter",
1181                self.name
1182            ))
1183        })?;
1184        adapter(args)
1185    }
1186}
1187
1188impl std::fmt::Debug for NativeFn {
1189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1190        f.debug_struct("NativeFn")
1191            .field("name", &self.name)
1192            .field("arity", &self.arity)
1193            .field("func", &"<fn>")
1194            .finish()
1195    }
1196}
1197
1198impl cljrs_gc::Trace for NativeFn {
1199    #[cfg(feature = "no-gc")]
1200    fn immutable_children(&self) -> bool {
1201        true
1202    }
1203    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1204        for value in self.captures.iter() {
1205            value.trace(visitor);
1206        }
1207    }
1208}
1209
1210// ── CljxFnArity ───────────────────────────────────────────────────────────────
1211
1212/// One arity branch of a Clojure function.
1213#[derive(Debug, Clone)]
1214pub struct CljxFnArity {
1215    /// Simple parameter names (no `&`).
1216    /// For destructured params, these are gensym'd names.
1217    pub params: Vec<Arc<str>>,
1218    /// The name after `&`, if any.
1219    pub rest_param: Option<Arc<str>>,
1220    /// The body forms for this arity. Shared, so cloning a fn (e.g. when
1221    /// `lazy-seq` wraps a thunk, or a closure is copied) does not deep-copy
1222    /// its body.
1223    pub body: Arc<[Form]>,
1224    /// Destructuring patterns: (param_index, original_form).
1225    /// After binding the gensym'd param, these patterns are applied
1226    /// via `bind_pattern` to destructure the value.
1227    pub destructure_params: Vec<(usize, Form)>,
1228    /// If the rest param is destructured, the original form.
1229    pub destructure_rest: Option<Form>,
1230    /// Unique ID for IR cache lookup (assigned by the evaluator).
1231    pub ir_arity_id: u64,
1232    /// Optional primitive type hint per positional parameter (parallel to
1233    /// `params`).  `^long x` → `Some(TypeHint::Long)`; an un-hinted or
1234    /// non-primitive-tagged param → `None`.  Drives unboxed codegen.
1235    pub param_hints: Vec<Option<TypeHint>>,
1236    /// Primitive type hint on the rest param, if any (rarely useful, but parsed
1237    /// for symmetry).
1238    pub rest_hint: Option<TypeHint>,
1239}
1240
1241impl CljxFnArity {
1242    /// Heap bytes owned by this arity, not counting the `CljxFnArity` struct itself.
1243    pub fn heap_size(&self) -> usize {
1244        // params Vec buffer (Arc<str> pointers; the str data is shared, skip it)
1245        self.params.capacity() * mem::size_of::<Arc<str>>()
1246        // body: the dominant consumer — Form AST trees stored inline
1247        + self.body.len() * mem::size_of::<Form>()
1248        + self.body.iter().map(|f| f.heap_size()).sum::<usize>()
1249        // destructure_params
1250        + self.destructure_params.capacity() * mem::size_of::<(usize, Form)>()
1251        + self.destructure_params.iter().map(|(_, f)| f.heap_size()).sum::<usize>()
1252        // destructure_rest
1253        + self.destructure_rest.as_ref()
1254            .map_or(0, |f| mem::size_of::<Form>() + f.heap_size())
1255        // param_hints (Copy elements, no nested heap)
1256        + self.param_hints.capacity() * mem::size_of::<Option<TypeHint>>()
1257    }
1258}
1259
1260// ── CljxFn ────────────────────────────────────────────────────────────────────
1261
1262/// An interpreted Clojure closure with captured environment.
1263#[derive(Debug, Clone)]
1264pub struct CljxFn {
1265    pub name: Option<Arc<str>>,
1266    pub arities: Vec<CljxFnArity>,
1267    /// Names of closed-over bindings (parallel to `closed_over_vals`).
1268    pub closed_over_names: Vec<Arc<str>>,
1269    /// Values of closed-over bindings (parallel to `closed_over_names`).
1270    pub closed_over_vals: Vec<Value>,
1271    /// True if this function was defined with `defmacro`.
1272    pub is_macro: bool,
1273    /// True if this function carries `^:async` metadata. When an async runtime
1274    /// (`cljrs-async`) is registered, calling such a function spawns its body as
1275    /// a task and returns a `Value::Future` immediately instead of running it
1276    /// synchronously. Without a runtime it runs synchronously like any other fn.
1277    pub is_async: bool,
1278    /// Namespace in which this function was defined (for macro hygiene).
1279    pub defining_ns: Arc<str>,
1280    /// Back-pointer to the `GcPtr` that owns this `CljxFn`, set immediately
1281    /// after allocation so that a named anonymous function's self-reference
1282    /// (e.g. `(fn g [] g)`) returns the *identical* pointer to the caller,
1283    /// preserving pointer-equality semantics (`(= f (f))` → `true`).
1284    pub self_ptr: Option<GcPtr<CljxFn>>,
1285}
1286
1287impl CljxFn {
1288    pub fn new(
1289        name: Option<Arc<str>>,
1290        arities: Vec<CljxFnArity>,
1291        closed_over_names: Vec<Arc<str>>,
1292        closed_over_vals: Vec<Value>,
1293        is_macro: bool,
1294        defining_ns: Arc<str>,
1295    ) -> Self {
1296        Self {
1297            name,
1298            arities,
1299            closed_over_names,
1300            closed_over_vals,
1301            is_macro,
1302            is_async: false,
1303            defining_ns,
1304            self_ptr: None,
1305        }
1306    }
1307}
1308
1309impl cljrs_gc::Trace for CljxFn {
1310    #[cfg(feature = "no-gc")]
1311    fn immutable_children(&self) -> bool {
1312        true
1313    }
1314    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1315        use cljrs_gc::GcVisitor as _;
1316        for v in &self.closed_over_vals {
1317            v.trace(visitor);
1318        }
1319        if let Some(ref p) = self.self_ptr {
1320            visitor.visit(p);
1321        }
1322    }
1323
1324    fn gc_size_extra(&self) -> usize {
1325        // Vec<CljxFnArity> buffer + each arity's inline-owned heap
1326        self.arities.capacity() * mem::size_of::<CljxFnArity>()
1327            + self
1328                .arities
1329                .iter()
1330                .map(CljxFnArity::heap_size)
1331                .sum::<usize>()
1332    }
1333}
1334
1335// ── BoundFn ──────────────────────────────────────────────────────────────────
1336
1337/// A function wrapped with captured dynamic bindings.
1338/// When called, the captured bindings are pushed as a frame before delegating
1339/// to the wrapped function. This means captured bindings override the caller's
1340/// for the same var, but vars not in the capture fall through normally.
1341#[derive(Debug)]
1342pub struct BoundFn {
1343    /// The wrapped callable.
1344    pub wrapped: Value,
1345    /// Captured dynamic bindings (merged flat frame; opaque to cljrs-value).
1346    pub captured_bindings: HashMap<usize, Value>,
1347}
1348
1349impl cljrs_gc::Trace for BoundFn {
1350    #[cfg(feature = "no-gc")]
1351    fn immutable_children(&self) -> bool {
1352        true
1353    }
1354    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1355        self.wrapped.trace(visitor);
1356        for val in self.captured_bindings.values() {
1357            val.trace(visitor);
1358        }
1359    }
1360
1361    fn gc_size_extra(&self) -> usize {
1362        // HashMap<usize, Value>: hashbrown open-addressing, ~1 control byte + entry per slot.
1363        self.captured_bindings.capacity() * (1 + mem::size_of::<usize>() + mem::size_of::<Value>())
1364    }
1365}
1366
1367// ── Thunk / LazySeq ───────────────────────────────────────────────────────────
1368
1369/// A deferred computation that produces a `Value` when forced.
1370pub trait Thunk: std::fmt::Debug + cljrs_gc::Trace {
1371    /// Errors other than `crate::ValueError::Cancelled` are cached as their message.
1372    /// A thunk interrupted by fiber cancellation must return
1373    /// `crate::ValueError::Cancelled`, so the cell stays pending for other readers.
1374    fn force(&self) -> crate::ValueResult<Value>;
1375}
1376
1377/// The cached message of a failed thunk.
1378fn thunk_error_message(error: crate::ValueError) -> String {
1379    match error {
1380        crate::ValueError::Other(message) => message,
1381        other => other.to_string(),
1382    }
1383}
1384
1385/// Identifies a computation by its isolate thread and optional fiber.
1386#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1387pub struct Realizer {
1388    thread: std::thread::ThreadId,
1389    fiber: Option<u64>,
1390}
1391impl Realizer {
1392    pub fn current() -> Self {
1393        Self {
1394            thread: std::thread::current().id(),
1395            fiber: crate::parking::current(),
1396        }
1397    }
1398
1399    /// Reject a carrier wait that would prevent this owner's fiber resuming.
1400    /// A different fiber can park; a different thread can block normally.
1401    pub fn ensure_can_wait(self) -> Result<(), String> {
1402        if self.thread == std::thread::current().id()
1403            && self.fiber.is_some()
1404            && crate::parking::current().is_none()
1405        {
1406            Err("cannot block the isolate thread waiting for a parked fiber. Run this code inside a fiber.".into())
1407        } else {
1408            Ok(())
1409        }
1410    }
1411}
1412
1413// Cancellation/panic returns the original thunk to Pending for another reader.
1414struct RealizationGuard<'a> {
1415    lazy: Option<&'a LazySeq>,
1416    delay: Option<&'a Delay>,
1417}
1418impl Drop for RealizationGuard<'_> {
1419    fn drop(&mut self) {
1420        if let Some(cell) = self.lazy {
1421            let mut state = cell.state.lock().unwrap();
1422            if let LazySeqState::Realizing { thunk, .. } = &*state {
1423                *state = LazySeqState::Pending(thunk.clone());
1424            }
1425            drop(state);
1426            cell.ready.notify_all();
1427        }
1428        if let Some(cell) = self.delay {
1429            let mut state = cell.state.lock().unwrap();
1430            if let DelayState::Realizing { thunk, .. } = &*state {
1431                *state = DelayState::Pending(thunk.clone());
1432            }
1433            drop(state);
1434            cell.ready.notify_all();
1435        }
1436    }
1437}
1438
1439/// Internal state of a lazy sequence cell.
1440pub enum LazySeqState {
1441    /// Thunk not yet evaluated.
1442    Pending(Rc<dyn Thunk>),
1443    Realizing {
1444        owner: Realizer,
1445        thunk: Rc<dyn Thunk>,
1446    },
1447    /// Result cached after first force.
1448    Forced(Value),
1449    /// Thunk evaluation failed; error message is cached.
1450    Error(String),
1451}
1452
1453/// A lazy sequence that caches its result and retries interrupted realizations.
1454pub struct LazySeq {
1455    ready: Event,
1456    pub state: TrackedMutex<LazySeqState>,
1457}
1458
1459impl LazySeq {
1460    pub fn new(thunk: Box<dyn Thunk>) -> Self {
1461        Self {
1462            state: TrackedMutex::new(LazySeqState::Pending(Rc::from(thunk))),
1463            ready: Event::default(),
1464        }
1465    }
1466
1467    /// Realize the sequence: force the thunk on first call, return cached value on subsequent calls.
1468    /// Cached thunk errors return nil and remain available through `error()`.
1469    /// Panics if a carrier wait would prevent the owning fiber resuming.
1470    /// Use `try_realize` when the caller can return an error.
1471    pub fn realize(&self) -> Value {
1472        match self.try_realize() {
1473            Ok(value) => value,
1474            // No error channel here: continue the cancellation as an unwind.
1475            Err(crate::ValueError::Cancelled) => crate::parking::resume_cancelled(),
1476            Err(error) => {
1477                // Infallible trait callers cannot return a transient wait
1478                // error. Fail explicitly instead of silently losing data.
1479                assert!(self.error().is_some(), "{error}");
1480                Value::Nil
1481            }
1482        }
1483    }
1484
1485    /// Realize a sequence with explicit errors, including unsafe carrier waits.
1486    /// A rejected wait does not change the thunk or its realization state.
1487    /// Cached and ordinary errors are `crate::ValueError::Other`; a thunk cancelled
1488    /// with its fiber returns `crate::ValueError::Cancelled` and stays pending.
1489    pub fn try_realize(&self) -> crate::ValueResult<Value> {
1490        let owner = Realizer::current();
1491        let thunk = loop {
1492            let listener = self.ready.listen();
1493            let mut state = self.state.lock().unwrap();
1494            match &*state {
1495                LazySeqState::Forced(v) => {
1496                    let value = v.clone();
1497                    drop(state);
1498                    crate::publish::read_slot(self as *const _ as usize, 0);
1499                    return Ok(value);
1500                }
1501                LazySeqState::Error(error) => return Err(crate::ValueError::Other(error.clone())),
1502                LazySeqState::Realizing { owner: active, .. } => {
1503                    if *active == owner {
1504                        return Ok(Value::Nil);
1505                    }
1506                    active.ensure_can_wait().map_err(crate::ValueError::Other)?;
1507                    drop(state);
1508                    listener.wait(None);
1509                    continue;
1510                }
1511                LazySeqState::Pending(_) => {}
1512            }
1513            let LazySeqState::Pending(thunk) =
1514                mem::replace(&mut *state, LazySeqState::Forced(Value::Nil))
1515            else {
1516                unreachable!()
1517            };
1518            *state = LazySeqState::Realizing {
1519                owner,
1520                thunk: thunk.clone(),
1521            };
1522            break thunk;
1523        };
1524        let mut guard = RealizationGuard {
1525            lazy: Some(self),
1526            delay: None,
1527        };
1528        let result = match thunk.force() {
1529            Ok(result) => {
1530                let result = crate::publish::publish_for_owner(result, self as *const _ as usize);
1531                *self.state.lock().unwrap() = LazySeqState::Forced(result.clone());
1532                Ok(result)
1533            }
1534            // Cancellation is not the thunk's result: the guard returns it to
1535            // Pending for other readers, and the error propagates typed.
1536            Err(crate::ValueError::Cancelled) => {
1537                drop(thunk);
1538                drop(guard);
1539                return Err(crate::ValueError::Cancelled);
1540            }
1541            Err(error) => {
1542                let message = thunk_error_message(error);
1543                *self.state.lock().unwrap() = LazySeqState::Error(message.clone());
1544                Err(crate::ValueError::Other(message))
1545            }
1546        };
1547        drop(thunk);
1548        #[cfg(feature = "no-gc")]
1549        cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize).set_slot(
1550            self as *const _ as usize,
1551            usize::MAX,
1552            &Default::default(),
1553        );
1554        guard.lazy = None;
1555        if result.is_ok() {
1556            crate::publish::read_slot(self as *const _ as usize, 0);
1557        }
1558        self.ready.notify_all();
1559        result
1560    }
1561
1562    /// Return the cached error message, if the thunk failed.
1563    pub fn error(&self) -> Option<String> {
1564        let guard = self.state.lock().unwrap();
1565        if let LazySeqState::Error(e) = &*guard {
1566            Some(e.clone())
1567        } else {
1568            None
1569        }
1570    }
1571}
1572
1573impl std::fmt::Debug for LazySeq {
1574    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1575        write!(f, "LazySeq(...)")
1576    }
1577}
1578
1579impl Drop for LazySeq {
1580    fn drop(&mut self) {
1581        crate::publish::clear_slots(self as *const _ as usize);
1582    }
1583}
1584impl cljrs_gc::Trace for LazySeq {
1585    #[cfg(feature = "no-gc")]
1586    fn init_ownership(&self) {
1587        let state = self.state.lock().unwrap();
1588        if let LazySeqState::Pending(thunk) = &*state {
1589            struct ThunkRef<'a>(&'a dyn Thunk);
1590            impl cljrs_gc::Trace for ThunkRef<'_> {
1591                fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1592                    self.0.trace(visitor);
1593                }
1594            }
1595            let lease = cljrs_gc::region::RegionLease::for_value(&ThunkRef(thunk.as_ref()));
1596            cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize).set_slot(
1597                self as *const _ as usize,
1598                usize::MAX,
1599                &lease,
1600            );
1601        }
1602    }
1603    #[cfg(feature = "no-gc")]
1604    fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
1605    #[cfg(not(feature = "no-gc"))]
1606    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1607        // Safe to lock unconditionally: realize() drops the lock before entering
1608        // eval (thunk.force()), so the lock is never held across a GC safepoint.
1609        {
1610            let state = self.state.lock().unwrap();
1611            match &*state {
1612                LazySeqState::Pending(thunk) => thunk.trace(visitor),
1613                LazySeqState::Realizing { thunk, .. } => thunk.trace(visitor),
1614                LazySeqState::Forced(v) => v.trace(visitor),
1615                LazySeqState::Error(_) => {}
1616            }
1617        }
1618    }
1619}
1620
1621// ── CljxCons ──────────────────────────────────────────────────────────────────
1622
1623/// A lazy cons cell: head element + tail (may be a `LazySeq`, `List`, or `Nil`).
1624///
1625/// Used when `cons` is called with a `LazySeq` or `Cons` tail, enabling lazy
1626/// sequences without eagerly realizing them.
1627#[derive(Debug, Clone)]
1628pub struct CljxCons {
1629    pub head: Value,
1630    pub tail: Value,
1631}
1632
1633impl cljrs_gc::Trace for CljxCons {
1634    #[cfg(feature = "no-gc")]
1635    fn immutable_children(&self) -> bool {
1636        true
1637    }
1638    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1639        self.head.trace(visitor);
1640        self.tail.trace(visitor);
1641    }
1642}
1643
1644// ── Volatile ──────────────────────────────────────────────────────────────────
1645
1646/// Non-atomic mutable cell (single-thread performance, no CAS).
1647pub struct Volatile {
1648    pub value: TrackedMutex<Value>,
1649    #[cfg(feature = "no-gc")]
1650    value_lease: std::cell::RefCell<crate::publish::ValueLease>,
1651}
1652
1653impl Volatile {
1654    pub fn new(v: Value) -> Self {
1655        // GC builds: heap-promotion fallback (see `Var::bind`).
1656        #[cfg(feature = "no-gc")]
1657        let (v, lease) =
1658            crate::publish::prepare_for_owner(v, &cljrs_gc::region::AllocationOwner::current());
1659        #[cfg(not(feature = "no-gc"))]
1660        let v = crate::publish::publish_value(v);
1661        Self {
1662            value: TrackedMutex::new(v),
1663            #[cfg(feature = "no-gc")]
1664            value_lease: std::cell::RefCell::new(lease),
1665        }
1666    }
1667
1668    pub fn snapshot(&self) -> crate::publish::ValueSnapshot {
1669        crate::publish::ValueSnapshot {
1670            value: self.value.lock().unwrap().clone(),
1671            #[cfg(feature = "no-gc")]
1672            lease: self.value_lease.borrow().snapshot(),
1673        }
1674    }
1675
1676    pub fn deref(&self) -> Value {
1677        let snapshot = self.snapshot();
1678        snapshot.protect_current();
1679        snapshot.value
1680    }
1681
1682    pub fn reset(&self, v: Value) -> Value {
1683        #[cfg(not(feature = "no-gc"))]
1684        let v = crate::publish::publish_value(v);
1685        #[cfg(feature = "no-gc")]
1686        let (v, lease) = crate::publish::prepare_for_owner(
1687            v,
1688            &cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize),
1689        );
1690        #[cfg(feature = "no-gc")]
1691        lease.publish_return();
1692        *self.value.lock().unwrap() = v.clone();
1693        #[cfg(feature = "no-gc")]
1694        {
1695            *self.value_lease.borrow_mut() = lease;
1696        }
1697        v
1698    }
1699}
1700
1701impl std::fmt::Debug for Volatile {
1702    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1703        write!(f, "Volatile")
1704    }
1705}
1706
1707impl cljrs_gc::Trace for Volatile {
1708    #[cfg(feature = "no-gc")]
1709    fn init_ownership(&self) {
1710        let owner = cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize);
1711        let lease = self.value_lease.borrow().reparent(&owner);
1712        let old = self.value_lease.replace(lease);
1713        drop(old);
1714    }
1715
1716    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1717        #[cfg(not(feature = "no-gc"))]
1718        {
1719            let value = self.value.lock().unwrap();
1720            value.trace(visitor);
1721        }
1722    }
1723}
1724
1725// ── Delay ─────────────────────────────────────────────────────────────────────
1726
1727/// Internal state of a delay cell.
1728pub enum DelayState {
1729    Error(String),
1730    Pending(Rc<dyn Thunk>),
1731    Realizing {
1732        owner: Realizer,
1733        thunk: Rc<dyn Thunk>,
1734    },
1735    Forced(Value),
1736}
1737
1738/// A lazy computation with a cached result and retry after interruption.
1739pub struct Delay {
1740    ready: Event,
1741    pub state: TrackedMutex<DelayState>,
1742}
1743
1744impl Delay {
1745    pub fn new(thunk: Box<dyn Thunk>) -> Self {
1746        Self {
1747            state: TrackedMutex::new(DelayState::Pending(Rc::from(thunk))),
1748            ready: Event::default(),
1749        }
1750    }
1751
1752    /// Force the delay and cache the result. Errors are as for
1753    /// [`LazySeq::try_realize`].
1754    pub fn force(&self) -> crate::ValueResult<Value> {
1755        let owner = Realizer::current();
1756        let thunk = loop {
1757            let listener = self.ready.listen();
1758            let mut state = self.state.lock().unwrap();
1759            match &*state {
1760                DelayState::Forced(v) => {
1761                    let value = v.clone();
1762                    drop(state);
1763                    crate::publish::read_slot(self as *const _ as usize, 0);
1764                    return Ok(value);
1765                }
1766                DelayState::Error(e) => return Err(crate::ValueError::Other(e.clone())),
1767                DelayState::Realizing { owner: active, .. } => {
1768                    if *active == owner {
1769                        return Err(crate::ValueError::Other(
1770                            "recursive delay realization".into(),
1771                        ));
1772                    }
1773                    active.ensure_can_wait().map_err(crate::ValueError::Other)?;
1774                    drop(state);
1775                    listener.wait(None);
1776                    continue;
1777                }
1778                DelayState::Pending(_) => {}
1779            }
1780            let DelayState::Pending(thunk) =
1781                mem::replace(&mut *state, DelayState::Forced(Value::Nil))
1782            else {
1783                unreachable!()
1784            };
1785            *state = DelayState::Realizing {
1786                owner,
1787                thunk: thunk.clone(),
1788            };
1789            break thunk;
1790        };
1791        let mut guard = RealizationGuard {
1792            lazy: None,
1793            delay: Some(self),
1794        };
1795        let result = match thunk.force() {
1796            Ok(value) => Ok(crate::publish::publish_for_owner(
1797                value,
1798                self as *const _ as usize,
1799            )),
1800            // As for lazy seqs: the guard leaves the thunk Pending.
1801            Err(crate::ValueError::Cancelled) => {
1802                drop(thunk);
1803                drop(guard);
1804                return Err(crate::ValueError::Cancelled);
1805            }
1806            Err(error) => Err(thunk_error_message(error)),
1807        };
1808        *self.state.lock().unwrap() = match &result {
1809            Ok(value) => DelayState::Forced(value.clone()),
1810            Err(error) => DelayState::Error(error.clone()),
1811        };
1812        let result = result.map_err(crate::ValueError::Other);
1813        drop(thunk);
1814        #[cfg(feature = "no-gc")]
1815        cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize).set_slot(
1816            self as *const _ as usize,
1817            usize::MAX,
1818            &Default::default(),
1819        );
1820        guard.delay = None;
1821        if result.is_ok() {
1822            crate::publish::read_slot(self as *const _ as usize, 0);
1823        }
1824        self.ready.notify_all();
1825        result
1826    }
1827
1828    /// True if the delay has already been forced.
1829    pub fn is_realized(&self) -> bool {
1830        matches!(
1831            &*self.state.lock().unwrap(),
1832            DelayState::Forced(_) | DelayState::Error(_)
1833        )
1834    }
1835}
1836
1837impl std::fmt::Debug for Delay {
1838    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1839        write!(f, "Delay")
1840    }
1841}
1842
1843impl Drop for Delay {
1844    fn drop(&mut self) {
1845        crate::publish::clear_slots(self as *const _ as usize);
1846    }
1847}
1848impl cljrs_gc::Trace for Delay {
1849    #[cfg(feature = "no-gc")]
1850    fn init_ownership(&self) {
1851        let state = self.state.lock().unwrap();
1852        if let DelayState::Pending(thunk) = &*state {
1853            struct ThunkRef<'a>(&'a dyn Thunk);
1854            impl cljrs_gc::Trace for ThunkRef<'_> {
1855                fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1856                    self.0.trace(visitor);
1857                }
1858            }
1859            let lease = cljrs_gc::region::RegionLease::for_value(&ThunkRef(thunk.as_ref()));
1860            cljrs_gc::region::AllocationOwner::for_address(self as *const _ as usize).set_slot(
1861                self as *const _ as usize,
1862                usize::MAX,
1863                &lease,
1864            );
1865        }
1866    }
1867    #[cfg(feature = "no-gc")]
1868    fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
1869    #[cfg(not(feature = "no-gc"))]
1870    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1871        // Safe to lock unconditionally: force() drops the lock before entering
1872        // eval (thunk.force()), so the lock is never held across a GC safepoint.
1873        {
1874            let state = self.state.lock().unwrap();
1875            match &*state {
1876                DelayState::Pending(thunk) => thunk.trace(visitor),
1877                DelayState::Realizing { thunk, .. } => thunk.trace(visitor),
1878                DelayState::Error(_) => {}
1879                DelayState::Forced(v) => v.trace(visitor),
1880            }
1881        }
1882    }
1883}
1884
1885// ── CljxPromise ───────────────────────────────────────────────────────────────
1886
1887/// A one-shot rendezvous (promise).
1888pub struct CljxPromise {
1889    pub ready: Event,
1890    pub value: Mutex<Option<Value>>,
1891    pub cond: Condvar,
1892}
1893
1894impl CljxPromise {
1895    pub fn new() -> Self {
1896        Self {
1897            value: Mutex::new(None),
1898            cond: Condvar::new(),
1899            ready: Event::default(),
1900        }
1901    }
1902
1903    /// Deliver a value (no-op if already delivered).
1904    pub fn deliver(&self, v: Value) {
1905        if self.value.lock().unwrap().is_some() {
1906            return;
1907        }
1908        // GC builds: heap-promotion fallback — the promise may outlive (and be
1909        // read from outside) any region scope active at delivery time.
1910        let v = crate::publish::publish_for_owner(v, self as *const _ as usize);
1911        let mut guard = self.value.lock().unwrap();
1912        if guard.is_none() {
1913            *guard = Some(v);
1914            self.cond.notify_all();
1915            drop(guard);
1916            self.ready.notify_all();
1917        }
1918    }
1919
1920    /// Block until a value is available, then return it.
1921    pub fn deref_blocking(&self) -> Value {
1922        self.wait_value(None).unwrap()
1923    }
1924
1925    pub fn wait_value(&self, timeout: Option<Duration>) -> Option<Value> {
1926        let deadline = timeout.map(|d| Instant::now() + d);
1927        loop {
1928            let listener = self.ready.listen();
1929            let value = self.value.lock().unwrap().clone();
1930            if let Some(value) = value {
1931                crate::publish::read_slot(self as *const _ as usize, 0);
1932                return Some(value);
1933            }
1934            if !listener.wait(deadline.map(|d| d.saturating_duration_since(Instant::now()))) {
1935                return self.value.lock().unwrap().clone();
1936            }
1937        }
1938    }
1939
1940    /// True if already delivered.
1941    pub fn is_realized(&self) -> bool {
1942        self.value.lock().unwrap().is_some()
1943    }
1944}
1945
1946impl Drop for CljxPromise {
1947    fn drop(&mut self) {
1948        crate::publish::clear_slots(self as *const _ as usize);
1949    }
1950}
1951
1952impl Default for CljxPromise {
1953    fn default() -> Self {
1954        Self::new()
1955    }
1956}
1957
1958impl std::fmt::Debug for CljxPromise {
1959    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1960        write!(f, "Promise")
1961    }
1962}
1963
1964impl cljrs_gc::Trace for CljxPromise {
1965    #[cfg(feature = "no-gc")]
1966    fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
1967    #[cfg(not(feature = "no-gc"))]
1968    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1969        {
1970            let value = self.value.lock().unwrap();
1971            if let Some(v) = value.as_ref() {
1972                v.trace(visitor);
1973            }
1974        }
1975    }
1976}
1977
1978// ── CljxFuture ────────────────────────────────────────────────────────────────
1979
1980/// Thread-pool future state.
1981pub enum FutureState {
1982    Running,
1983    Done(Value),
1984    /// The future's body threw. Holds the thrown Clojure value (a
1985    /// `Value::Error`) so `await`/`deref` can re-throw it with its
1986    /// `ex-data`/`ex-cause` intact, rather than a stringified message.
1987    Failed(Value),
1988    /// The future exhausted the evaluation budget. Kept distinct from a
1989    /// thrown value so user-level catch clauses cannot intercept it.
1990    GasExhausted,
1991    /// The producer was interrupted by fiber cancellation (uncatchable).
1992    Interrupted,
1993    /// The future's body failed with a managed-memory limit fault (strict
1994    /// governor mode). Kept distinct for the same reason as `GasExhausted`.
1995    MemoryLimitExceeded(cljrs_gc::MemoryLimitExceeded),
1996    Cancelled,
1997}
1998
1999/// The message carried by the error raised when reading a cancelled future.
2000pub const FUTURE_CANCELLED_MSG: &str = "future was cancelled";
2001
2002/// A future value computed asynchronously on another thread.
2003pub struct CljxFuture {
2004    pub ready: Event,
2005    pub state: Mutex<FutureState>,
2006    pub cond: Condvar,
2007    executor_thread: Option<std::thread::ThreadId>,
2008    /// Set once a consumer has read the settled result (via `await`/`deref`).
2009    /// Used to warn about a `Failed` future that is discarded without anyone
2010    /// ever observing its error (the fire-and-forget footgun).
2011    observed: std::sync::atomic::AtomicBool,
2012}
2013
2014impl CljxFuture {
2015    pub fn new() -> Self {
2016        Self {
2017            state: Mutex::new(FutureState::Running),
2018            cond: Condvar::new(),
2019            ready: Event::default(),
2020            executor_thread: None,
2021            observed: std::sync::atomic::AtomicBool::new(false),
2022        }
2023    }
2024
2025    /// A future whose producer needs this thread's local executor.
2026    pub fn new_local() -> Self {
2027        let mut future = Self::new();
2028        future.executor_thread = Some(std::thread::current().id());
2029        future
2030    }
2031
2032    /// Wait for settlement, parking only when running on a fiber.
2033    pub fn wait_value(&self, timeout: Option<Duration>) -> crate::ValueResult<Option<Value>> {
2034        let deadline = timeout.map(|d| Instant::now() + d);
2035        loop {
2036            let listener = self.ready.listen();
2037            if let Some(result) = self.settled_value() {
2038                return result.map(Some);
2039            }
2040            if deadline.is_some_and(|deadline| deadline <= Instant::now()) {
2041                return self.settled_value().transpose();
2042            }
2043            if crate::parking::current().is_none()
2044                && self.executor_thread == Some(std::thread::current().id())
2045            {
2046                return Err(crate::ValueError::Other(
2047                    "cannot wait for an isolate future outside a fiber; use await in async code or fiber::run_root for synchronous evaluation".into(),
2048                ));
2049            }
2050            if !listener.wait(deadline.map(|d| d.saturating_duration_since(Instant::now()))) {
2051                return self.settled_value().transpose();
2052            }
2053        }
2054    }
2055
2056    pub fn settled_value(&self) -> Option<crate::ValueResult<Value>> {
2057        use crate::ValueError;
2058        let state = self.state.lock().unwrap();
2059        let value = match &*state {
2060            FutureState::Running => return None,
2061            FutureState::Done(v) => Ok(v.clone()),
2062            FutureState::Failed(v) => Err(crate::ValueError::Thrown(v.clone())),
2063            FutureState::GasExhausted => Err(crate::ValueError::GasExhausted),
2064            FutureState::MemoryLimitExceeded(f) => Err(crate::ValueError::MemoryLimitExceeded(*f)),
2065            // A producer interrupted by fiber cancellation reaches awaiters as
2066            // the same catchable error as `future-cancel`. A cancelled
2067            // awaiter stops at its own park; healthy ones can handle it.
2068            FutureState::Cancelled | FutureState::Interrupted => {
2069                drop(state);
2070                return Some(Err(crate::ValueError::Thrown(Self::cancelled_error())));
2071            }
2072        };
2073        self.mark_observed();
2074        drop(state);
2075        #[cfg(feature = "no-gc")]
2076        match &value {
2077            Ok(value) | Err(crate::ValueError::Thrown(value)) => {
2078                cljrs_gc::region::RegionLease::for_value(value).protect_read()
2079            }
2080            _ => {}
2081        }
2082        Some(value)
2083    }
2084
2085    /// True if done, failed, or cancelled (not still running).
2086    pub fn is_done(&self) -> bool {
2087        !matches!(&*self.state.lock().unwrap(), FutureState::Running)
2088    }
2089
2090    /// True if explicitly cancelled.
2091    pub fn is_cancelled(&self) -> bool {
2092        matches!(&*self.state.lock().unwrap(), FutureState::Cancelled)
2093    }
2094
2095    /// Mark a still-running future cancelled, waking anyone waiting on it.
2096    ///
2097    /// Returns false when it had already settled. The task itself is not
2098    /// interrupted; what is cancelled is the result.
2099    pub fn cancel(&self) -> bool {
2100        let mut state = self.state.lock().unwrap();
2101        if matches!(&*state, FutureState::Running) {
2102            *state = FutureState::Cancelled;
2103            self.cond.notify_all();
2104            drop(state);
2105            self.ready.notify_all();
2106            true
2107        } else {
2108            false
2109        }
2110    }
2111
2112    /// The catchable value every reader of a cancelled future raises.
2113    ///
2114    /// One shape for every path — the sync tree-walker's `await`/`deref`, the
2115    /// async tree-walker, and the compiled state machine all hand user code
2116    /// the same `ex-info`-like error, so a `catch` behaves the same whether or
2117    /// not the enclosing fn happened to be async-JIT-compiled.
2118    pub fn cancelled_error() -> Value {
2119        Value::Error(GcPtr::new(crate::ExceptionInfo::new(
2120            crate::ValueError::Other(FUTURE_CANCELLED_MSG.to_string()),
2121            FUTURE_CANCELLED_MSG.to_string(),
2122            None,
2123            None,
2124        )))
2125    }
2126
2127    /// Mark this future's result as observed. Call when a consumer reads the
2128    /// settled value (`await`/`deref`), so a later drop doesn't warn about an
2129    /// unobserved error.
2130    pub fn mark_observed(&self) {
2131        self.observed
2132            .store(true, std::sync::atomic::Ordering::Relaxed);
2133    }
2134}
2135
2136impl Drop for CljxFuture {
2137    fn drop(&mut self) {
2138        // Warn if a future failed but nobody ever observed the error — the
2139        // fire-and-forget case where a thrown error would otherwise vanish.
2140        // Tied to GC sweep timing: only fires once the future is unreachable,
2141        // so a not-yet-awaited (still reachable) failed future won't warn.
2142        //
2143        // SAFETY: this Drop can run during GC sweep. The thrown value held in
2144        // `Failed(v)` is itself a GC value whose backing box may be freed in
2145        // the *same* sweep, so we must NOT dereference it here (no `{v}`). We
2146        // only inspect the state discriminant, which is inline in our own
2147        // (still-valid) allocation.
2148        if !self.observed.load(std::sync::atomic::Ordering::Relaxed)
2149            && let Ok(state) = self.state.lock()
2150        {
2151            match &*state {
2152                FutureState::Failed(_)
2153                | FutureState::GasExhausted
2154                | FutureState::MemoryLimitExceeded(_) => eprintln!(
2155                    "[clojurust warning] a failed future was discarded without its error \
2156                     being observed (no await/deref); the failure was lost"
2157                ),
2158                // Interruption is deliberate, like `future-cancel`: silent
2159                // unless CLJRS_WARN_INTERRUPTED_FUTURES opts in.
2160                FutureState::Interrupted if warn_interrupted_futures() => eprintln!(
2161                    "[clojurust warning] an interrupted future was discarded without \
2162                     being observed (no await/deref)"
2163                ),
2164                _ => {}
2165            }
2166        }
2167        crate::publish::clear_slots(self as *const _ as usize);
2168    }
2169}
2170
2171/// Whether discarding an unread `Interrupted` future warns, like a failed
2172/// one. Off by default; `CLJRS_WARN_INTERRUPTED_FUTURES=1` (or `true`/`yes`)
2173/// enables it. Read once per process: `CljxFuture::drop` can run during a GC
2174/// sweep.
2175fn warn_interrupted_futures() -> bool {
2176    static WARN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2177    *WARN.get_or_init(|| {
2178        std::env::var("CLJRS_WARN_INTERRUPTED_FUTURES").is_ok_and(|value| {
2179            matches!(
2180                value.trim().to_ascii_lowercase().as_str(),
2181                "1" | "true" | "yes"
2182            )
2183        })
2184    })
2185}
2186
2187impl Default for CljxFuture {
2188    fn default() -> Self {
2189        Self::new()
2190    }
2191}
2192
2193impl std::fmt::Debug for CljxFuture {
2194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2195        write!(f, "Future")
2196    }
2197}
2198
2199impl cljrs_gc::Trace for CljxFuture {
2200    #[cfg(feature = "no-gc")]
2201    fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
2202    #[cfg(not(feature = "no-gc"))]
2203    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
2204        {
2205            let state = self.state.lock().unwrap();
2206            // Both Done and Failed hold a Value (the result or the thrown
2207            // error); trace either so the GC keeps it alive until observed.
2208            if let FutureState::Done(v) | FutureState::Failed(v) = &*state {
2209                v.trace(visitor);
2210            }
2211        }
2212    }
2213}
2214
2215// ── Agent ─────────────────────────────────────────────────────────────────────
2216
2217/// A Clojure agent — asynchronous state update queue (stub: not yet implemented).
2218pub struct Agent {
2219    /// Current state.
2220    pub state: Arc<Mutex<Value>>,
2221    /// Last error.
2222    pub error: Arc<Mutex<Option<Value>>>,
2223    pub watches: TrackedMutex<Vec<(Value, Value)>>,
2224}
2225
2226impl Agent {
2227    pub fn get_state(&self) -> Value {
2228        self.state.lock().unwrap().clone()
2229    }
2230
2231    pub fn get_error(&self) -> Option<Value> {
2232        self.error.lock().unwrap().clone()
2233    }
2234
2235    pub fn clear_error(&self) {
2236        *self.error.lock().unwrap() = None;
2237    }
2238}
2239
2240impl std::fmt::Debug for Agent {
2241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2242        write!(f, "Agent")
2243    }
2244}
2245
2246impl cljrs_gc::Trace for Agent {
2247    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
2248        {
2249            let state = self.state.lock().unwrap();
2250            state.trace(visitor);
2251        }
2252        {
2253            let error = self.error.lock().unwrap();
2254            if let Some(e) = error.as_ref() {
2255                e.trace(visitor);
2256            }
2257        }
2258        {
2259            let watches = self.watches.lock().unwrap();
2260            for (key, f) in watches.iter() {
2261                key.trace(visitor);
2262                f.trace(visitor);
2263            }
2264        }
2265    }
2266}
2267
2268// ── Tests ─────────────────────────────────────────────────────────────────────
2269
2270#[cfg(test)]
2271mod var_tests {
2272    use super::*;
2273    use crate::shared::SharedValue;
2274
2275    #[test]
2276    fn native_captures_share_one_immutable_allocation() {
2277        let native = NativeFn::with_captures(
2278            "capture",
2279            Arity::Fixed(0),
2280            vec![Value::Long(7)],
2281            |captures, _| Ok(captures[0].clone()),
2282        );
2283        assert_eq!(Arc::strong_count(&native.captures), 2);
2284        assert_eq!((native.func)(&[]).unwrap(), Value::Long(7));
2285    }
2286
2287    #[test]
2288    fn bind_promotable_mirrors_shared_root() {
2289        let var = Var::new("user", "x");
2290        assert!(var.shared_root.load().is_none());
2291        var.bind(Value::Long(7));
2292        assert!(matches!(
2293            var.shared_root.load().as_ref().as_ref(),
2294            Some(SharedValue::Long(7))
2295        ));
2296        assert_eq!(var.deref(), Some(Value::Long(7)));
2297        assert_eq!(var.deref_shared(), Some(Value::Long(7)));
2298    }
2299
2300    #[test]
2301    fn bind_nonpromotable_clears_shared_root() {
2302        let var = Var::new("user", "f");
2303        var.bind(Value::Long(1));
2304        assert!(var.shared_root.load().is_some());
2305        // Rebinding to a non-promotable value clears the mirror, but the
2306        // isolate-local fast path still holds it.
2307        let f = Value::NativeFunction(GcPtr::new(NativeFn::new("f", Arity::Fixed(0), |_| {
2308            Ok(Value::Nil)
2309        })));
2310        var.bind(f);
2311        assert!(var.shared_root.load().is_none());
2312        assert!(var.is_bound());
2313        assert_eq!(var.deref_shared(), None);
2314    }
2315
2316    #[test]
2317    fn from_shared_root_seeds_local_slot() {
2318        let src = Var::new("user", "y");
2319        src.bind(Value::Long(99));
2320        let recv = Var::from_shared_root("user", "y", false, src.shared_root.clone());
2321        assert_eq!(recv.deref(), Some(Value::Long(99)));
2322        // Same underlying cell.
2323        src.bind(Value::Long(100));
2324        assert_eq!(recv.deref_shared(), Some(Value::Long(100)));
2325    }
2326}