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