Skip to main content

cljrs_gc/
lib.rs

1//! Garbage collector (default) or region-based allocator (`no-gc` feature) for clojurust.
2
3#![allow(clippy::missing_safety_doc)]
4#![allow(private_interfaces)]
5
6use std::ptr::NonNull;
7
8pub mod fiber;
9pub mod governor;
10pub mod mutator;
11pub mod region;
12pub mod stats;
13
14#[cfg(not(feature = "no-gc"))]
15pub mod cancellation;
16#[cfg(not(feature = "no-gc"))]
17pub mod config;
18
19#[cfg(feature = "no-gc")]
20pub mod alloc_ctx;
21#[cfg(feature = "no-gc")]
22pub mod static_arena;
23
24pub use governor::{
25    Admission, CriticalSection, IsolateAccount, IsolateControl, MemoryCharge, MemoryClass,
26    MemoryConfig, MemoryConfigError, MemoryLimitExceeded, MemorySnapshot, PressureLevel,
27    ProcessMemoryGovernor, ShedHookId, enter_critical, governor, register_current_isolate,
28    resolve_runtime_fault, with_critical,
29};
30pub use stats::{
31    CLJRS_GC_STATS_ENV, GC_STATS, GcStats, GcStatsSnapshot, dump_stats_from_env,
32    report as stats_report,
33};
34
35// ── Re-exports from active implementation ─────────────────────────────────────
36
37#[cfg(not(feature = "no-gc"))]
38pub use cancellation::{
39    MutatorGuard, StwGuard, begin_stw, check_cancellation, gc_requested, park_thread,
40    register_mutator, registered_threads, request_gc, safepoint, take_gc_request, unpark_thread,
41    wait_for_threads_to_park,
42};
43#[cfg(not(feature = "no-gc"))]
44pub use config::{GC_CANCELLATION as CONFIG_CANCELLATION, GcConfig, GcParked};
45
46#[cfg(not(feature = "no-gc"))]
47pub use gc_full::{
48    AllocRootGuard, GcHeap, HEAP, HeapProxy, push_alloc_frame, trace_thread_alloc_roots,
49};
50#[cfg(feature = "no-gc")]
51pub use nogc_stubs::{
52    AllocRootGuard, CONFIG_CANCELLATION, GcConfig, GcHeap, GcParked, HEAP, MutatorGuard, StwGuard,
53    begin_stw, check_cancellation, gc_requested, park_thread, push_alloc_frame, register_mutator,
54    registered_threads, request_gc, safepoint, take_gc_request, unpark_thread,
55    wait_for_threads_to_park,
56};
57
58/// Return `true` if `addr` was allocated by the global `StaticArena`.
59///
60/// Available only in `no-gc` debug builds.  Downstream crates (`cljrs-value`)
61/// use this to implement write-site provenance assertions.
62#[cfg(all(feature = "no-gc", debug_assertions))]
63pub fn is_static_addr(addr: usize) -> bool {
64    static_arena::is_static_addr(addr)
65}
66
67// ── Trace trait ───────────────────────────────────────────────────────────────
68
69/// Implemented by every type that can be stored behind a [`GcPtr`].
70///
71/// The `gc_size_extra` method accounts for heap bytes owned by the value that
72/// are NOT captured by `size_of::<GcBox<T>>()` (e.g. `Vec` buffers, `String`
73/// capacity, `Form` AST trees stored inline).  The default returns 0, which is
74/// correct for primitives and types with no out-of-line heap.
75///
76/// Rules for implementors of `gc_size_extra`:
77/// - Count only bytes THIS value owns and will free when dropped.
78/// - Do NOT cross `GcPtr` boundaries — each pointed-to box is counted
79///   separately when it is allocated.
80pub trait Trace {
81    /// Initialize independent no-GC ownership slots after allocation.
82    #[cfg(feature = "no-gc")]
83    fn init_ownership(&self) {}
84
85    /// Whether this object's managed child references never change after allocation.
86    /// No-GC publication can cache their ownership after its first graph walk.
87    #[cfg(feature = "no-gc")]
88    fn immutable_children(&self) -> bool {
89        false
90    }
91
92    fn trace(&self, visitor: &mut MarkVisitor);
93
94    fn gc_size_extra(&self) -> usize {
95        0
96    }
97}
98
99// ── Leaf Trace impls ──────────────────────────────────────────────────────────
100
101impl Trace for String {
102    fn trace(&self, _: &mut MarkVisitor) {}
103
104    fn gc_size_extra(&self) -> usize {
105        self.capacity()
106    }
107}
108impl Trace for i64 {
109    fn trace(&self, _: &mut MarkVisitor) {}
110}
111impl Trace for f64 {
112    fn trace(&self, _: &mut MarkVisitor) {}
113}
114impl Trace for bool {
115    fn trace(&self, _: &mut MarkVisitor) {}
116}
117impl Trace for num_bigint::BigInt {
118    fn trace(&self, _: &mut MarkVisitor) {}
119}
120impl Trace for bigdecimal::BigDecimal {
121    fn trace(&self, _: &mut MarkVisitor) {}
122}
123impl Trace for num_rational::Ratio<num_bigint::BigInt> {
124    fn trace(&self, _: &mut MarkVisitor) {}
125}
126macro_rules! impl_trace_prim_array {
127    ($t:ty) => {
128        impl Trace for std::sync::Mutex<Vec<$t>> {
129            fn trace(&self, _: &mut MarkVisitor) {}
130            fn gc_size_extra(&self) -> usize {
131                self.lock().unwrap().capacity() * std::mem::size_of::<$t>()
132            }
133        }
134    };
135}
136impl_trace_prim_array!(i32);
137impl_trace_prim_array!(i64);
138impl_trace_prim_array!(i16);
139impl_trace_prim_array!(i8);
140impl_trace_prim_array!(char);
141impl_trace_prim_array!(f64);
142impl_trace_prim_array!(f32);
143impl_trace_prim_array!(bool);
144
145// ── GcVisitor ─────────────────────────────────────────────────────────────────
146
147pub trait GcVisitor {
148    fn visit<T: Trace + 'static>(&mut self, ptr: &GcPtr<T>);
149}
150
151// =============================================================================
152// GC build: GcBox with header, MarkVisitor with grey stack
153// =============================================================================
154
155#[cfg(not(feature = "no-gc"))]
156pub use self::gc_header::{GcBox, GcBoxHeader};
157
158#[cfg(not(feature = "no-gc"))]
159mod gc_header {
160    use crate::{MarkVisitor, Trace};
161    use std::cell::Cell;
162
163    // Objects start at lives = GC_INITIAL_LIVES - 1.  The mark phase sets
164    // lives = GC_INITIAL_LIVES for reachable objects; sweep frees objects
165    // whose lives reach 0.  A value of 2 gives exactly one cycle of grace:
166    // enough to cover the window between an alloc frame dropping and the
167    // next GC safepoint (where VALUE_ROOTS or a new alloc frame re-roots it).
168    // 10 was chosen conservatively but keeps 9× more garbage in RAM than
169    // necessary, worsening OOM pressure under test suites with many forms.
170    pub(crate) const GC_INITIAL_LIVES: u8 = 2;
171
172    #[cfg(debug_assertions)]
173    pub(crate) const GC_MAGIC_ALIVE: u64 = 0xCAFE_BABE_DEAD_BEEF;
174    #[cfg(debug_assertions)]
175    pub(crate) const GC_MAGIC_FREED: u64 = 0xDEAD_DEAD_DEAD_DEAD;
176
177    #[repr(C)]
178    pub struct GcBoxHeader {
179        #[cfg(debug_assertions)]
180        pub(crate) magic: Cell<u64>,
181        /// Exact size of the GcBox<T> allocation in bytes.
182        pub(crate) size: usize,
183        pub(crate) lives: Cell<u8>,
184        pub(crate) next: Cell<*mut GcBoxHeader>,
185        pub(crate) trace_fn: unsafe fn(*const GcBoxHeader, &mut MarkVisitor),
186        pub(crate) drop_fn: unsafe fn(*mut GcBoxHeader),
187    }
188
189    impl GcBoxHeader {
190        pub(crate) fn new<T: Trace + 'static>(heap_extra: usize) -> Self {
191            Self {
192                #[cfg(debug_assertions)]
193                magic: Cell::new(GC_MAGIC_ALIVE),
194                size: std::mem::size_of::<GcBox<T>>() + heap_extra,
195                lives: Cell::new(GC_INITIAL_LIVES - 1),
196                next: Cell::new(std::ptr::null_mut()),
197                trace_fn: trace_gc_box::<T>,
198                drop_fn: drop_gc_box::<T>,
199            }
200        }
201    }
202
203    unsafe impl Send for GcBoxHeader {}
204    unsafe impl Sync for GcBoxHeader {}
205
206    #[repr(C)]
207    pub struct GcBox<T: Trace + 'static> {
208        pub(crate) header: GcBoxHeader,
209        pub value: T,
210    }
211
212    pub(crate) unsafe fn trace_gc_box<T: Trace + 'static>(
213        header: *const GcBoxHeader,
214        visitor: &mut MarkVisitor,
215    ) {
216        unsafe {
217            let gc_box = header as *const GcBox<T>;
218            (*gc_box).value.trace(visitor);
219        }
220    }
221
222    pub(crate) unsafe fn drop_gc_box<T: Trace + 'static>(header: *mut GcBoxHeader) {
223        unsafe {
224            #[cfg(debug_assertions)]
225            {
226                (*header).magic.set(GC_MAGIC_FREED);
227            }
228            let gc_box = header as *mut GcBox<T>;
229            drop(Box::from_raw(gc_box));
230        }
231    }
232}
233
234// =============================================================================
235// no-gc build: GcBox without header
236// =============================================================================
237
238#[cfg(feature = "no-gc")]
239pub use self::nogc_box::GcBox;
240
241#[cfg(feature = "no-gc")]
242mod nogc_box {
243    use crate::Trace;
244
245    pub(crate) const RELOCATED: u64 = 1 << 63;
246    pub(crate) const OWNED: u64 = 1 << 62;
247    pub(crate) const ID_MASK: u64 = OWNED - 1;
248    static NEXT_IDENTITY: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
249    pub struct GcBox<T: Trace + 'static> {
250        pub value: T,
251        pub(crate) identity: std::sync::atomic::AtomicU64,
252    }
253    impl<T: Trace + 'static> GcBox<T> {
254        pub(crate) fn new(value: T) -> Self {
255            Self {
256                value,
257                identity: std::sync::atomic::AtomicU64::new(
258                    NEXT_IDENTITY
259                        .fetch_update(
260                            std::sync::atomic::Ordering::Relaxed,
261                            std::sync::atomic::Ordering::Relaxed,
262                            |id| (id < ID_MASK).then_some(id + 1),
263                        )
264                        .expect("managed identity space exhausted"),
265                ),
266            }
267        }
268    }
269}
270
271// =============================================================================
272// MarkVisitor: full under GC, stub under no-gc
273// =============================================================================
274
275#[cfg(not(feature = "no-gc"))]
276#[derive(Default)]
277pub struct MarkVisitor {
278    pub(crate) grey: Vec<*mut GcBoxHeader>,
279}
280
281#[cfg(feature = "no-gc")]
282#[derive(Default)]
283pub struct MarkVisitor {
284    retaining: Option<std::collections::HashSet<usize>>,
285    collecting: Option<region::RegionLease>,
286    pending: Vec<PendingTrace>,
287    destination: Option<region::AllocationOwner>,
288    completed: Vec<*const std::sync::atomic::AtomicU64>,
289}
290
291/// Retain only regions reached by a published value in no-gc mode.
292#[cfg(feature = "no-gc")]
293pub fn retain_regions_for(value: &impl Trace) {
294    let mut visitor = MarkVisitor {
295        retaining: Some(Default::default()),
296        ..Default::default()
297    };
298    value.trace(&mut visitor);
299    visitor.drain_owned();
300}
301
302#[cfg(feature = "no-gc")]
303fn leases_for(value: &impl Trace) -> region::RegionLease {
304    let mut visitor = MarkVisitor {
305        retaining: Some(Default::default()),
306        collecting: Some(Default::default()),
307        ..Default::default()
308    };
309    value.trace(&mut visitor);
310    visitor.drain_owned();
311    visitor.collecting.take().unwrap()
312}
313
314#[cfg(not(feature = "no-gc"))]
315impl MarkVisitor {
316    pub fn new() -> Self {
317        Self::default()
318    }
319
320    pub fn grey_len(&self) -> usize {
321        self.grey.len()
322    }
323
324    pub unsafe fn mark_header(&mut self, header: *mut GcBoxHeader) {
325        use gc_header::GC_INITIAL_LIVES;
326        let h = unsafe { &*header };
327        if h.lives.get() < GC_INITIAL_LIVES {
328            h.lives.set(GC_INITIAL_LIVES);
329            self.grey.push(header);
330        }
331    }
332
333    pub(crate) fn drain(&mut self) {
334        let mut visited = 0usize;
335        while let Some(header) = self.grey.pop() {
336            visited += 1;
337            let h = unsafe { &*header };
338            unsafe { (h.trace_fn)(header as *const GcBoxHeader, self) };
339        }
340        tracing::debug!(target: "gc", "drain visited {} objects", visited);
341    }
342}
343
344#[cfg(not(feature = "no-gc"))]
345impl GcVisitor for MarkVisitor {
346    fn visit<T: Trace + 'static>(&mut self, ptr: &GcPtr<T>) {
347        use gc_header::GC_INITIAL_LIVES;
348        // Region-allocated objects are not on the GC heap; their lifetime is
349        // bounded by the enclosing `Region`, not the collector.  Never
350        // dereference one here: once the region's scope ends its memory is
351        // freed/reused, so the header would be garbage and we'd follow a
352        // dangling `trace_fn`.  Live regions are traced as roots separately
353        // (see `region::trace_active_regions`), so a region's heap-allocated
354        // children are still kept alive.
355        if ptr.is_region_alloc() {
356            return;
357        }
358        let raw = ptr.raw();
359        let header = unsafe { &(*raw).header };
360        if header.lives.get() < GC_INITIAL_LIVES {
361            header.lives.set(GC_INITIAL_LIVES);
362            self.grey.push(raw as *mut GcBoxHeader);
363        }
364    }
365}
366
367#[cfg(feature = "no-gc")]
368impl MarkVisitor {
369    pub fn grey_len(&self) -> usize {
370        0
371    }
372    pub unsafe fn mark_header(&mut self, _: *mut u8) {}
373}
374
375#[cfg(feature = "no-gc")]
376struct PendingTrace {
377    pointer: *const u8,
378    trace: unsafe fn(*const u8, &mut MarkVisitor),
379    destination: Option<region::AllocationOwner>,
380}
381#[cfg(feature = "no-gc")]
382impl MarkVisitor {
383    fn drain_owned(&mut self) {
384        while let Some(pending) = self.pending.pop() {
385            self.destination = pending.destination;
386            // SAFETY: the collected lease and the producer's scope keep queued objects live.
387            unsafe { (pending.trace)(pending.pointer, self) };
388        }
389        for identity in self.completed.drain(..) {
390            // Commit only after the complete graph walk succeeds.
391            unsafe { &*identity }.fetch_or(nogc_box::OWNED, std::sync::atomic::Ordering::Release);
392        }
393    }
394}
395#[cfg(feature = "no-gc")]
396impl GcVisitor for MarkVisitor {
397    fn visit<T: Trace + 'static>(&mut self, ptr: &GcPtr<T>) {
398        if self.retaining.is_none() {
399            return;
400        }
401        let address = ptr.0.as_ptr() as usize;
402        let source = region::RegionLease::for_address(address);
403        if let Some(destination) = &self.destination {
404            destination.adopt(&source);
405        }
406        if let Some(lease) = &mut self.collecting {
407            lease.extend(&source);
408        } else {
409            region::retain_containing(address);
410        }
411        if !self.retaining.as_mut().unwrap().insert(address) {
412            return;
413        }
414        let immutable = ptr.get().immutable_children();
415        if immutable
416            && unsafe {
417                (*ptr.raw())
418                    .identity
419                    .load(std::sync::atomic::Ordering::Acquire)
420                    & nogc_box::OWNED
421                    != 0
422            }
423        {
424            return;
425        }
426        unsafe fn trace<T: Trace + 'static>(raw: *const u8, visitor: &mut MarkVisitor) {
427            unsafe { &*(raw as *const T) }.trace(visitor);
428        }
429        let destination = if immutable {
430            self.completed.push(unsafe { &(*ptr.raw()).identity });
431            Some(region::AllocationOwner::for_address(address))
432        } else {
433            self.destination.clone()
434        };
435        self.pending.push(PendingTrace {
436            pointer: ptr.get() as *const T as *const u8,
437            trace: trace::<T>,
438            destination,
439        });
440    }
441}
442
443// =============================================================================
444// GcPtr — always present
445// =============================================================================
446
447pub struct GcPtr<T: Trace + 'static>(NonNull<GcBox<T>>);
448
449/// Low pointer bit reserved to mark region-allocated `GcPtr`s in GC builds.
450///
451/// `GcBox<T>` (a `GcBoxHeader` followed by the value) is always ≥8-byte
452/// aligned in GC builds, so bit 0 is free.  Region-allocated pointers set it;
453/// GC-heap pointers leave it clear.  This lets the mark phase distinguish a
454/// region object from a heap object **without dereferencing it** — essential,
455/// because a region whose scope has ended leaves dangling pointers whose
456/// headers point at freed (or reused) memory.
457#[cfg(not(feature = "no-gc"))]
458pub(crate) const REGION_PTR_TAG: usize = 1;
459
460/// A governed allocation the process governor refused.  Returns ownership of
461/// the value, so dropping the error releases whatever memory it owns.
462pub struct AllocError<T> {
463    value: T,
464    error: MemoryLimitExceeded,
465}
466
467impl<T> AllocError<T> {
468    /// The refusal.
469    pub fn error(&self) -> MemoryLimitExceeded {
470        self.error
471    }
472
473    /// Take back the value that was not allocated.
474    pub fn into_inner(self) -> T {
475        self.value
476    }
477
478    /// Split into the value and the refusal.
479    pub fn into_parts(self) -> (T, MemoryLimitExceeded) {
480        (self.value, self.error)
481    }
482}
483
484impl<T> std::fmt::Debug for AllocError<T> {
485    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
486        f.debug_struct("AllocError")
487            .field("error", &self.error)
488            .finish_non_exhaustive()
489    }
490}
491
492impl<T> std::fmt::Display for AllocError<T> {
493    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
494        self.error.fmt(f)
495    }
496}
497
498impl<T> std::error::Error for AllocError<T> {}
499
500impl<T> From<AllocError<T>> for MemoryLimitExceeded {
501    fn from(e: AllocError<T>) -> Self {
502        e.error
503    }
504}
505
506impl<T: Trace + 'static> GcPtr<T> {
507    /// Allocate `value`.  Infallible: in strict mode a refused allocation
508    /// uses the critical reserve and raises the thread's runtime fault,
509    /// which the next evaluation safepoint resolves (see
510    /// [`governor`](crate::governor) and [`Self::try_new`]).
511    #[cfg(not(feature = "no-gc"))]
512    pub fn new(value: T) -> Self {
513        gc_full::HEAP.alloc(value)
514    }
515
516    #[cfg(feature = "no-gc")]
517    pub fn new(value: T) -> Self {
518        alloc_ctx::alloc_in_ctx(value)
519    }
520
521    /// Allocate `value`, or return it with the refusal if the process
522    /// governor cannot admit its bytes.  The governor is consulted before
523    /// the object exists; a refusal changes no counter.  Observe-only mode
524    /// never refuses.
525    ///
526    /// In `no-gc` builds the governor is asked only when the active region
527    /// or the static arena must add a chunk; an object that fits in memory
528    /// already charged is never refused.
529    pub fn try_new(value: T) -> Result<Self, AllocError<T>> {
530        #[cfg(not(feature = "no-gc"))]
531        {
532            let bytes = std::mem::size_of::<GcBox<T>>() + value.gc_size_extra();
533            match governor::try_reserve_current(bytes) {
534                Ok(()) => Ok(Self::new(value)),
535                Err(error) => Err(AllocError { value, error }),
536            }
537        }
538        #[cfg(feature = "no-gc")]
539        {
540            alloc_ctx::try_alloc_in_ctx(value)
541        }
542    }
543
544    /// Allocate `value` for runtime control work (error reporting, shutdown):
545    /// the allocation may use the critical reserve and never raises a
546    /// runtime fault.
547    pub fn new_critical(value: T) -> Self {
548        let _section = governor::enter_critical();
549        Self::new(value)
550    }
551
552    /// The untagged `GcBox<T>` address.  In GC builds this masks off the
553    /// region-provenance tag bit; in no-gc builds pointers are never tagged.
554    #[inline]
555    fn raw(&self) -> *mut GcBox<T> {
556        #[cfg(not(feature = "no-gc"))]
557        {
558            (self.0.as_ptr() as usize & !REGION_PTR_TAG) as *mut GcBox<T>
559        }
560        #[cfg(feature = "no-gc")]
561        {
562            self.0.as_ptr()
563        }
564    }
565
566    /// `true` if this pointer was bump-allocated in a [`region::Region`]
567    /// rather than the GC heap.  Region objects are not GC-managed.
568    #[cfg(not(feature = "no-gc"))]
569    #[inline]
570    pub fn is_region_alloc(&self) -> bool {
571        (self.0.as_ptr() as usize & REGION_PTR_TAG) != 0
572    }
573
574    /// Construct a region-tagged pointer from a raw `GcBox<T>` allocated in a
575    /// bump region.
576    ///
577    /// # Safety
578    /// `raw` must be a valid, non-null, ≥8-aligned `GcBox<T>` whose header was
579    /// initialised by [`region::Region::alloc`].
580    #[cfg(not(feature = "no-gc"))]
581    #[inline]
582    pub(crate) unsafe fn from_region_raw(raw: *mut GcBox<T>) -> Self {
583        GcPtr(unsafe { NonNull::new_unchecked((raw as usize | REGION_PTR_TAG) as *mut GcBox<T>) })
584    }
585
586    pub fn get(&self) -> &T {
587        #[cfg(all(debug_assertions, not(feature = "no-gc")))]
588        {
589            use gc_header::GC_MAGIC_ALIVE;
590            let header = unsafe { &(*self.raw()).header };
591            assert_eq!(
592                header.magic.get(),
593                GC_MAGIC_ALIVE,
594                "GcPtr::get() on freed object! magic={:#x}",
595                header.magic.get(),
596            );
597        }
598        unsafe { &(*self.raw()).value }
599    }
600
601    #[cfg(not(feature = "no-gc"))]
602    pub fn get_mut(&mut self) -> &mut T {
603        #[cfg(all(debug_assertions, not(feature = "no-gc")))]
604        {
605            use gc_header::GC_MAGIC_ALIVE;
606            let header = unsafe { &(*self.raw()).header };
607            assert_eq!(
608                header.magic.get(),
609                GC_MAGIC_ALIVE,
610                "GcPtr::get_mut() on freed object! magic={:#x}",
611                header.magic.get(),
612            );
613        }
614        unsafe { &mut (*self.raw()).value }
615    }
616
617    /// Mutate an object and refresh its backing ownership when the guard drops.
618    #[cfg(feature = "no-gc")]
619    pub fn get_mut(&mut self) -> GcMut<'_, T> {
620        unsafe { &(*self.raw()).identity }
621            .fetch_and(!nogc_box::OWNED, std::sync::atomic::Ordering::Relaxed);
622        GcMut { pointer: self }
623    }
624
625    pub fn ptr_eq(a: &Self, b: &Self) -> bool {
626        a.0 == b.0
627    }
628
629    /// Compare logical object identity, including relocated immutable scalar boxes.
630    pub fn identity_eq(a: &Self, b: &Self) -> bool {
631        #[cfg(feature = "no-gc")]
632        {
633            unsafe {
634                (*a.raw())
635                    .identity
636                    .load(std::sync::atomic::Ordering::Relaxed)
637                    & nogc_box::ID_MASK
638                    == (*b.raw())
639                        .identity
640                        .load(std::sync::atomic::Ordering::Relaxed)
641                        & nogc_box::ID_MASK
642            }
643        }
644        #[cfg(not(feature = "no-gc"))]
645        {
646            Self::ptr_eq(a, b)
647        }
648    }
649
650    /// Allocate a root in a separate region sized to its box.
651    /// The caller's scope owns the result; invocation and static destinations retain their rules.
652    pub fn new_isolated(value: T) -> Self {
653        #[cfg(feature = "no-gc")]
654        if !alloc_ctx::is_static_context() && !alloc_ctx::invocation_is_active() {
655            let mut scratch =
656                alloc_ctx::ScratchGuard::with_capacity(std::mem::size_of::<GcBox<T>>() + 32);
657            let pointer = Self::new(value);
658            let lease = region::RegionLease::for_address(pointer.raw() as usize);
659            region::AllocationOwner::for_address(pointer.raw() as usize)
660                .adopt(&region::RegionLease::for_value(pointer.get()));
661            scratch.pop_for_return();
662            lease.protect_current();
663            return pointer;
664        }
665        Self::new(value)
666    }
667
668    /// Relocate an immutable value while preserving its logical identity.
669    ///
670    /// # Safety
671    /// `value` must represent the same immutable object, including the identities of its children.
672    #[cfg(feature = "no-gc")]
673    pub unsafe fn relocate_immutable(&self, value: T) -> Self {
674        let copy = Self::new(value);
675        unsafe {
676            (*copy.raw()).identity.store(
677                ((*self.raw())
678                    .identity
679                    .load(std::sync::atomic::Ordering::Relaxed)
680                    & nogc_box::ID_MASK)
681                    | nogc_box::RELOCATED,
682                std::sync::atomic::Ordering::Relaxed,
683            );
684        }
685        copy
686    }
687
688    /// True if this immutable box was relocated for compact storage.
689    #[cfg(feature = "no-gc")]
690    pub fn is_relocated_immutable(&self) -> bool {
691        unsafe {
692            (*self.raw())
693                .identity
694                .load(std::sync::atomic::Ordering::Relaxed)
695                & nogc_box::RELOCATED
696                != 0
697        }
698    }
699
700    /// Copy an immutable scalar into the current allocation destination.
701    ///
702    /// # Safety
703    /// The pointee must be immutable, and its clone must represent the same
704    /// value without mutable state. Both allocations retain one logical identity.
705    #[cfg(feature = "no-gc")]
706    pub unsafe fn clone_immutable(&self) -> Self
707    where
708        T: Clone,
709    {
710        let copy = Self::new(self.get().clone());
711        unsafe {
712            (*copy.raw()).identity.store(
713                ((*self.raw())
714                    .identity
715                    .load(std::sync::atomic::Ordering::Relaxed)
716                    & nogc_box::ID_MASK)
717                    | nogc_box::RELOCATED,
718                std::sync::atomic::Ordering::Relaxed,
719            );
720        }
721        copy
722    }
723
724    /// Return `true` for static storage or a permanently retained backing scope.
725    ///
726    /// Available in no-gc builds for publication decisions and
727    /// write-site assertions in `Atom::reset` / `Var::bind` to catch
728    /// region-local values being stored in program-lifetime containers.
729    #[cfg(feature = "no-gc")]
730    pub fn is_static_alloc(&self) -> bool {
731        alloc_ctx::invocation_is_active()
732            || static_arena::is_static_addr(self.0.as_ptr() as usize)
733            || region::is_permanent_address(self.0.as_ptr() as usize)
734    }
735}
736
737/// Mutable no-GC access that refreshes child ownership before releasing the borrow.
738#[cfg(feature = "no-gc")]
739pub struct GcMut<'a, T: Trace + 'static> {
740    pointer: &'a mut GcPtr<T>,
741}
742#[cfg(feature = "no-gc")]
743impl<T: Trace + 'static> std::ops::Deref for GcMut<'_, T> {
744    type Target = T;
745    fn deref(&self) -> &T {
746        self.pointer.get()
747    }
748}
749#[cfg(feature = "no-gc")]
750impl<T: Trace + 'static> std::ops::DerefMut for GcMut<'_, T> {
751    fn deref_mut(&mut self) -> &mut T {
752        unsafe { &mut (*self.pointer.raw()).value }
753    }
754}
755#[cfg(feature = "no-gc")]
756impl<T: Trace + 'static> Drop for GcMut<'_, T> {
757    fn drop(&mut self) {
758        if !alloc_ctx::invocation_is_active() {
759            region::AllocationOwner::for_address(self.pointer.raw() as usize)
760                .adopt(&region::RegionLease::for_value(self.pointer.get()));
761        }
762    }
763}
764
765impl<T: Trace + 'static> Clone for GcPtr<T> {
766    fn clone(&self) -> Self {
767        GcPtr(self.0)
768    }
769}
770
771impl<T: Trace + 'static + std::fmt::Debug> std::fmt::Debug for GcPtr<T> {
772    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
773        unsafe { (*self.raw()).value.fmt(f) }
774    }
775}
776
777impl<T: Trace + 'static> Drop for GcPtr<T> {
778    fn drop(&mut self) {}
779}
780
781// =============================================================================
782// StaticGcPtr — Send+Sync pointer to program-lifetime data
783// =============================================================================
784
785/// A raw pointer to a value that lives for the entire program lifetime.
786///
787/// Backed by the global `StaticArena` (in `no-gc` builds) or by `Box::leak`
788/// (in GC builds).  Either way the pointee is never freed and never moved, so
789/// it is safe to share across isolate threads.
790///
791/// `StaticGcPtr<T>` wraps `*const T` — it does **not** involve a `GcBox`
792/// header — so it is independent of the GC build mode and carries no GC
793/// overhead.
794pub struct StaticGcPtr<T: 'static>(NonNull<T>);
795
796// SAFETY: program-lifetime allocations are never moved, freed, or mutated
797// after the initial write.  The stored types (Keyword, Symbol, …) are
798// themselves `Sync` (no unsynchronised interior mutability).
799unsafe impl<T: 'static> Send for StaticGcPtr<T> {}
800unsafe impl<T: 'static> Sync for StaticGcPtr<T> {}
801
802impl<T: 'static> StaticGcPtr<T> {
803    /// Borrow the contained value.
804    pub fn get(&self) -> &T {
805        // SAFETY: pointer is program-lifetime, always valid.
806        unsafe { self.0.as_ref() }
807    }
808
809    /// Pointer equality: `true` iff both `StaticGcPtr`s point to the exact
810    /// same allocation (i.e. the same interned entry).
811    pub fn ptr_eq(a: &Self, b: &Self) -> bool {
812        a.0 == b.0
813    }
814}
815
816impl<T: 'static> Clone for StaticGcPtr<T> {
817    fn clone(&self) -> Self {
818        StaticGcPtr(self.0)
819    }
820}
821
822impl<T: 'static + std::fmt::Debug> std::fmt::Debug for StaticGcPtr<T> {
823    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
824        unsafe { self.0.as_ref().fmt(f) }
825    }
826}
827
828/// Allocate `value` as program-lifetime memory and return a [`StaticGcPtr`].
829///
830/// In `no-gc` builds the allocation comes from the global bump-allocated
831/// `StaticArena` (never freed, no GC header overhead).  In GC builds
832/// `Box::leak` is used instead — the memory lives until the process exits.
833/// Either way the governor records the bytes under `MemoryClass::Static`
834/// (arena chunks in `no-gc` builds; `size_of::<T>()` in GC builds).
835pub fn static_alloc<T: 'static>(value: T) -> StaticGcPtr<T> {
836    static_alloc_inner(value)
837}
838
839/// Fallible [`static_alloc`]: return the value with the refusal if the
840/// governor cannot admit it.  In `no-gc` builds the governor is asked only
841/// when the arena must add a chunk.
842pub fn try_static_alloc<T: 'static>(value: T) -> Result<StaticGcPtr<T>, AllocError<T>> {
843    #[cfg(not(feature = "no-gc"))]
844    {
845        match governor::governor()
846            .try_charge_permanent(governor::MemoryClass::Static, std::mem::size_of::<T>())
847        {
848            Ok(()) => Ok(StaticGcPtr(NonNull::from(Box::leak(Box::new(value))))),
849            Err(error) => Err(AllocError { value, error }),
850        }
851    }
852    #[cfg(feature = "no-gc")]
853    {
854        static_arena::static_arena().try_alloc_val(value)
855    }
856}
857
858fn static_alloc_inner<T: 'static>(value: T) -> StaticGcPtr<T> {
859    #[cfg(feature = "no-gc")]
860    {
861        static_arena::static_alloc_val(value)
862    }
863    #[cfg(not(feature = "no-gc"))]
864    {
865        governor::governor()
866            .charge_permanent(governor::MemoryClass::Static, std::mem::size_of::<T>());
867        StaticGcPtr(NonNull::from(Box::leak(Box::new(value))))
868    }
869}
870
871// =============================================================================
872// Full GC implementation (default build)
873// =============================================================================
874
875#[cfg(not(feature = "no-gc"))]
876mod gc_full {
877    use std::cell::RefCell;
878    use std::ptr::NonNull;
879    use std::sync::atomic::{AtomicUsize, Ordering};
880    use std::sync::{Arc, Mutex};
881
882    use crate::config::GcConfig;
883    use crate::gc_header::GC_INITIAL_LIVES;
884    use crate::{GcBox, GcBoxHeader, GcPtr, MarkVisitor, Trace};
885
886    type RootTracer = Box<dyn Fn(&mut MarkVisitor)>;
887
888    pub struct GcHeap {
889        inner: Mutex<GcHeapInner>,
890        config: Mutex<Option<Arc<GcConfig>>>,
891        memory_in_use: AtomicUsize,
892        total_allocated_bytes: AtomicUsize,
893        root_tracers: Mutex<Vec<RootTracer>>,
894        gc_suppressed: std::sync::atomic::AtomicBool,
895        /// memory_in_use threshold above which GC is re-enabled after a
896        /// zero-yield collection.  The headroom doubles on each consecutive
897        /// zero-yield cycle (exponential backoff, capped at soft_limit) so a
898        /// long computation where all objects are live doesn't spin in a
899        /// constant GC storm of O(N) sweeps.  Resets to the base headroom
900        /// (soft_limit / 10) once GC actually frees something.
901        suppressed_threshold: AtomicUsize,
902        /// Current headroom used for exponential backoff after zero-yield cycles.
903        zero_yield_headroom: AtomicUsize,
904    }
905
906    struct GcHeapInner {
907        head: *mut GcBoxHeader,
908        count: usize,
909        total_allocated: usize,
910        total_freed: usize,
911    }
912
913    unsafe impl Send for GcHeapInner {}
914
915    impl GcHeapInner {
916        const fn new() -> Self {
917            Self {
918                head: std::ptr::null_mut(),
919                count: 0,
920                total_allocated: 0,
921                total_freed: 0,
922            }
923        }
924    }
925
926    unsafe impl Sync for GcHeap {}
927
928    impl Default for GcHeap {
929        fn default() -> Self {
930            Self::new()
931        }
932    }
933
934    /// Parse a megabyte limit from a (raw) environment value into a byte count,
935    /// falling back to `default` (and warning) on malformed input rather than
936    /// panicking on user misconfiguration. `value` is `None` when the variable
937    /// is unset. Saturates instead of overflowing on absurdly large values.
938    pub(crate) fn parse_limit_mb(var: &str, value: Option<&str>, default: usize) -> usize {
939        match value {
940            Some(s) => match s.trim().parse::<usize>() {
941                Ok(mb) => mb.saturating_mul(1024 * 1024),
942                Err(_) => {
943                    eprintln!(
944                        "[gc] warning: ignoring invalid {var}={s:?} (expected a number of megabytes)"
945                    );
946                    default
947                }
948            },
949            None => default,
950        }
951    }
952
953    impl GcHeap {
954        pub const fn new() -> Self {
955            Self {
956                inner: Mutex::new(GcHeapInner::new()),
957                config: Mutex::new(None),
958                memory_in_use: AtomicUsize::new(0),
959                total_allocated_bytes: AtomicUsize::new(0),
960                root_tracers: Mutex::new(Vec::new()),
961                gc_suppressed: std::sync::atomic::AtomicBool::new(false),
962                suppressed_threshold: AtomicUsize::new(0),
963                zero_yield_headroom: AtomicUsize::new(0),
964            }
965        }
966
967        pub fn set_config(&self, config: Arc<GcConfig>) {
968            *self.config.lock().unwrap() = Some(config);
969        }
970
971        pub fn set_config_from_env(&self) {
972            #[cfg(not(target_arch = "wasm32"))]
973            let default_soft_limit: usize = (system_memory::total() / 3) as usize;
974            #[cfg(target_arch = "wasm32")]
975            let default_soft_limit: usize = 64 * 1024 * 1024;
976
977            let soft_limit_mb = parse_limit_mb(
978                "CLJRS_GC_SOFT_LIMIT_MB",
979                std::env::var("CLJRS_GC_SOFT_LIMIT_MB").ok().as_deref(),
980                default_soft_limit,
981            );
982            let mut hard_limit_mb = parse_limit_mb(
983                "CLJRS_GC_HARD_LIMIT_MB",
984                std::env::var("CLJRS_GC_HARD_LIMIT_MB").ok().as_deref(),
985                soft_limit_mb,
986            );
987            if hard_limit_mb < soft_limit_mb {
988                eprintln!(
989                    "[gc] warning: CLJRS_GC_HARD_LIMIT_MB is below CLJRS_GC_SOFT_LIMIT_MB; using the soft limit for both"
990                );
991                hard_limit_mb = soft_limit_mb;
992            }
993            self.set_config(Arc::new(GcConfig::with_limits(
994                soft_limit_mb,
995                hard_limit_mb,
996            )));
997        }
998
999        pub fn register_root_tracer(&self, tracer: impl Fn(&mut MarkVisitor) + 'static) {
1000            self.root_tracers.lock().unwrap().push(Box::new(tracer));
1001        }
1002
1003        pub fn trace_registered_roots(&self, visitor: &mut MarkVisitor) {
1004            let tracers = self.root_tracers.lock().unwrap();
1005            for tracer in tracers.iter() {
1006                tracer(visitor);
1007            }
1008        }
1009
1010        pub fn memory_in_use(&self) -> usize {
1011            self.memory_in_use.load(Ordering::Relaxed)
1012        }
1013
1014        #[cfg(test)]
1015        pub fn set_memory_in_use(&self, bytes: usize) {
1016            self.memory_in_use.store(bytes, Ordering::Relaxed);
1017        }
1018
1019        pub fn alloc<T: Trace + 'static>(&self, value: T) -> GcPtr<T> {
1020            crate::cancellation::safepoint();
1021            let heap_extra = value.gc_size_extra();
1022            let gc_box = Box::new(GcBox {
1023                header: GcBoxHeader::new::<T>(heap_extra),
1024                value,
1025            });
1026            let obj_size = gc_box.header.size; // GcBox<T> size + gc_size_extra()
1027            let raw: *mut GcBox<T> = Box::into_raw(gc_box);
1028            {
1029                let mut inner = self.inner.lock().unwrap();
1030                unsafe {
1031                    (*raw).header.next.set(inner.head);
1032                    inner.head = raw as *mut GcBoxHeader;
1033                }
1034                inner.count += 1;
1035                inner.total_allocated += 1;
1036            }
1037            self.total_allocated_bytes
1038                .fetch_add(obj_size, Ordering::Relaxed);
1039            // The account batches the allocation statistics; record them
1040            // directly only during thread-local teardown.
1041            if crate::governor::with_current_account(|a| a.charge(obj_size)).is_none() {
1042                crate::stats::GC_STATS.record_gc_alloc(obj_size);
1043            }
1044            let current_usage =
1045                self.memory_in_use.fetch_add(obj_size, Ordering::Relaxed) + obj_size;
1046
1047            if let Some(config) = self.config.lock().unwrap().as_ref()
1048                && config.soft_limit_exceeded(current_usage)
1049            {
1050                if self.gc_suppressed.load(Ordering::Relaxed) {
1051                    // Suppression active: only re-enable GC once memory has
1052                    // grown past the threshold set by the last zero-yield
1053                    // collection (current_memory + soft_limit/10).
1054                    let threshold = self.suppressed_threshold.load(Ordering::Relaxed);
1055                    if current_usage > threshold {
1056                        self.gc_suppressed.store(false, Ordering::Relaxed);
1057                        crate::cancellation::request_gc();
1058                    }
1059                } else {
1060                    crate::cancellation::request_gc();
1061                }
1062            }
1063
1064            register_alloc(raw as *mut GcBoxHeader);
1065            GcPtr(unsafe { NonNull::new_unchecked(raw) })
1066        }
1067
1068        pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, trace_roots: F) {
1069            let _pause = crate::fiber::gc_pause();
1070            let pre_count = self.inner.lock().unwrap().count;
1071            let pre_memory = self.memory_in_use.load(Ordering::Relaxed);
1072            tracing::debug!(
1073                target: "gc",
1074                "starting collection: {} objects, ~{} bytes in use",
1075                pre_count,
1076                pre_memory
1077            );
1078            let mark_start = std::time::Instant::now();
1079            let mut visitor = MarkVisitor::new();
1080            trace_roots(&mut visitor);
1081            crate::mutator::trace_suspended(&mut visitor);
1082            // Active bump regions are additional roots: a live region object
1083            // may hold `GcPtr`s into the heap, and those heap objects must not
1084            // be collected while the region can still reach them.  The mark
1085            // phase skips region objects themselves (they are not heap-managed),
1086            // so we trace their children here instead.
1087            crate::region::trace_active_regions(&mut visitor);
1088            // Retired (poisoned) regions are immortal roots: their objects may
1089            // still be referenced and may hold `GcPtr`s into the heap.
1090            crate::region::trace_retired_regions(&mut visitor);
1091            tracing::debug!(
1092                target: "gc",
1093                "starting drain with {} grey objects",
1094                visitor.grey.len()
1095            );
1096            visitor.drain();
1097            let mark_elapsed = mark_start.elapsed();
1098
1099            let sweep_start = std::time::Instant::now();
1100            let mut inner = self.inner.lock().unwrap();
1101            let mut live: Vec<*mut GcBoxHeader> = Vec::with_capacity(inner.count);
1102            let mut dead: Vec<*mut GcBoxHeader> = Vec::new();
1103            // Bytes of objects with lives==0 that will be freed now.
1104            let mut freed_bytes: usize = 0;
1105            // Bytes of unreachable objects whose grace period ends with this
1106            // cycle: the next collection frees them unless they are re-rooted.
1107            let mut pending_bytes: usize = 0;
1108            let mut current = inner.head;
1109            while !current.is_null() {
1110                let header = unsafe { &*current };
1111                let next = header.next.get();
1112                let lives = header.lives.get();
1113                let obj_size = header.size;
1114                if lives >= GC_INITIAL_LIVES {
1115                    // Marked reachable this cycle — reset grace counter.
1116                    header.lives.set(GC_INITIAL_LIVES - 1);
1117                    live.push(current);
1118                } else if lives > 0 {
1119                    // In grace period (unreachable but not yet freed).
1120                    header.lives.set(lives - 1);
1121                    if lives == 1 {
1122                        pending_bytes += obj_size;
1123                    }
1124                    live.push(current);
1125                } else {
1126                    // Grace period exhausted — collect now.
1127                    freed_bytes += obj_size;
1128                    dead.push(current);
1129                }
1130                current = next;
1131            }
1132            let freed_count = dead.len();
1133            for ptr in dead {
1134                let header = unsafe { &*ptr };
1135                unsafe { (header.drop_fn)(ptr) };
1136                inner.count -= 1;
1137                inner.total_freed += 1;
1138            }
1139            inner.head = std::ptr::null_mut();
1140            for ptr in live {
1141                let header = unsafe { &*ptr };
1142                header.next.set(inner.head);
1143                inner.head = ptr;
1144            }
1145            // Decrement memory_in_use by the bytes actually freed.  All heap
1146            // objects (live + grace-period) remain counted; only physically
1147            // freed objects are subtracted.  This keeps memory pressure
1148            // accurate so GC fires again when the heap genuinely grows.
1149            self.memory_in_use.fetch_sub(freed_bytes, Ordering::Relaxed);
1150            let sweep_elapsed = sweep_start.elapsed();
1151            let post_memory = self.memory_in_use.load(Ordering::Relaxed);
1152            crate::governor::with_current_account(|a| {
1153                a.release(freed_bytes);
1154                a.record_collection(crate::governor::CollectionReport {
1155                    bytes_before: pre_memory,
1156                    bytes_after: post_memory,
1157                    bytes_returned: freed_bytes,
1158                    bytes_pending: pending_bytes,
1159                    duration: mark_elapsed + sweep_elapsed,
1160                });
1161            });
1162            crate::stats::GC_STATS.record_gc_pause(
1163                mark_elapsed + sweep_elapsed,
1164                freed_count as u64,
1165                freed_bytes as u64,
1166            );
1167            tracing::debug!(
1168                target: "gc",
1169                "collection complete: freed {} (~{} bytes), {} remaining (~{} bytes, ~{} pending), mark={:.2?} sweep={:.2?}",
1170                freed_count,
1171                freed_bytes,
1172                inner.count,
1173                post_memory,
1174                pending_bytes,
1175                mark_elapsed,
1176                sweep_elapsed
1177            );
1178            if freed_count == 0 && pending_bytes == 0 {
1179                // Zero-yield collection: exponential-backoff suppression.
1180                // Objects that only entered their grace period this cycle do
1181                // not count as zero yield: the next collection frees them, and
1182                // suppressing it would keep that garbage until the heap grows
1183                // by another headroom.
1184                // Each consecutive zero-yield cycle doubles the headroom before
1185                // the next GC attempt (capped at soft_limit/4).  This prevents a
1186                // GC storm during deep recursion where all objects are live —
1187                // without backoff, GC fires every soft_limit/10 bytes, tracing
1188                // the entire live set O(N) times to no benefit.
1189                // The headroom resets to soft_limit/10 when GC frees something.
1190                // Cap at soft_limit/4 (not soft_limit) so that GC still fires
1191                // frequently enough to catch short-lived test allocations after
1192                // a long namespace-loading phase of zero-yield cycles.
1193                let soft_limit = self
1194                    .config
1195                    .lock()
1196                    .unwrap()
1197                    .as_ref()
1198                    .map(|c| c.soft_limit())
1199                    .unwrap_or(64 * 1024 * 1024);
1200                let base_headroom = (soft_limit / 10).max(1);
1201                let max_headroom = (soft_limit / 4).max(base_headroom);
1202                let prev_headroom = self.zero_yield_headroom.load(Ordering::Relaxed);
1203                let headroom = if prev_headroom == 0 {
1204                    base_headroom
1205                } else {
1206                    prev_headroom.saturating_mul(2).min(max_headroom)
1207                };
1208                self.zero_yield_headroom.store(headroom, Ordering::Relaxed);
1209                self.suppressed_threshold
1210                    .store(post_memory + headroom, Ordering::Relaxed);
1211                self.gc_suppressed.store(true, Ordering::Relaxed);
1212            } else {
1213                // GC freed something: reset exponential backoff.
1214                self.zero_yield_headroom.store(0, Ordering::Relaxed);
1215                self.gc_suppressed.store(false, Ordering::Relaxed);
1216            }
1217        }
1218
1219        pub fn count(&self) -> usize {
1220            self.inner.lock().unwrap().count
1221        }
1222        pub fn total_allocated(&self) -> usize {
1223            self.inner.lock().unwrap().total_allocated
1224        }
1225        pub fn total_freed(&self) -> usize {
1226            self.inner.lock().unwrap().total_freed
1227        }
1228
1229        pub fn collect_auto(&self) -> bool {
1230            tracing::debug!(target: "gc", "automatic collection requested");
1231            let Some(_stw_guard) = crate::cancellation::begin_stw() else {
1232                tracing::debug!(target: "gc", "automatic collection skipped");
1233                return false;
1234            };
1235            self.collect(|visitor| self.trace_registered_roots(visitor));
1236            true
1237        }
1238    }
1239
1240    thread_local! {
1241        static ISOLATE_HEAP: GcHeap = const { GcHeap::new() };
1242    }
1243
1244    /// Zero-sized proxy that dispatches all heap operations to the calling
1245    /// thread's [`GcHeap`] via the `ISOLATE_HEAP` thread-local.
1246    ///
1247    /// This means every isolate (OS thread) owns an independent heap; GC runs
1248    /// fully in parallel on different threads with no cross-isolate coordination.
1249    pub struct HeapProxy;
1250
1251    impl HeapProxy {
1252        pub fn alloc<T: Trace + 'static>(&self, value: T) -> GcPtr<T> {
1253            ISOLATE_HEAP.with(|h| h.alloc(value))
1254        }
1255
1256        pub fn set_config(&self, config: Arc<GcConfig>) {
1257            ISOLATE_HEAP.with(|h| h.set_config(config));
1258        }
1259
1260        pub fn set_config_from_env(&self) {
1261            ISOLATE_HEAP.with(|h| h.set_config_from_env());
1262        }
1263
1264        pub fn register_root_tracer(&self, tracer: impl Fn(&mut MarkVisitor) + 'static) {
1265            ISOLATE_HEAP.with(|h| h.register_root_tracer(tracer));
1266        }
1267
1268        pub fn trace_registered_roots(&self, visitor: &mut MarkVisitor) {
1269            ISOLATE_HEAP.with(|h| h.trace_registered_roots(visitor));
1270        }
1271
1272        pub fn memory_in_use(&self) -> usize {
1273            ISOLATE_HEAP.with(|h| h.memory_in_use())
1274        }
1275
1276        pub fn count(&self) -> usize {
1277            ISOLATE_HEAP.with(|h| h.count())
1278        }
1279
1280        pub fn total_allocated(&self) -> usize {
1281            ISOLATE_HEAP.with(|h| h.total_allocated())
1282        }
1283
1284        pub fn total_freed(&self) -> usize {
1285            ISOLATE_HEAP.with(|h| h.total_freed())
1286        }
1287
1288        pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, trace_roots: F) {
1289            ISOLATE_HEAP.with(|h| h.collect(trace_roots));
1290        }
1291
1292        pub fn collect_auto(&self) -> bool {
1293            ISOLATE_HEAP.with(|h| h.collect_auto())
1294        }
1295
1296        #[cfg(test)]
1297        pub fn set_memory_in_use(&self, bytes: usize) {
1298            ISOLATE_HEAP.with(|h| h.set_memory_in_use(bytes));
1299        }
1300    }
1301
1302    // SAFETY: HeapProxy is zero-sized; all state lives in a thread-local GcHeap.
1303    // The Send + Sync impls are needed so `pub static HEAP: HeapProxy` is valid.
1304    unsafe impl Sync for HeapProxy {}
1305
1306    pub static HEAP: HeapProxy = HeapProxy;
1307
1308    thread_local! {
1309        pub(crate) static ALLOC_ROOTS: RefCell<Vec<*mut GcBoxHeader>> = const { RefCell::new(Vec::new()) };
1310    }
1311    crate::mutator_slot!("cljrs-gc/ALLOC_ROOTS", refcell ALLOC_ROOTS: Vec<*mut GcBoxHeader>, trace trace_saved_alloc_roots);
1312
1313    fn trace_saved_alloc_roots(roots: &Vec<*mut GcBoxHeader>, visitor: &mut MarkVisitor) {
1314        for &header in roots {
1315            // SAFETY: the detached mutator owns the still-live allocation frame.
1316            unsafe { visitor.mark_header(header) };
1317        }
1318    }
1319
1320    pub struct AllocRootGuard {
1321        saved_len: usize,
1322    }
1323
1324    impl AllocRootGuard {
1325        /// Unroot every allocation made since this frame was pushed, keeping
1326        /// the frame open for further allocations.  Equivalent to dropping
1327        /// the guard and pushing a new frame at the same depth.
1328        pub fn release(&self) {
1329            ALLOC_ROOTS.with(|roots| roots.borrow_mut().truncate(self.saved_len));
1330        }
1331    }
1332
1333    impl Drop for AllocRootGuard {
1334        fn drop(&mut self) {
1335            self.release();
1336        }
1337    }
1338
1339    pub fn push_alloc_frame() -> AllocRootGuard {
1340        let saved_len = ALLOC_ROOTS.with(|roots| roots.borrow().len());
1341        AllocRootGuard { saved_len }
1342    }
1343
1344    fn register_alloc(header: *mut GcBoxHeader) {
1345        ALLOC_ROOTS.with(|roots| roots.borrow_mut().push(header));
1346    }
1347
1348    pub fn trace_thread_alloc_roots(visitor: &mut MarkVisitor) {
1349        ALLOC_ROOTS.with(|roots| {
1350            let roots = roots.borrow();
1351            for &header in roots.iter() {
1352                unsafe { visitor.mark_header(header) };
1353            }
1354        });
1355    }
1356}
1357
1358// =============================================================================
1359// no-gc stubs
1360// =============================================================================
1361
1362#[cfg(feature = "no-gc")]
1363mod nogc_stubs {
1364    use crate::MarkVisitor;
1365    use std::sync::Arc;
1366
1367    #[derive(Debug, Clone)]
1368    pub struct GcConfig;
1369    impl GcConfig {
1370        pub fn new() -> Self {
1371            Self
1372        }
1373        pub fn with_hard_limit(_: usize) -> Self {
1374            Self
1375        }
1376        pub fn with_limits(_: usize, _: usize) -> Self {
1377            Self
1378        }
1379        pub fn with_soft_limit(_: usize) -> Self {
1380            Self
1381        }
1382        pub fn try_with_limits(
1383            soft_limit: usize,
1384            hard_limit: usize,
1385        ) -> Result<Self, crate::governor::MemoryConfigError> {
1386            crate::governor::MemoryConfig::with_limits(soft_limit, hard_limit).map(|_| Self)
1387        }
1388        pub fn validate(&self) -> Result<(), crate::governor::MemoryConfigError> {
1389            Ok(())
1390        }
1391    }
1392    impl Default for GcConfig {
1393        fn default() -> Self {
1394            Self::new()
1395        }
1396    }
1397
1398    pub struct GcHeap;
1399    impl Default for GcHeap {
1400        fn default() -> Self {
1401            Self::new()
1402        }
1403    }
1404    impl GcHeap {
1405        pub const fn new() -> Self {
1406            Self
1407        }
1408        pub fn set_config(&self, _: Arc<GcConfig>) {}
1409        /// No-op counterpart of the GC build's env-driven configuration.
1410        /// Region allocation has no soft limit to configure, but the runtime
1411        /// builder calls this unconditionally, so the method must exist in
1412        /// both builds.
1413        pub fn set_config_from_env(&self) {}
1414        pub fn register_root_tracer(&self, _: impl Fn(&mut MarkVisitor) + 'static) {}
1415        pub fn trace_registered_roots(&self, _: &mut MarkVisitor) {}
1416        pub fn memory_in_use(&self) -> usize {
1417            0
1418        }
1419        pub fn count(&self) -> usize {
1420            0
1421        }
1422        pub fn total_allocated(&self) -> usize {
1423            0
1424        }
1425        pub fn total_freed(&self) -> usize {
1426            0
1427        }
1428        pub fn collect<F: FnOnce(&mut MarkVisitor)>(&self, _: F) {}
1429        pub fn collect_auto(&self) -> bool {
1430            false
1431        }
1432    }
1433    unsafe impl Sync for GcHeap {}
1434    pub static HEAP: GcHeap = GcHeap::new();
1435
1436    pub struct MutatorGuard;
1437    impl Drop for MutatorGuard {
1438        fn drop(&mut self) {}
1439    }
1440    pub struct StwGuard;
1441    impl Drop for StwGuard {
1442        fn drop(&mut self) {}
1443    }
1444    pub struct GcParked;
1445
1446    pub struct GcCancellationStub;
1447    impl GcCancellationStub {
1448        pub const fn new() -> Self {
1449            Self
1450        }
1451        pub fn in_progress(&self) -> bool {
1452            false
1453        }
1454    }
1455    pub static CONFIG_CANCELLATION: GcCancellationStub = GcCancellationStub::new();
1456
1457    pub fn safepoint() {}
1458    pub fn gc_requested() -> bool {
1459        false
1460    }
1461    pub fn take_gc_request() -> bool {
1462        false
1463    }
1464    pub fn begin_stw() -> Option<StwGuard> {
1465        None
1466    }
1467    pub fn register_mutator() -> MutatorGuard {
1468        MutatorGuard
1469    }
1470    pub fn registered_threads() -> usize {
1471        0
1472    }
1473    pub fn request_gc() {}
1474    pub fn check_cancellation() -> Result<(), GcParked> {
1475        Ok(())
1476    }
1477    pub fn park_thread() {}
1478    pub fn unpark_thread() {}
1479    pub fn wait_for_threads_to_park() {}
1480
1481    pub struct AllocRootGuard;
1482    impl AllocRootGuard {
1483        pub fn release(&self) {}
1484    }
1485    impl Drop for AllocRootGuard {
1486        fn drop(&mut self) {}
1487    }
1488    pub fn push_alloc_frame() -> AllocRootGuard {
1489        AllocRootGuard
1490    }
1491}
1492
1493// =============================================================================
1494// Tests
1495// =============================================================================
1496
1497#[cfg(all(test, not(feature = "no-gc")))]
1498mod tests {
1499    use super::*;
1500    use std::sync::{Arc, Mutex};
1501
1502    #[derive(Debug)]
1503    #[allow(dead_code)]
1504    struct Tracked {
1505        value: i32,
1506        dropped: Arc<Mutex<bool>>,
1507    }
1508    impl Drop for Tracked {
1509        fn drop(&mut self) {
1510            *self.dropped.lock().unwrap() = true;
1511        }
1512    }
1513    impl Trace for Tracked {
1514        fn trace(&self, _: &mut MarkVisitor) {}
1515    }
1516
1517    #[derive(Debug)]
1518    #[allow(dead_code)]
1519    struct Parent {
1520        child: GcPtr<Tracked>,
1521    }
1522    impl Trace for Parent {
1523        fn trace(&self, visitor: &mut MarkVisitor) {
1524            visitor.visit(&self.child);
1525        }
1526    }
1527
1528    fn fresh_heap() -> gc_full::GcHeap {
1529        let heap = gc_full::GcHeap::new();
1530        heap.set_config(Arc::new(GcConfig::with_limits(10000, 50000)));
1531        heap
1532    }
1533
1534    #[test]
1535    fn alloc_and_get() {
1536        let heap = fresh_heap();
1537        let p = heap.alloc(42i64);
1538        assert_eq!(*p.get(), 42);
1539        assert_eq!(heap.count(), 1);
1540    }
1541
1542    #[test]
1543    fn clone_is_same_ptr() {
1544        let heap = fresh_heap();
1545        let p = heap.alloc(99i64);
1546        let q = p.clone();
1547        assert!(GcPtr::ptr_eq(&p, &q));
1548    }
1549
1550    #[test]
1551    fn parse_limit_mb_handles_valid_unset_and_malformed() {
1552        // Valid: converts megabytes to bytes.
1553        assert_eq!(gc_full::parse_limit_mb("X", Some("4"), 7), 4 * 1024 * 1024);
1554        // Surrounding whitespace is tolerated.
1555        assert_eq!(
1556            gc_full::parse_limit_mb("X", Some(" 4 "), 7),
1557            4 * 1024 * 1024
1558        );
1559        // Unset: falls back to the default.
1560        assert_eq!(gc_full::parse_limit_mb("X", None, 7), 7);
1561        // Malformed must NOT panic — it falls back to the default.
1562        assert_eq!(gc_full::parse_limit_mb("X", Some("foo"), 7), 7);
1563        assert_eq!(gc_full::parse_limit_mb("X", Some(""), 7), 7);
1564        assert_eq!(gc_full::parse_limit_mb("X", Some("-1"), 7), 7);
1565        // Absurdly large value saturates rather than overflowing.
1566        assert_eq!(
1567            gc_full::parse_limit_mb("X", Some(&usize::MAX.to_string()), 7),
1568            usize::MAX
1569        );
1570    }
1571
1572    #[test]
1573    fn collect_keeps_reachable() {
1574        let heap = fresh_heap();
1575        let dropped = Arc::new(Mutex::new(false));
1576        let p = heap.alloc(Tracked {
1577            value: 2,
1578            dropped: dropped.clone(),
1579        });
1580        heap.collect(|vis| vis.visit(&p));
1581        assert_eq!(heap.count(), 1);
1582        assert!(!*dropped.lock().unwrap());
1583    }
1584
1585    #[test]
1586    fn b1_two_isolates_independent_heaps() {
1587        use std::sync::{Arc, Barrier};
1588        // Each thread has its own ISOLATE_HEAP; allocations on one do not appear
1589        // in the other.
1590        let barrier = Arc::new(Barrier::new(2));
1591        let b1 = barrier.clone();
1592        let h1 = std::thread::Builder::new()
1593            .name("isolate-1".into())
1594            .spawn(move || {
1595                let _mutator = crate::register_mutator();
1596                // Allocate 100 objects on this isolate's heap
1597                let _ptrs: Vec<_> = (0_i64..100)
1598                    .map(|i| crate::gc_full::HEAP.alloc(i))
1599                    .collect();
1600                b1.wait(); // both threads are now at peak allocation
1601                // This isolate has exactly 100 live objects
1602                assert_eq!(
1603                    crate::gc_full::HEAP.count(),
1604                    100,
1605                    "isolate-1 heap count should be 100"
1606                );
1607            })
1608            .unwrap();
1609
1610        let b2 = barrier.clone();
1611        let h2 = std::thread::Builder::new()
1612            .name("isolate-2".into())
1613            .spawn(move || {
1614                let _mutator = crate::register_mutator();
1615                // Allocate 200 objects on this isolate's heap
1616                let _ptrs: Vec<_> = (0_i64..200)
1617                    .map(|i| crate::gc_full::HEAP.alloc(i))
1618                    .collect();
1619                b2.wait();
1620                // This isolate has exactly 200 live objects, unaffected by isolate-1
1621                assert_eq!(
1622                    crate::gc_full::HEAP.count(),
1623                    200,
1624                    "isolate-2 heap count should be 200"
1625                );
1626            })
1627            .unwrap();
1628
1629        h1.join().expect("isolate-1 panicked");
1630        h2.join().expect("isolate-2 panicked");
1631    }
1632
1633    #[test]
1634    fn b1_two_isolates_gc_independently() {
1635        // Two threads run allocation-heavy loops and GC their own heaps independently.
1636        let h1 = std::thread::Builder::new()
1637            .name("gc-isolate-1".into())
1638            .spawn(|| {
1639                let _mutator = crate::register_mutator();
1640                let heap = &crate::gc_full::HEAP;
1641                heap.set_config(Arc::new(GcConfig::with_limits(16_384, 65_536)));
1642                // Allocate in batches and collect; each collection touches only this heap
1643                for _ in 0..5 {
1644                    let _ptrs: Vec<_> = (0_i64..50).map(|i| heap.alloc(i)).collect();
1645                    // drive a manual collect with no roots so objects are freed
1646                    heap.collect(|_| {});
1647                    heap.collect(|_| {}); // second pass clears grace-period objects
1648                }
1649                // After all collections the heap should be empty (or close to it).
1650                // We don't assert an exact count because alloc_frame roots may keep
1651                // some alive; just assert we can collect without panicking.
1652            })
1653            .unwrap();
1654
1655        let h2 = std::thread::Builder::new()
1656            .name("gc-isolate-2".into())
1657            .spawn(|| {
1658                let _mutator = crate::register_mutator();
1659                let heap = &crate::gc_full::HEAP;
1660                heap.set_config(Arc::new(GcConfig::with_limits(16_384, 65_536)));
1661                for _ in 0..5 {
1662                    let _ptrs: Vec<_> = (0_i64..50).map(|i| heap.alloc(i)).collect();
1663                    heap.collect(|_| {});
1664                    heap.collect(|_| {});
1665                }
1666            })
1667            .unwrap();
1668
1669        h1.join().expect("gc-isolate-1 panicked");
1670        h2.join().expect("gc-isolate-2 panicked");
1671    }
1672}
1673
1674#[cfg(all(test, feature = "no-gc"))]
1675mod nogc_tests {
1676    use super::*;
1677    use alloc_ctx::{ScratchGuard, StaticCtxGuard};
1678
1679    #[test]
1680    fn cached_parent_survives_a_child_mutation_into_another_scope() {
1681        struct Node {
1682            child: Option<GcPtr<Node>>,
1683            label: String,
1684        }
1685        impl Trace for Node {
1686            fn immutable_children(&self) -> bool {
1687                true
1688            }
1689            fn trace(&self, visitor: &mut MarkVisitor) {
1690                if let Some(child) = &self.child {
1691                    visitor.visit(child);
1692                }
1693            }
1694        }
1695        struct Root(GcPtr<Node>);
1696        impl Trace for Root {
1697            fn trace(&self, visitor: &mut MarkVisitor) {
1698                visitor.visit(&self.0);
1699            }
1700        }
1701        let (parent, lease) = {
1702            let _source = ScratchGuard::new();
1703            let mut child = GcPtr::new(Node {
1704                child: None,
1705                label: "middle".into(),
1706            });
1707            let parent = GcPtr::new(Node {
1708                child: Some(child.clone()),
1709                label: "parent".into(),
1710            });
1711            let lease = region::RegionLease::for_value(&Root(parent.clone()));
1712            {
1713                let _new_source = ScratchGuard::new();
1714                child.get_mut().child = Some(GcPtr::new(Node {
1715                    child: None,
1716                    label: "new".into(),
1717                }));
1718            }
1719            // This stops at the cached parent. The child's mutation guard must
1720            // have acquired the newly referenced region independently.
1721            let cached = region::RegionLease::for_value(&Root(parent.clone()));
1722            drop(lease);
1723            (parent, cached)
1724        };
1725        assert_eq!(
1726            parent
1727                .get()
1728                .child
1729                .as_ref()
1730                .unwrap()
1731                .get()
1732                .child
1733                .as_ref()
1734                .unwrap()
1735                .get()
1736                .label,
1737            "new"
1738        );
1739        drop(lease);
1740    }
1741
1742    #[test]
1743    fn invocation_caches_follow_their_mutator_and_expire_before_reuse() {
1744        let first = alloc_ctx::InvocationGuard::new(4096);
1745        alloc_ctx::invocation_cache_insert("test", "same".into(), 1);
1746        let mut other = mutator::MutatorState::fresh();
1747        other.swap_with_current();
1748        {
1749            let _second = alloc_ctx::InvocationGuard::new(4096);
1750            assert!(alloc_ctx::invocation_cache_get("test", "same").is_none());
1751            alloc_ctx::invocation_cache_insert("test", "same".into(), 2);
1752        }
1753        other.swap_with_current();
1754        assert_eq!(alloc_ctx::invocation_cache_get("test", "same"), Some(1));
1755        drop(first);
1756        let _next = alloc_ctx::InvocationGuard::new(4096);
1757        assert!(alloc_ctx::invocation_cache_get("test", "same").is_none());
1758    }
1759
1760    #[test]
1761    fn immutable_flags_do_not_grow_boxes_or_change_identity() {
1762        assert_eq!(std::mem::size_of::<GcBox<i64>>(), 16);
1763        let _source = ScratchGuard::new();
1764        let original = GcPtr::new(String::from("compact"));
1765        let copy = unsafe { original.clone_immutable() };
1766        assert!(copy.is_relocated_immutable());
1767        assert!(!original.is_relocated_immutable());
1768        assert!(GcPtr::identity_eq(&original, &copy));
1769    }
1770
1771    #[test]
1772    fn alloc_in_static_context() {
1773        let _g = StaticCtxGuard::new();
1774        let p = GcPtr::new(42i64);
1775        assert_eq!(*p.get(), 42);
1776    }
1777
1778    #[test]
1779    fn alloc_in_scratch_region() {
1780        let mut scratch = ScratchGuard::new();
1781        let p = GcPtr::new(99i64);
1782        assert_eq!(*p.get(), 99);
1783        scratch.pop_for_return();
1784        assert_eq!(*p.get(), 99);
1785        // scratch drops here, resets the region
1786    }
1787
1788    #[test]
1789    fn ptr_eq() {
1790        let _g = StaticCtxGuard::new();
1791        let p = GcPtr::new(1i64);
1792        let q = p.clone();
1793        assert!(GcPtr::ptr_eq(&p, &q));
1794    }
1795}