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