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#[derive(Debug)]
234pub struct Var {
235    pub namespace: Arc<str>,
236    pub name: Arc<str>,
237    pub value: Mutex<Option<Value>>,
238    pub is_macro: bool,
239    /// Metadata map (e.g. `{:dynamic true}`).
240    pub meta: Mutex<Option<Value>>,
241    pub watches: Mutex<Vec<(Value, Value)>>,
242}
243
244impl Var {
245    pub fn new(namespace: impl Into<Arc<str>>, name: impl Into<Arc<str>>) -> Self {
246        Self {
247            namespace: namespace.into(),
248            name: name.into(),
249            value: Mutex::new(None),
250            is_macro: false,
251            meta: Mutex::new(None),
252            watches: Mutex::new(Vec::new()),
253        }
254    }
255
256    pub fn is_bound(&self) -> bool {
257        self.value.lock().unwrap().is_some()
258    }
259
260    pub fn deref(&self) -> Option<Value> {
261        self.value.lock().unwrap().clone()
262    }
263
264    pub fn bind(&self, v: Value) {
265        // In no-gc debug builds: assert the value being stored in this
266        // program-lifetime Var came from the StaticArena, not a scratch region.
267        // A region-local pointer would dangle after the function returns.
268        #[cfg(all(feature = "no-gc", debug_assertions))]
269        debug_assert!(
270            value_gcptr_is_static(&v),
271            "no-gc: Var::bind({}/{}) received a region-local value — store violations \
272             indicate a missing StaticCtxGuard around the value expression",
273            self.namespace,
274            self.name
275        );
276        // GC builds: heap-promotion fallback — a region-allocated value bound
277        // to a program-lifetime var is deep-copied to the heap (or the active
278        // regions are retired when it cannot be).  One depth check when no
279        // region is open.
280        let v = crate::publish::publish_value(v);
281        // Replace the binding, holding the lock only across the swap.  The
282        // previous value (if any) is handed to the JIT rebind hook so it can
283        // reclaim native code compiled for a now-superseded definition
284        // (Phase 10.2 — code unloading).  `v.clone()` is O(1) for the only
285        // values that carry compiled code (`Value::Fn`, a `GcPtr` clone).
286        let prev = {
287            let mut slot = self.value.lock().unwrap();
288            slot.replace(v.clone())
289        };
290        if let Some(prev) = prev {
291            crate::jit_hooks::notify_var_rebind(&prev, &v);
292        }
293    }
294
295    pub fn get_meta(&self) -> Option<Value> {
296        self.meta.lock().unwrap().clone()
297    }
298
299    pub fn set_meta(&self, m: Value) {
300        *self.meta.lock().unwrap() = Some(m);
301    }
302
303    pub fn full_name(&self) -> String {
304        format!("{}/{}", self.namespace, self.name)
305    }
306}
307
308impl cljrs_gc::Trace for Var {
309    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
310        {
311            let value = self.value.lock().unwrap();
312            if let Some(v) = value.as_ref() {
313                v.trace(visitor);
314            }
315        }
316        {
317            let meta = self.meta.lock().unwrap();
318            if let Some(m) = meta.as_ref() {
319                m.trace(visitor);
320            }
321        }
322        {
323            let watches = self.watches.lock().unwrap();
324            for (key, f) in watches.iter() {
325                key.trace(visitor);
326                f.trace(visitor);
327            }
328        }
329    }
330}
331
332// ── Atom ──────────────────────────────────────────────────────────────────────
333
334/// A Clojure atom — a thread-safe mutable reference.
335#[derive(Debug)]
336pub struct Atom {
337    pub value: Mutex<Value>,
338    pub meta: Mutex<Option<Value>>,
339    pub validator: Mutex<Option<Value>>,
340    pub watches: Mutex<Vec<(Value, Value)>>,
341}
342
343impl Atom {
344    pub fn new(v: Value) -> Self {
345        // Heap-promotion fallback (GC builds): an atom is program-lifetime
346        // shared state, so its initial value must not be region-allocated.
347        let v = crate::publish::publish_value(v);
348        Self {
349            value: Mutex::new(v),
350            meta: Mutex::new(None),
351            validator: Mutex::new(None),
352            watches: Mutex::new(Vec::new()),
353        }
354    }
355
356    pub fn deref(&self) -> Value {
357        self.value.lock().unwrap().clone()
358    }
359
360    pub fn reset(&self, v: Value) -> Value {
361        // In no-gc debug builds: assert the new value came from the StaticArena.
362        #[cfg(all(feature = "no-gc", debug_assertions))]
363        debug_assert!(
364            value_gcptr_is_static(&v),
365            "no-gc: Atom::reset() received a region-local value — the new-value \
366             expression must be computed inside a StaticCtxGuard (i.e. inside \
367             the swap! / reset! call) so it is allocated in the static arena"
368        );
369        // GC builds: heap-promotion fallback (see `Var::bind`).
370        let v = crate::publish::publish_value(v);
371        let mut guard = self.value.lock().unwrap();
372        *guard = v.clone();
373        v
374    }
375
376    pub fn get_meta(&self) -> Option<Value> {
377        self.meta.lock().unwrap().clone()
378    }
379
380    pub fn set_meta(&self, m: Option<Value>) {
381        *self.meta.lock().unwrap() = m;
382    }
383
384    pub fn get_validator(&self) -> Option<Value> {
385        self.validator.lock().unwrap().clone()
386    }
387
388    pub fn set_validator(&self, vf: Option<Value>) {
389        *self.validator.lock().unwrap() = vf;
390    }
391}
392
393impl cljrs_gc::Trace for Atom {
394    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
395        {
396            let value = self.value.lock().unwrap();
397            value.trace(visitor);
398        }
399        {
400            let meta = self.meta.lock().unwrap();
401            if let Some(m) = meta.as_ref() {
402                m.trace(visitor);
403            }
404        }
405        {
406            let validator = self.validator.lock().unwrap();
407            if let Some(vf) = validator.as_ref() {
408                vf.trace(visitor);
409            }
410        }
411        {
412            let watches = self.watches.lock().unwrap();
413            for (key, f) in watches.iter() {
414                key.trace(visitor);
415                f.trace(visitor);
416            }
417        }
418    }
419}
420
421// ── Namespace ─────────────────────────────────────────────────────────────────
422
423/// A Clojure namespace with intern table, refers, and aliases.
424#[derive(Debug)]
425pub struct Namespace {
426    pub name: Arc<str>,
427    /// Vars interned directly in this namespace.
428    pub interns: Mutex<HashMap<Arc<str>, GcPtr<Var>>>,
429    /// Vars referred from other namespaces (e.g. clojure.core).
430    pub refers: Mutex<HashMap<Arc<str>, GcPtr<Var>>>,
431    /// Namespace aliases: short-name → full namespace name.
432    pub aliases: Mutex<HashMap<Arc<str>, Arc<str>>>,
433    /// Absolute path of the source file this namespace was loaded from,
434    /// populated by the loader.  Used by the versioned resolver to locate the
435    /// file for `git show`.
436    pub source_file: Mutex<Option<Arc<str>>>,
437    /// Absolute path of the git repository root that contains `source_file`.
438    pub git_repo_root: Mutex<Option<Arc<str>>>,
439    /// `true` for namespaces loaded from a specific commit (`name@hash`).
440    /// Versioned namespaces are immutable: `intern()` will refuse new bindings.
441    pub is_versioned: bool,
442}
443
444impl Namespace {
445    pub fn new(name: impl Into<Arc<str>>) -> Self {
446        Self {
447            name: name.into(),
448            interns: Mutex::new(HashMap::new()),
449            refers: Mutex::new(HashMap::new()),
450            aliases: Mutex::new(HashMap::new()),
451            source_file: Mutex::new(None),
452            git_repo_root: Mutex::new(None),
453            is_versioned: false,
454        }
455    }
456
457    /// Create a versioned (immutable) namespace for `name@commit`.
458    pub fn new_versioned(name: impl Into<Arc<str>>) -> Self {
459        Self {
460            is_versioned: true,
461            ..Self::new(name)
462        }
463    }
464
465    /// Record the source file path and its git repo root (if in a repo).
466    pub fn set_source_location(&self, file: &str, repo_root: Option<&str>) {
467        *self.source_file.lock().unwrap() = Some(Arc::from(file));
468        *self.git_repo_root.lock().unwrap() = repo_root.map(Arc::from);
469    }
470}
471
472impl cljrs_gc::Trace for Namespace {
473    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
474        use cljrs_gc::GcVisitor as _;
475        {
476            let interns = self.interns.lock().unwrap();
477            for var in interns.values() {
478                visitor.visit(var);
479            }
480        }
481        {
482            let refers = self.refers.lock().unwrap();
483            for var in refers.values() {
484                visitor.visit(var);
485            }
486        }
487    }
488}
489
490// ── NativeFn ──────────────────────────────────────────────────────────────────
491
492/// A Rust function callable from Clojure.
493/// Legacy type alias kept for source compatibility. Bare `fn` pointers
494/// implement `Fn` and can be passed anywhere a `NativeFnFunc` is expected.
495pub type NativeFnPtr = fn(&[Value]) -> crate::error::ValueResult<Value>;
496
497/// The callable stored inside a `NativeFn`. Supports both bare function
498/// pointers and closures that capture state.
499pub type NativeFnFunc = Arc<dyn Fn(&[Value]) -> crate::error::ValueResult<Value>>;
500
501#[derive(Clone, Debug)]
502pub enum Arity {
503    Fixed(usize),
504    Variadic { min: usize },
505}
506
507pub struct NativeFn {
508    pub name: Arc<str>,
509    pub arity: Arity,
510    pub func: NativeFnFunc,
511}
512
513impl NativeFn {
514    /// Create from a bare function pointer (backwards-compatible).
515    pub fn new(name: impl Into<Arc<str>>, arity: Arity, func: NativeFnPtr) -> Self {
516        Self {
517            name: name.into(),
518            arity,
519            func: Arc::new(func),
520        }
521    }
522
523    /// Create from a closure or any `Fn(&[Value]) -> ValueResult<Value>`.
524    pub fn with_closure(
525        name: impl Into<Arc<str>>,
526        arity: Arity,
527        func: impl Fn(&[Value]) -> crate::error::ValueResult<Value> + 'static,
528    ) -> Self {
529        Self {
530            name: name.into(),
531            arity,
532            func: Arc::new(func),
533        }
534    }
535}
536
537impl std::fmt::Debug for NativeFn {
538    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
539        f.debug_struct("NativeFn")
540            .field("name", &self.name)
541            .field("arity", &self.arity)
542            .field("func", &"<fn>")
543            .finish()
544    }
545}
546
547impl cljrs_gc::Trace for NativeFn {
548    fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
549}
550
551// ── CljxFnArity ───────────────────────────────────────────────────────────────
552
553/// One arity branch of a Clojure function.
554#[derive(Debug, Clone)]
555pub struct CljxFnArity {
556    /// Simple parameter names (no `&`).
557    /// For destructured params, these are gensym'd names.
558    pub params: Vec<Arc<str>>,
559    /// The name after `&`, if any.
560    pub rest_param: Option<Arc<str>>,
561    /// The body forms for this arity.
562    pub body: Vec<Form>,
563    /// Destructuring patterns: (param_index, original_form).
564    /// After binding the gensym'd param, these patterns are applied
565    /// via `bind_pattern` to destructure the value.
566    pub destructure_params: Vec<(usize, Form)>,
567    /// If the rest param is destructured, the original form.
568    pub destructure_rest: Option<Form>,
569    /// Unique ID for IR cache lookup (assigned by the evaluator).
570    pub ir_arity_id: u64,
571    /// Optional primitive type hint per positional parameter (parallel to
572    /// `params`).  `^long x` → `Some(TypeHint::Long)`; an un-hinted or
573    /// non-primitive-tagged param → `None`.  Drives unboxed codegen.
574    pub param_hints: Vec<Option<TypeHint>>,
575    /// Primitive type hint on the rest param, if any (rarely useful, but parsed
576    /// for symmetry).
577    pub rest_hint: Option<TypeHint>,
578}
579
580impl CljxFnArity {
581    /// Heap bytes owned by this arity, not counting the `CljxFnArity` struct itself.
582    pub fn heap_size(&self) -> usize {
583        // params Vec buffer (Arc<str> pointers; the str data is shared, skip it)
584        self.params.capacity() * mem::size_of::<Arc<str>>()
585        // body: the dominant consumer — Form AST trees stored inline
586        + self.body.capacity() * mem::size_of::<Form>()
587        + self.body.iter().map(|f| f.heap_size()).sum::<usize>()
588        // destructure_params
589        + self.destructure_params.capacity() * mem::size_of::<(usize, Form)>()
590        + self.destructure_params.iter().map(|(_, f)| f.heap_size()).sum::<usize>()
591        // destructure_rest
592        + self.destructure_rest.as_ref()
593            .map_or(0, |f| mem::size_of::<Form>() + f.heap_size())
594        // param_hints (Copy elements, no nested heap)
595        + self.param_hints.capacity() * mem::size_of::<Option<TypeHint>>()
596    }
597}
598
599// ── CljxFn ────────────────────────────────────────────────────────────────────
600
601/// An interpreted Clojure closure with captured environment.
602#[derive(Debug, Clone)]
603pub struct CljxFn {
604    pub name: Option<Arc<str>>,
605    pub arities: Vec<CljxFnArity>,
606    /// Names of closed-over bindings (parallel to `closed_over_vals`).
607    pub closed_over_names: Vec<Arc<str>>,
608    /// Values of closed-over bindings (parallel to `closed_over_names`).
609    pub closed_over_vals: Vec<Value>,
610    /// True if this function was defined with `defmacro`.
611    pub is_macro: bool,
612    /// True if this function carries `^:async` metadata. When an async runtime
613    /// (`cljrs-async`) is registered, calling such a function spawns its body as
614    /// a task and returns a `Value::Future` immediately instead of running it
615    /// synchronously. Without a runtime it runs synchronously like any other fn.
616    pub is_async: bool,
617    /// Namespace in which this function was defined (for macro hygiene).
618    pub defining_ns: Arc<str>,
619}
620
621impl CljxFn {
622    pub fn new(
623        name: Option<Arc<str>>,
624        arities: Vec<CljxFnArity>,
625        closed_over_names: Vec<Arc<str>>,
626        closed_over_vals: Vec<Value>,
627        is_macro: bool,
628        defining_ns: Arc<str>,
629    ) -> Self {
630        Self {
631            name,
632            arities,
633            closed_over_names,
634            closed_over_vals,
635            is_macro,
636            is_async: false,
637            defining_ns,
638        }
639    }
640}
641
642impl cljrs_gc::Trace for CljxFn {
643    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
644        for v in &self.closed_over_vals {
645            v.trace(visitor);
646        }
647    }
648
649    fn gc_size_extra(&self) -> usize {
650        // Vec<CljxFnArity> buffer + each arity's inline-owned heap
651        self.arities.capacity() * mem::size_of::<CljxFnArity>()
652            + self
653                .arities
654                .iter()
655                .map(CljxFnArity::heap_size)
656                .sum::<usize>()
657    }
658}
659
660// ── BoundFn ──────────────────────────────────────────────────────────────────
661
662/// A function wrapped with captured dynamic bindings.
663/// When called, the captured bindings are pushed as a frame before delegating
664/// to the wrapped function. This means captured bindings override the caller's
665/// for the same var, but vars not in the capture fall through normally.
666#[derive(Debug)]
667pub struct BoundFn {
668    /// The wrapped callable.
669    pub wrapped: Value,
670    /// Captured dynamic bindings (merged flat frame; opaque to cljrs-value).
671    pub captured_bindings: HashMap<usize, Value>,
672}
673
674impl cljrs_gc::Trace for BoundFn {
675    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
676        self.wrapped.trace(visitor);
677        for val in self.captured_bindings.values() {
678            val.trace(visitor);
679        }
680    }
681
682    fn gc_size_extra(&self) -> usize {
683        // HashMap<usize, Value>: hashbrown open-addressing, ~1 control byte + entry per slot.
684        self.captured_bindings.capacity() * (1 + mem::size_of::<usize>() + mem::size_of::<Value>())
685    }
686}
687
688// ── Thunk / LazySeq ───────────────────────────────────────────────────────────
689
690/// A deferred computation that produces a `Value` when forced.
691pub trait Thunk: std::fmt::Debug + cljrs_gc::Trace {
692    fn force(&self) -> Result<Value, String>;
693}
694
695/// Internal state of a lazy sequence cell.
696pub enum LazySeqState {
697    /// Thunk not yet evaluated.
698    Pending(Box<dyn Thunk>),
699    /// Result cached after first force.
700    Forced(Value),
701    /// Thunk evaluation failed; error message is cached.
702    Error(String),
703}
704
705/// A lazy sequence that forces its thunk exactly once and caches the result.
706pub struct LazySeq {
707    pub state: Mutex<LazySeqState>,
708}
709
710impl LazySeq {
711    pub fn new(thunk: Box<dyn Thunk>) -> Self {
712        Self {
713            state: Mutex::new(LazySeqState::Pending(thunk)),
714        }
715    }
716
717    /// Realize the sequence: force the thunk on first call, return cached value on subsequent calls.
718    /// On error, returns `Value::Nil` and caches the error (retrievable via `error()`).
719    pub fn realize(&self) -> Value {
720        let thunk = {
721            let mut guard = self.state.lock().unwrap();
722            match &*guard {
723                LazySeqState::Forced(v) => return v.clone(),
724                LazySeqState::Error(_) => return Value::Nil,
725                LazySeqState::Pending(_) => {}
726            }
727            // Replace the pending state with a temporary Forced(Nil), extract the thunk.
728            let prev = mem::replace(&mut *guard, LazySeqState::Forced(Value::Nil));
729            let LazySeqState::Pending(thunk) = prev else {
730                unreachable!("state was not Pending")
731            };
732            thunk
733            // guard dropped here — lock released before forcing
734        };
735        // Force the thunk WITHOUT holding the lock. This ensures GC's
736        // lock().unwrap() in LazySeq::trace() will not deadlock.
737        match thunk.force() {
738            Ok(result) => {
739                *self.state.lock().unwrap() = LazySeqState::Forced(result.clone());
740                result
741            }
742            Err(msg) => {
743                *self.state.lock().unwrap() = LazySeqState::Error(msg);
744                Value::Nil
745            }
746        }
747    }
748
749    /// Return the cached error message, if the thunk failed.
750    pub fn error(&self) -> Option<String> {
751        let guard = self.state.lock().unwrap();
752        if let LazySeqState::Error(e) = &*guard {
753            Some(e.clone())
754        } else {
755            None
756        }
757    }
758}
759
760impl std::fmt::Debug for LazySeq {
761    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
762        write!(f, "LazySeq(...)")
763    }
764}
765
766impl cljrs_gc::Trace for LazySeq {
767    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
768        // Safe to lock unconditionally: realize() drops the lock before entering
769        // eval (thunk.force()), so the lock is never held across a GC safepoint.
770        {
771            let state = self.state.lock().unwrap();
772            match &*state {
773                LazySeqState::Pending(thunk) => thunk.trace(visitor),
774                LazySeqState::Forced(v) => v.trace(visitor),
775                LazySeqState::Error(_) => {}
776            }
777        }
778    }
779}
780
781// ── CljxCons ──────────────────────────────────────────────────────────────────
782
783/// A lazy cons cell: head element + tail (may be a `LazySeq`, `List`, or `Nil`).
784///
785/// Used when `cons` is called with a `LazySeq` or `Cons` tail, enabling lazy
786/// sequences without eagerly realizing them.
787#[derive(Debug, Clone)]
788pub struct CljxCons {
789    pub head: Value,
790    pub tail: Value,
791}
792
793impl cljrs_gc::Trace for CljxCons {
794    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
795        self.head.trace(visitor);
796        self.tail.trace(visitor);
797    }
798}
799
800// ── Volatile ──────────────────────────────────────────────────────────────────
801
802/// Non-atomic mutable cell (single-thread performance, no CAS).
803pub struct Volatile {
804    pub value: Mutex<Value>,
805}
806
807impl Volatile {
808    pub fn new(v: Value) -> Self {
809        // GC builds: heap-promotion fallback (see `Var::bind`).
810        let v = crate::publish::publish_value(v);
811        Self {
812            value: Mutex::new(v),
813        }
814    }
815
816    pub fn deref(&self) -> Value {
817        self.value.lock().unwrap().clone()
818    }
819
820    pub fn reset(&self, v: Value) -> Value {
821        // In no-gc debug builds: assert the new value came from the StaticArena.
822        #[cfg(all(feature = "no-gc", debug_assertions))]
823        debug_assert!(
824            value_gcptr_is_static(&v),
825            "no-gc: Volatile::reset() received a region-local value — ensure the \
826             new-value expression is inside a StaticCtxGuard (vreset! handles this)"
827        );
828        // GC builds: heap-promotion fallback (see `Var::bind`).
829        let v = crate::publish::publish_value(v);
830        *self.value.lock().unwrap() = v.clone();
831        v
832    }
833}
834
835impl std::fmt::Debug for Volatile {
836    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
837        write!(f, "Volatile")
838    }
839}
840
841impl cljrs_gc::Trace for Volatile {
842    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
843        {
844            let value = self.value.lock().unwrap();
845            value.trace(visitor);
846        }
847    }
848}
849
850// ── Delay ─────────────────────────────────────────────────────────────────────
851
852/// Internal state of a delay cell.
853pub enum DelayState {
854    Pending(Box<dyn Thunk>),
855    Forced(Value),
856}
857
858/// A lazy one-time computation (forced at most once, result cached).
859pub struct Delay {
860    pub state: Mutex<DelayState>,
861}
862
863impl Delay {
864    pub fn new(thunk: Box<dyn Thunk>) -> Self {
865        Self {
866            state: Mutex::new(DelayState::Pending(thunk)),
867        }
868    }
869
870    /// Force the delay and cache the result.
871    /// Returns the value on success, or an error message on failure.
872    pub fn force(&self) -> Result<Value, String> {
873        let thunk = {
874            let mut guard = self.state.lock().unwrap();
875            if let DelayState::Forced(v) = &*guard {
876                return Ok(v.clone());
877            }
878            let prev = mem::replace(&mut *guard, DelayState::Forced(Value::Nil));
879            let DelayState::Pending(thunk) = prev else {
880                unreachable!("state was not Pending")
881            };
882            thunk
883            // guard dropped here — lock released before forcing
884        };
885        // Force the thunk WITHOUT holding the lock so GC's lock().unwrap() in
886        // Delay::trace() will not deadlock.
887        let result = thunk.force()?;
888        *self.state.lock().unwrap() = DelayState::Forced(result.clone());
889        Ok(result)
890    }
891
892    /// True if the delay has already been forced.
893    pub fn is_realized(&self) -> bool {
894        matches!(&*self.state.lock().unwrap(), DelayState::Forced(_))
895    }
896}
897
898impl std::fmt::Debug for Delay {
899    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
900        write!(f, "Delay")
901    }
902}
903
904impl cljrs_gc::Trace for Delay {
905    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
906        // Safe to lock unconditionally: force() drops the lock before entering
907        // eval (thunk.force()), so the lock is never held across a GC safepoint.
908        {
909            let state = self.state.lock().unwrap();
910            match &*state {
911                DelayState::Pending(thunk) => thunk.trace(visitor),
912                DelayState::Forced(v) => v.trace(visitor),
913            }
914        }
915    }
916}
917
918// ── CljxPromise ───────────────────────────────────────────────────────────────
919
920/// A one-shot rendezvous (promise).
921pub struct CljxPromise {
922    pub value: Mutex<Option<Value>>,
923    pub cond: Condvar,
924}
925
926impl CljxPromise {
927    pub fn new() -> Self {
928        Self {
929            value: Mutex::new(None),
930            cond: Condvar::new(),
931        }
932    }
933
934    /// Deliver a value (no-op if already delivered).
935    pub fn deliver(&self, v: Value) {
936        // GC builds: heap-promotion fallback — the promise may outlive (and be
937        // read from outside) any region scope active at delivery time.
938        let v = crate::publish::publish_value(v);
939        let mut guard = self.value.lock().unwrap();
940        if guard.is_none() {
941            *guard = Some(v);
942            self.cond.notify_all();
943        }
944    }
945
946    /// Block until a value is available, then return it.
947    pub fn deref_blocking(&self) -> Value {
948        let mut guard = self.value.lock().unwrap();
949        while guard.is_none() {
950            guard = self.cond.wait(guard).unwrap();
951        }
952        guard.as_ref().unwrap().clone()
953    }
954
955    /// True if already delivered.
956    pub fn is_realized(&self) -> bool {
957        self.value.lock().unwrap().is_some()
958    }
959}
960
961impl Default for CljxPromise {
962    fn default() -> Self {
963        Self::new()
964    }
965}
966
967impl std::fmt::Debug for CljxPromise {
968    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
969        write!(f, "Promise")
970    }
971}
972
973impl cljrs_gc::Trace for CljxPromise {
974    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
975        {
976            let value = self.value.lock().unwrap();
977            if let Some(v) = value.as_ref() {
978                v.trace(visitor);
979            }
980        }
981    }
982}
983
984// ── CljxFuture ────────────────────────────────────────────────────────────────
985
986/// Thread-pool future state.
987pub enum FutureState {
988    Running,
989    Done(Value),
990    /// The future's body threw. Holds the thrown Clojure value (a
991    /// `Value::Error`) so `await`/`deref` can re-throw it with its
992    /// `ex-data`/`ex-cause` intact, rather than a stringified message.
993    Failed(Value),
994    Cancelled,
995}
996
997/// A future value computed asynchronously on another thread.
998pub struct CljxFuture {
999    pub state: Mutex<FutureState>,
1000    pub cond: Condvar,
1001    /// Set once a consumer has read the settled result (via `await`/`deref`).
1002    /// Used to warn about a `Failed` future that is discarded without anyone
1003    /// ever observing its error (the fire-and-forget footgun).
1004    observed: std::sync::atomic::AtomicBool,
1005}
1006
1007impl CljxFuture {
1008    pub fn new() -> Self {
1009        Self {
1010            state: Mutex::new(FutureState::Running),
1011            cond: Condvar::new(),
1012            observed: std::sync::atomic::AtomicBool::new(false),
1013        }
1014    }
1015
1016    /// True if done, failed, or cancelled (not still running).
1017    pub fn is_done(&self) -> bool {
1018        !matches!(&*self.state.lock().unwrap(), FutureState::Running)
1019    }
1020
1021    /// True if explicitly cancelled.
1022    pub fn is_cancelled(&self) -> bool {
1023        matches!(&*self.state.lock().unwrap(), FutureState::Cancelled)
1024    }
1025
1026    /// Mark this future's result as observed. Call when a consumer reads the
1027    /// settled value (`await`/`deref`), so a later drop doesn't warn about an
1028    /// unobserved error.
1029    pub fn mark_observed(&self) {
1030        self.observed
1031            .store(true, std::sync::atomic::Ordering::Relaxed);
1032    }
1033}
1034
1035impl Drop for CljxFuture {
1036    fn drop(&mut self) {
1037        // Warn if a future failed but nobody ever observed the error — the
1038        // fire-and-forget case where a thrown error would otherwise vanish.
1039        // Tied to GC sweep timing: only fires once the future is unreachable,
1040        // so a not-yet-awaited (still reachable) failed future won't warn.
1041        //
1042        // SAFETY: this Drop can run during GC sweep. The thrown value held in
1043        // `Failed(v)` is itself a GC value whose backing box may be freed in
1044        // the *same* sweep, so we must NOT dereference it here (no `{v}`). We
1045        // only inspect the state discriminant, which is inline in our own
1046        // (still-valid) allocation.
1047        if !self.observed.load(std::sync::atomic::Ordering::Relaxed)
1048            && let Ok(state) = self.state.lock()
1049            && matches!(&*state, FutureState::Failed(_))
1050        {
1051            eprintln!(
1052                "[clojurust warning] a failed future was discarded without its error \
1053                 being observed (no await/deref); the thrown exception was lost"
1054            );
1055        }
1056    }
1057}
1058
1059impl Default for CljxFuture {
1060    fn default() -> Self {
1061        Self::new()
1062    }
1063}
1064
1065impl std::fmt::Debug for CljxFuture {
1066    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1067        write!(f, "Future")
1068    }
1069}
1070
1071impl cljrs_gc::Trace for CljxFuture {
1072    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1073        {
1074            let state = self.state.lock().unwrap();
1075            // Both Done and Failed hold a Value (the result or the thrown
1076            // error); trace either so the GC keeps it alive until observed.
1077            if let FutureState::Done(v) | FutureState::Failed(v) = &*state {
1078                v.trace(visitor);
1079            }
1080        }
1081    }
1082}
1083
1084// ── Agent ─────────────────────────────────────────────────────────────────────
1085
1086/// A Clojure agent — asynchronous state update queue (stub: not yet implemented).
1087pub struct Agent {
1088    /// Current state.
1089    pub state: Arc<Mutex<Value>>,
1090    /// Last error.
1091    pub error: Arc<Mutex<Option<Value>>>,
1092    pub watches: Mutex<Vec<(Value, Value)>>,
1093}
1094
1095impl Agent {
1096    pub fn get_state(&self) -> Value {
1097        self.state.lock().unwrap().clone()
1098    }
1099
1100    pub fn get_error(&self) -> Option<Value> {
1101        self.error.lock().unwrap().clone()
1102    }
1103
1104    pub fn clear_error(&self) {
1105        *self.error.lock().unwrap() = None;
1106    }
1107}
1108
1109impl std::fmt::Debug for Agent {
1110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1111        write!(f, "Agent")
1112    }
1113}
1114
1115impl cljrs_gc::Trace for Agent {
1116    fn trace(&self, visitor: &mut cljrs_gc::MarkVisitor) {
1117        {
1118            let state = self.state.lock().unwrap();
1119            state.trace(visitor);
1120        }
1121        {
1122            let error = self.error.lock().unwrap();
1123            if let Some(e) = error.as_ref() {
1124                e.trace(visitor);
1125            }
1126        }
1127        {
1128            let watches = self.watches.lock().unwrap();
1129            for (key, f) in watches.iter() {
1130                key.trace(visitor);
1131                f.trace(visitor);
1132            }
1133        }
1134    }
1135}