Skip to main content

cljrs_value/
lib.rs

1#![allow(clippy::arc_with_non_send_sync)]
2pub mod clone;
3pub mod collections;
4pub mod error;
5pub mod hash;
6pub mod intern;
7pub mod jit_hooks;
8pub mod keyword;
9pub mod native_object;
10pub mod parking;
11pub mod publish;
12pub mod realization;
13pub mod regex;
14pub mod resource;
15pub mod shared;
16pub mod symbol;
17pub mod type_hint;
18pub mod types;
19pub mod value;
20
21pub use collections::{
22    PersistentArrayMap, PersistentHashMap, PersistentHashSet, PersistentList, PersistentQueue,
23    PersistentVector, SortedMap, SortedSet,
24};
25pub use error::{ExceptionInfo, ValueError, ValueResult};
26pub use hash::ClojureHash;
27pub use intern::{intern_keyword, intern_symbol};
28pub use jit_hooks::set_var_rebind_hook;
29pub use keyword::Keyword;
30pub use native_object::{NativeObject, NativeObjectBox, gc_native_object};
31pub use resource::{Resource, ResourceHandle};
32pub use shared::{ByteBlob, PromoteError, SharedAtom, SharedSlot, SharedValue, demote, promote};
33pub use symbol::Symbol;
34pub use type_hint::TypeHint;
35pub use types::{
36    Agent, Arity, Atom, BoundFn, CljxCons, CljxFn, CljxFnArity, CljxFuture, CljxPromise, Delay,
37    DelayState, FUTURE_CANCELLED_MSG, FutureState, LazySeq, MultiFn, Namespace, NativeFn,
38    NativeFnFunc, NativeFnPtr, Protocol, ProtocolFn, ProtocolMethod, ReferClojureFilter, Thunk,
39    Var, Volatile, bump_protocol_generation, protocol_generation,
40};
41pub use value::{MapValue, ObjectArray, SetValue, TypeInstance, Value};
42
43#[cfg(all(test, feature = "no-gc"))]
44mod publication_review_tests {
45    use super::*;
46    use cljrs_gc::GcPtr;
47    struct Probe(std::rc::Rc<std::cell::Cell<bool>>);
48    impl cljrs_gc::Trace for Probe {
49        fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
50    }
51    impl Drop for Probe {
52        fn drop(&mut self) {
53            self.0.set(true);
54        }
55    }
56    #[derive(Debug)]
57    struct Counted {
58        traces: std::rc::Rc<std::cell::Cell<usize>>,
59        dropped: std::rc::Rc<std::cell::Cell<bool>>,
60    }
61    impl cljrs_gc::Trace for Counted {
62        fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {
63            self.traces.set(self.traces.get() + 1);
64        }
65    }
66    impl NativeObject for Counted {
67        fn type_tag(&self) -> &str {
68            "Counted"
69        }
70        fn as_any(&self) -> &dyn std::any::Any {
71            self
72        }
73    }
74    impl Drop for Counted {
75        fn drop(&mut self) {
76            self.dropped.set(true);
77        }
78    }
79    #[test]
80    fn cached_cell_snapshots_do_not_retrace_or_permanently_pin_payloads() {
81        let atom = GcPtr::new(Atom::new(Value::Nil));
82        let traces = std::rc::Rc::new(std::cell::Cell::new(0));
83        let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
84        {
85            let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
86            let counted = Value::NativeObject(gc_native_object(Counted {
87                traces: traces.clone(),
88                dropped: dropped.clone(),
89            }));
90            let map = Value::Map(MapValue::from_pairs(
91                (0..1000)
92                    .map(|i| (Value::Long(i), counted.clone()))
93                    .collect(),
94            ));
95            atom.get().reset(map);
96        }
97        let initial = traces.get();
98        assert_eq!(initial, 1);
99        for _ in 0..1000 {
100            let scope = cljrs_gc::region::ReadScope::enter().unwrap();
101            let snapshot = atom.get().snapshot();
102            assert!(matches!(snapshot.value, Value::Map(_)));
103            atom.get().deref();
104            drop(scope);
105        }
106        assert_eq!(traces.get(), initial, "reads walked the full map graph");
107        atom.get().reset(Value::Nil);
108        assert!(
109            dropped.get(),
110            "top-level reads permanently pinned the old payload"
111        );
112    }
113
114    #[test]
115    fn stored_scalar_boxes_preserve_identity_and_release_with_the_last_alias() {
116        let atom = GcPtr::new(Atom::new(Value::Nil));
117        let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
118        {
119            let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
120            let _temporary = GcPtr::new(Probe(dropped.clone()));
121            let uuid = GcPtr::new(crate::value::UuidValue(42));
122            let returned = atom.get().reset(Value::Uuid(uuid.clone()));
123            let Value::Uuid(returned) = returned else {
124                unreachable!()
125            };
126            assert!(!GcPtr::ptr_eq(&uuid, &returned));
127            assert!(GcPtr::identity_eq(&uuid, &returned));
128        }
129        assert!(
130            dropped.get(),
131            "a scalar kept unrelated source temporaries alive"
132        );
133        let snapshot = atom.get().snapshot();
134        let Value::Uuid(uuid) = &snapshot.value else {
135            unreachable!()
136        };
137        assert_eq!(uuid.get().0, 42);
138        atom.get().reset(Value::Nil);
139        assert_eq!(uuid.get().0, 42);
140        drop(snapshot);
141        assert!(dropped.get());
142    }
143
144    #[test]
145    fn exception_payloads_own_data_from_another_scope() {
146        let var = GcPtr::new(Var::new("user", "error-payload"));
147        let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
148        {
149            let _data = cljrs_gc::alloc_ctx::ScratchGuard::new();
150            let object = Value::NativeObject(gc_native_object(Counted {
151                traces: Default::default(),
152                dropped: dropped.clone(),
153            }));
154            let data = MapValue::from_pairs(vec![(Value::Long(1), object)]);
155            let _error = cljrs_gc::alloc_ctx::ScratchGuard::new();
156            var.get().bind(Value::Error(GcPtr::new(ExceptionInfo::new(
157                ValueError::Other("owned".into()),
158                "owned".into(),
159                Some(data),
160                None,
161            ))));
162        }
163        assert!(!dropped.get(), "the error lost its data region");
164        {
165            let _reads = cljrs_gc::region::ReadScope::nested();
166            let Value::Error(error) = var.get().deref().unwrap() else {
167                panic!("expected error")
168            };
169            var.get().bind(Value::Nil);
170            assert!(matches!(
171                error.get().data().unwrap().get(&Value::Long(1)),
172                Some(Value::NativeObject(_))
173            ));
174        }
175        assert!(dropped.get());
176    }
177
178    #[test]
179    fn definition_roots_release_initializer_temporaries() {
180        for atom in [false, true] {
181            let var = GcPtr::new(Var::new("user", "packed"));
182            let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
183            {
184                let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
185                let _temporary = GcPtr::new(Probe(dropped.clone()));
186                let payload =
187                    Value::Vector(GcPtr::new(PersistentVector::from_iter([Value::Long(42)])));
188                let value = if atom {
189                    Value::Atom(GcPtr::new_isolated(Atom::new(payload.clone())))
190                } else {
191                    payload.clone()
192                };
193                var.get().bind(value);
194                if !atom {
195                    assert!(payload.identical(&var.get().deref().unwrap()));
196                }
197            }
198            assert!(
199                dropped.get(),
200                "a definition retained an unrelated initializer temporary"
201            );
202            let _reads = cljrs_gc::region::ReadScope::nested();
203            let value = var.get().deref().unwrap();
204            let value = match value {
205                Value::Atom(atom) => atom.get().deref(),
206                value => value,
207            };
208            assert_eq!(value.to_string(), "[42]");
209        }
210    }
211
212    #[test]
213    fn scalar_storage_uses_small_chunks_and_transient_returns_reuse_initial_ownership() {
214        let _mutator = cljrs_gc::register_mutator();
215        let array = GcPtr::new(ObjectArray::new(vec![Value::Nil; 1000]));
216        let retired =
217            || cljrs_gc::governor::with_current_account(|a| a.retired_region_bytes()).unwrap();
218        let before = retired();
219        for index in 0..1000 {
220            let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
221            let value = publish::publish_for_slot(
222                Value::string("x"),
223                array.get() as *const _ as usize,
224                index,
225            );
226            array.get().0.lock().unwrap()[index] = value;
227        }
228        assert!(
229            retired() - before <= 128 * 1000,
230            "scalar copies reserved full-sized scratch chunks"
231        );
232        for index in 0..1000 {
233            let value =
234                publish::publish_for_slot(Value::Nil, array.get() as *const _ as usize, index);
235            array.get().0.lock().unwrap()[index] = value;
236        }
237        let _scope = cljrs_gc::alloc_ctx::ScratchGuard::new();
238        let traces = std::rc::Rc::new(std::cell::Cell::new(0));
239        let value = Value::NativeObject(gc_native_object(Counted {
240            traces: traces.clone(),
241            dropped: Default::default(),
242        }));
243        let original = PersistentVector::from_iter(std::iter::repeat_n(value, 10000));
244        let transient = GcPtr::new(collections::TransientVector::from_persistent(&original));
245        let initial = traces.get();
246        assert_eq!(initial, 1, "initial ownership must be captured once");
247        for _ in 0..1000 {
248            publish::publish_return(Value::TransientVector(transient.clone()));
249        }
250        let _persistent = transient.get().persistent().unwrap();
251        assert_eq!(
252            traces.get(),
253            initial,
254            "transient returns retraced the initial graph"
255        );
256    }
257
258    #[test]
259    fn returned_updates_protect_shared_children_after_the_cell_is_replaced() {
260        let atom = GcPtr::new(Atom::new(Value::Nil));
261        let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
262        {
263            let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
264            let object = Value::NativeObject(gc_native_object(Counted {
265                traces: Default::default(),
266                dropped: dropped.clone(),
267            }));
268            atom.get().reset(Value::Map(MapValue::from_pairs(vec![(
269                Value::Long(0),
270                object,
271            )])));
272        }
273        {
274            let _consumer = cljrs_gc::alloc_ctx::ScratchGuard::new();
275            let previous = atom.get().snapshot();
276            let Value::Map(old) = &previous.value else {
277                unreachable!()
278            };
279            let returned = atom
280                .get()
281                .reset(Value::Map(old.assoc(Value::Long(1), Value::Long(2))));
282            drop(previous);
283            atom.get().reset(Value::Nil);
284            assert!(
285                !dropped.get(),
286                "replacing the cell freed a child of its returned value"
287            );
288            let Value::Map(returned) = returned else {
289                unreachable!()
290            };
291            let Value::NativeObject(child) = returned.get(&Value::Long(0)).unwrap() else {
292                unreachable!()
293            };
294            assert_eq!(child.get().type_tag(), "Counted");
295        }
296        assert!(dropped.get());
297    }
298
299    #[test]
300    fn metadata_validators_and_watches_release_replaced_callbacks() {
301        let _container = cljrs_gc::alloc_ctx::ScratchGuard::new();
302        let atom = GcPtr::new(Atom::new(Value::Nil));
303        for _ in 0..50 {
304            let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
305            {
306                let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
307                let callback = Value::NativeFunction(GcPtr::new(NativeFn::with_captures(
308                    "owned-callback",
309                    Arity::Fixed(1),
310                    vec![Value::NativeObject(gc_native_object(Counted {
311                        traces: Default::default(),
312                        dropped: dropped.clone(),
313                    }))],
314                    |captures, _| Ok(captures[0].clone()),
315                )));
316                atom.get().set_validator(Some(callback.clone()));
317                atom.get().set_meta(Some(callback.clone()));
318                atom.get().add_watch(Value::Long(1), callback);
319            }
320            assert!(!dropped.get());
321            {
322                let _reader = cljrs_gc::alloc_ctx::ScratchGuard::new();
323                let validator = atom.get().get_validator().unwrap();
324                assert!(matches!(validator, Value::NativeFunction(_)));
325                assert!(atom.get().get_meta().is_some());
326                atom.get().set_validator(None);
327                atom.get().set_meta(None);
328                atom.get().remove_watch(&Value::Long(1));
329                assert!(
330                    !dropped.get(),
331                    "a borrowed callback was freed during replacement"
332                );
333                let Value::NativeFunction(f) = validator else {
334                    unreachable!()
335                };
336                assert!(matches!(
337                    (f.get().func)(&[Value::Nil]).unwrap(),
338                    Value::NativeObject(_)
339                ));
340            }
341            assert!(
342                dropped.get(),
343                "removed auxiliary slots retained their source"
344            );
345        }
346    }
347
348    #[test]
349    fn durable_stores_outlive_an_active_read_scope() {
350        std::thread::spawn(|| {
351            let destination = cljrs_gc::region::AllocationSnapshot::capture();
352            let read = cljrs_gc::region::ReadScope::enter().unwrap();
353            let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
354            let value = {
355                let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
356                let value = Value::NativeObject(gc_native_object(Counted {
357                    traces: Default::default(),
358                    dropped: dropped.clone(),
359                }));
360                destination.protect(&value);
361                value
362            };
363            drop(read);
364            assert!(
365                !dropped.get(),
366                "a store was shortened to the reader lifetime"
367            );
368            let Value::NativeObject(value) = value else {
369                unreachable!()
370            };
371            assert_eq!(value.get().type_tag(), "Counted");
372        })
373        .join()
374        .unwrap();
375    }
376
377    #[test]
378    fn var_rebinding_releases_opaque_roots_after_the_last_reader() {
379        let var = GcPtr::new(Var::new("user", "root"));
380        for _ in 0..100 {
381            let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
382            {
383                let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
384                let _temporary = GcPtr::new(Probe(dropped.clone()));
385                var.get()
386                    .bind(Value::NativeFunction(GcPtr::new(NativeFn::with_captures(
387                        "root",
388                        Arity::Fixed(0),
389                        vec![Value::string("alive")],
390                        |captures, _| Ok(captures[0].clone()),
391                    ))));
392            }
393            let scope = cljrs_gc::region::ReadScope::enter().unwrap();
394            let alias = var.get().deref().unwrap();
395            var.get().bind(Value::Nil);
396            assert!(!dropped.get());
397            let Value::NativeFunction(f) = alias else {
398                unreachable!()
399            };
400            assert_eq!((f.get().func)(&[]).unwrap().to_string(), "\"alive\"");
401            drop(scope);
402            assert!(dropped.get(), "an obsolete Var root remained permanent");
403        }
404    }
405
406    #[test]
407    fn unscoped_host_reads_retain_raw_results_and_scoped_snapshots_release() {
408        std::thread::spawn(|| {
409            let atom = GcPtr::new(Atom::new(Value::Nil));
410            let var = GcPtr::new(Var::new("user", "host-read"));
411            {
412                let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
413                let value = Value::string("host payload");
414                atom.get().reset(value.clone());
415                var.get().bind(value);
416            }
417            let raw = atom.get().deref();
418            let root = var.get().deref().unwrap();
419            atom.get().reset(Value::Nil);
420            var.get().bind(Value::Nil);
421            assert_eq!(raw.to_string(), "\"host payload\"");
422            assert_eq!(root.to_string(), "\"host payload\"");
423        })
424        .join()
425        .unwrap();
426
427        let var = GcPtr::new(Var::new("user", "snapshot"));
428        let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
429        {
430            let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
431            var.get()
432                .bind(Value::NativeObject(gc_native_object(Counted {
433                    traces: Default::default(),
434                    dropped: dropped.clone(),
435                })));
436        }
437        let snapshot = var.get().snapshot().unwrap();
438        var.get().bind(Value::Nil);
439        assert!(!dropped.get());
440        drop(snapshot);
441        assert!(dropped.get());
442    }
443
444    #[test]
445    fn large_and_deep_definition_payloads_keep_their_storage() {
446        std::thread::Builder::new()
447            .stack_size(128 * 1024)
448            .spawn(|| {
449                let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
450                let large = Value::string("x".repeat(1024 * 1024));
451                let atom = Atom::new(large.clone());
452                let Value::Str(before) = large else {
453                    unreachable!()
454                };
455                let Value::Str(after) = atom.snapshot().value else {
456                    unreachable!()
457                };
458                assert!(GcPtr::ptr_eq(&before, &after), "large string was copied");
459                let large = Value::Vector(GcPtr::new(PersistentVector::from_iter(
460                    (0..10000).map(Value::Long),
461                )));
462                let atom = Atom::new(large.clone());
463                let Value::Vector(before) = large else {
464                    unreachable!()
465                };
466                let Value::Vector(after) = atom.snapshot().value else {
467                    unreachable!()
468                };
469                assert!(
470                    GcPtr::ptr_eq(&before, &after),
471                    "large shared vector was rebuilt"
472                );
473                let mut chain = Value::Nil;
474                for i in 0..30000 {
475                    chain = Value::Cons(GcPtr::new(CljxCons {
476                        head: Value::Long(i),
477                        tail: chain,
478                    }));
479                }
480                let atom = Atom::new(chain);
481                let mut current = atom.snapshot().value;
482                let mut count = 0;
483                while let Value::Cons(cons) = current {
484                    count += 1;
485                    current = cons.get().tail.clone();
486                }
487                assert_eq!(count, 30000);
488            })
489            .unwrap()
490            .join()
491            .unwrap();
492    }
493
494    #[test]
495    fn shared_captured_graphs_are_walked_only_once() {
496        let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
497        let traces = std::rc::Rc::new(std::cell::Cell::new(0));
498        let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
499        let object = Value::NativeObject(gc_native_object(Counted {
500            traces: traces.clone(),
501            dropped: dropped.clone(),
502        }));
503        let captured = Value::Vector(GcPtr::new(PersistentVector::from_iter(
504            std::iter::repeat_n(object, 10000),
505        )));
506        for _ in 0..1000 {
507            let _call = cljrs_gc::alloc_ctx::ScratchGuard::new();
508            let closure = GcPtr::new_isolated(NativeFn::with_captures(
509                "capture",
510                Arity::Fixed(0),
511                vec![captured.clone()],
512                |values, _| Ok(values[0].clone()),
513            ));
514            publish::publish_return(Value::NativeFunction(closure));
515        }
516        assert_eq!(
517            traces.get(),
518            1,
519            "closure creation retraced the shared graph"
520        );
521        assert!(!dropped.get());
522    }
523
524    #[test]
525    fn indexed_storage_releases_overwritten_sources_and_failed_deliveries() {
526        let _container = cljrs_gc::alloc_ctx::ScratchGuard::new();
527        let array = GcPtr::new(ObjectArray::new(vec![Value::Nil; 2]));
528        let mut previous: Option<std::rc::Rc<std::cell::Cell<bool>>> = None;
529        for _ in 0..100 {
530            let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
531            {
532                let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
533                let value = Value::NativeObject(gc_native_object(Counted {
534                    traces: Default::default(),
535                    dropped: dropped.clone(),
536                }));
537                let value = publish::publish_for_slot(value, array.get() as *const _ as usize, 0);
538                array.get().0.lock().unwrap()[0] = value;
539            }
540            if let Some(previous) = previous {
541                assert!(previous.get());
542            }
543            previous = Some(dropped);
544        }
545        let value = publish::publish_for_slot(Value::Nil, array.get() as *const _ as usize, 0);
546        array.get().0.lock().unwrap()[0] = value;
547        assert!(previous.unwrap().get());
548        let promise = CljxPromise::new();
549        promise.deliver(Value::Long(1));
550        let discarded = std::rc::Rc::new(std::cell::Cell::new(false));
551        {
552            let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
553            promise.deliver(Value::NativeObject(gc_native_object(Counted {
554                traces: Default::default(),
555                dropped: discarded.clone(),
556            })));
557        }
558        assert!(
559            discarded.get(),
560            "an unsuccessful promise delivery retained its candidate"
561        );
562        assert_eq!(promise.deref_blocking(), Value::Long(1));
563    }
564
565    #[test]
566    fn regional_cells_release_each_replaced_payload_without_changing_identity() {
567        let _container = cljrs_gc::alloc_ctx::ScratchGuard::new();
568        let atom = GcPtr::new(Atom::new(Value::Nil));
569        let other = GcPtr::new(Atom::new(Value::Nil));
570        for _ in 0..100 {
571            let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
572            {
573                let _source = cljrs_gc::alloc_ctx::ScratchGuard::new();
574                let _temporary = GcPtr::new(Probe(dropped.clone()));
575                let vector = GcPtr::new(PersistentVector::from_iter([Value::Long(1)]));
576                let returned = atom.get().reset(Value::Vector(vector.clone()));
577                let Value::Vector(returned) = returned else {
578                    unreachable!()
579                };
580                assert!(GcPtr::ptr_eq(&vector, &returned));
581                other.get().reset(Value::Vector(vector));
582            }
583            assert!(!dropped.get());
584            atom.get().reset(Value::Nil);
585            assert!(!dropped.get(), "another slot still owns the same source");
586            other.get().reset(Value::Nil);
587            assert!(
588                dropped.get(),
589                "obsolete regional payload kept its source alive"
590            );
591        }
592    }
593
594    #[test]
595    fn replacement_does_not_inherit_the_previous_payloads_read_lease() {
596        let _container = cljrs_gc::alloc_ctx::ScratchGuard::new();
597        let atom = GcPtr::new(Atom::new(Value::Nil));
598        let mut previous: Option<std::rc::Rc<std::cell::Cell<bool>>> = None;
599        for _ in 0..100 {
600            let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
601            {
602                let _iteration = cljrs_gc::alloc_ctx::ScratchGuard::new();
603                let _old = atom.get().deref();
604                atom.get()
605                    .reset(Value::NativeObject(gc_native_object(Counted {
606                        traces: Default::default(),
607                        dropped: dropped.clone(),
608                    })));
609            }
610            if let Some(old) = previous {
611                assert!(
612                    old.get(),
613                    "a read became a durable dependency of the replacement"
614                );
615            }
616            previous = Some(dropped);
617        }
618        atom.get().reset(Value::Nil);
619        assert!(previous.unwrap().get());
620    }
621
622    #[test]
623    fn static_cells_release_replaced_opaque_payloads_and_preserve_borrowed_aliases() {
624        let atom = GcPtr::new(Atom::new(Value::Nil));
625        let volatile = GcPtr::new(Volatile::new(Value::Nil));
626        for _ in 0..100 {
627            let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
628            {
629                let _scope = cljrs_gc::alloc_ctx::ScratchGuard::new();
630                let _temporary = GcPtr::new(Probe(dropped.clone()));
631                let payload = Value::NativeFunction(GcPtr::new(NativeFn::with_captures(
632                    "payload",
633                    Arity::Fixed(0),
634                    vec![Value::string("alive")],
635                    |captures, _| Ok(captures[0].clone()),
636                )));
637                atom.get().reset(payload.clone());
638                volatile.get().reset(payload);
639            }
640            assert!(!dropped.get());
641            {
642                let _consumer = cljrs_gc::alloc_ctx::ScratchGuard::new();
643                let alias = atom.get().deref();
644                atom.get().reset(Value::Nil);
645                volatile.get().reset(Value::Nil);
646                assert!(!dropped.get(), "a live alias lost its backing scope");
647                let Value::NativeFunction(f) = alias else {
648                    unreachable!()
649                };
650                assert_eq!((f.get().func)(&[]).unwrap().to_string(), "\"alive\"");
651            }
652            assert!(
653                dropped.get(),
654                "an obsolete opaque payload retained its scope"
655            );
656        }
657        let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
658        {
659            let _scope = cljrs_gc::alloc_ctx::ScratchGuard::new();
660            let _temporary = GcPtr::new(Probe(dropped.clone()));
661            let rejected =
662                Value::ObjectArray(GcPtr::new(ObjectArray(std::sync::Mutex::new(vec![]))));
663            assert!(!atom.get().compare_and_set(&Value::Long(1), rejected));
664        }
665        assert!(dropped.get(), "a failed CAS retained its candidate");
666    }
667
668    #[test]
669    fn opaque_publication_preserves_identity_and_releases_unrelated_scopes() {
670        let dropped = std::rc::Rc::new(std::cell::Cell::new(false));
671        let outer = cljrs_gc::alloc_ctx::ScratchGuard::new();
672        let probe = cljrs_gc::GcPtr::new(Probe(dropped.clone()));
673        let inner = cljrs_gc::alloc_ctx::ScratchGuard::new();
674        let array = cljrs_gc::GcPtr::new(ObjectArray(std::sync::Mutex::new(vec![Value::Long(1)])));
675        let value = Value::Vector(cljrs_gc::GcPtr::new(PersistentVector::from_iter(vec![
676            Value::ObjectArray(array.clone()),
677        ])));
678        let published = publish::publish_value(value);
679        drop(inner);
680        drop(probe);
681        drop(outer);
682        assert!(
683            dropped.get(),
684            "an unrelated region was permanently retained"
685        );
686        let Value::Vector(values) = published else {
687            panic!("not a vector")
688        };
689        let Value::ObjectArray(received) = values.get().iter().next().unwrap() else {
690            panic!("not an array")
691        };
692        assert!(cljrs_gc::GcPtr::ptr_eq(&array, received));
693        array.get().0.lock().unwrap()[0] = Value::Long(42);
694        assert_eq!(received.get().0.lock().unwrap()[0], Value::Long(42));
695    }
696}