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}