Skip to main content

cljrs_value/
types.rs

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