Skip to main content

cljrs_value/
types.rs

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