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