diff --git a/oscars/Cargo.toml b/oscars/Cargo.toml index afe9ddf..87c8265 100644 --- a/oscars/Cargo.toml +++ b/oscars/Cargo.toml @@ -33,4 +33,5 @@ default = ["mark_sweep"] std = [] mark_sweep = [] mark_sweep2 = ["mark_sweep"] +mark_sweep_branded = ["mark_sweep"] thin-vec = ["dep:thin-vec", "mark_sweep"] diff --git a/oscars/src/alloc/mempool3/alloc.rs b/oscars/src/alloc/mempool3/alloc.rs index d7ea091..64e5ba3 100644 --- a/oscars/src/alloc/mempool3/alloc.rs +++ b/oscars/src/alloc/mempool3/alloc.rs @@ -59,16 +59,28 @@ impl<'pool> ErasedPoolPointer<'pool> { } /// typed pointer into a pool slot -#[derive(Debug, Clone, Copy)] #[repr(transparent)] -pub struct PoolPointer<'pool, T>(NonNull>, PhantomData<&'pool T>); +pub struct PoolPointer<'pool, T: ?Sized>(NonNull>, PhantomData<(&'pool (), *mut T)>); -impl<'pool, T> PoolPointer<'pool, T> { - pub(crate) unsafe fn from_raw(raw: NonNull>) -> Self { - Self(raw, PhantomData) +impl<'pool, T: ?Sized> core::fmt::Debug for PoolPointer<'pool, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "PoolPointer({:p})", self.0.as_ptr()) } +} + +impl<'pool, T: ?Sized> Clone for PoolPointer<'pool, T> { + fn clone(&self) -> Self { + *self + } +} + +impl<'pool, T: ?Sized> Copy for PoolPointer<'pool, T> {} - pub fn as_inner_ref(&self) -> &'pool T { +impl<'pool, T: ?Sized> PoolPointer<'pool, T> { + pub fn as_inner_ref(&self) -> &'pool T + where + T: 'pool, + { // SAFETY: pointer is valid and properly aligned unsafe { &(*self.0.as_ptr()).0 } } @@ -77,7 +89,10 @@ impl<'pool, T> PoolPointer<'pool, T> { self.0 } - pub fn to_erased(self) -> ErasedPoolPointer<'pool> { + pub fn to_erased(self) -> ErasedPoolPointer<'pool> + where + T: Sized, + { ErasedPoolPointer(self.0.cast::(), PhantomData) } @@ -92,6 +107,12 @@ impl<'pool, T> PoolPointer<'pool, T> { } } +impl<'pool, T> PoolPointer<'pool, T> { + pub(crate) unsafe fn from_raw(raw: NonNull>) -> Self { + Self(raw, PhantomData) + } +} + // ==== SlotPool ==== // impl core::fmt::Debug for SlotPool { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { @@ -310,6 +331,18 @@ impl SlotPool { self.live.set(self.live.get().saturating_sub(1)); } + /// Iterates over all live (allocated) slot pointers in this pool. + pub(crate) fn iter_live(&self) -> impl Iterator> + '_ { + (0..self.slot_count).filter_map(move |i| { + let chunk = self.bitmap_chunk(i); + if chunk.get() & (1u64 << (i % 64)) != 0 { + Some(self.slot_ptr(i)) + } else { + None + } + }) + } + /// returns true when the pool is empty and safe to drop /// `live` tracks the count, so no bitmap scan is needed pub fn run_drop_check(&self) -> bool { diff --git a/oscars/src/alloc/mempool3/mod.rs b/oscars/src/alloc/mempool3/mod.rs index 9382548..90a8f23 100644 --- a/oscars/src/alloc/mempool3/mod.rs +++ b/oscars/src/alloc/mempool3/mod.rs @@ -19,6 +19,7 @@ pub enum PoolAllocError { LayoutError(LayoutError), OutOfMemory, AlignmentNotPossible, + AllocIdExhausted, } impl From for PoolAllocError { @@ -121,6 +122,13 @@ impl<'alloc> PoolAllocator<'alloc> { self.current_heap_size } + /// Iterates over every live slot pointer across all slot pools. + /// + /// Yields one `NonNull` per allocated (not yet freed) slot. + pub fn iter_live_slots(&self) -> impl Iterator> + '_ { + self.slot_pools.iter().flat_map(|pool| pool.iter_live()) + } + pub fn is_below_threshold(&self) -> bool { // keep 25% headroom so collection fires before the last page fills let margin = self.heap_threshold / 4; diff --git a/oscars/src/collectors/mark_sweep_branded/cell.rs b/oscars/src/collectors/mark_sweep_branded/cell.rs new file mode 100644 index 0000000..ebc2fa4 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/cell.rs @@ -0,0 +1,71 @@ +//! Interior mutability for GC-managed values. + +use crate::collectors::mark_sweep_branded::trace::{Finalize, Trace, Tracer}; +use core::cell::{Ref, RefCell, RefMut}; +use core::ops::{Deref, DerefMut}; + +/// A GC-aware wrapper around [`RefCell`]. +pub struct GcRefCell { + inner: RefCell, +} + +impl GcRefCell { + /// Wraps `value` in a new `GcRefCell`. + pub fn new(value: T) -> Self { + Self { + inner: RefCell::new(value), + } + } + + /// Acquires a shared borrow of the inner value. + /// + /// # Panics + /// + /// Panics if the value is currently mutably borrowed. + pub fn borrow(&self) -> GcRef<'_, T> { + GcRef(self.inner.borrow()) + } + + /// Acquires a mutable borrow of the inner value. + /// + /// # Panics + /// + /// Panics if the value is currently borrowed. + pub fn borrow_mut(&self) -> GcRefMut<'_, T> { + GcRefMut(self.inner.borrow_mut()) + } +} + +/// A shared borrow guard returned by [`GcRefCell::borrow`]. +pub struct GcRef<'a, T: Trace>(Ref<'a, T>); + +impl Deref for GcRef<'_, T> { + type Target = T; + fn deref(&self) -> &T { + &self.0 + } +} + +/// A mutable borrow guard returned by [`GcRefCell::borrow_mut`]. +pub struct GcRefMut<'a, T: Trace>(RefMut<'a, T>); + +impl Deref for GcRefMut<'_, T> { + type Target = T; + fn deref(&self) -> &T { + &self.0 + } +} + +impl DerefMut for GcRefMut<'_, T> { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl Finalize for GcRefCell {} + +impl Trace for GcRefCell { + fn trace(&mut self, tracer: &mut Tracer) { + self.inner.get_mut().trace(tracer); + } +} diff --git a/oscars/src/collectors/mark_sweep_branded/ephemeron.rs b/oscars/src/collectors/mark_sweep_branded/ephemeron.rs new file mode 100644 index 0000000..e77771b --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/ephemeron.rs @@ -0,0 +1,49 @@ +use crate::{ + alloc::mempool3::PoolPointer, + collectors::mark_sweep_branded::{ + gc::Gc, + gc_box::GcBox, + mutation_ctx::MutationContext, + trace::{Finalize, Trace, Tracer}, + }, +}; +use core::marker::PhantomData; + +pub struct Ephemeron<'id, K: Trace, V: Trace> { + pub(crate) key_ptr: Option>>, + pub(crate) key_alloc_id: usize, + pub(crate) value_ptr: PoolPointer<'static, GcBox>, + pub(crate) _marker: PhantomData<*mut &'id ()>, +} + +impl<'id, K: Trace, V: Trace> Ephemeron<'id, K, V> { + /// Returns the value if the key is alive. + pub fn get_value<'gc>(&self, _cx: &MutationContext<'id, 'gc>) -> Option> { + // SAFETY: `_cx` proves the collector is alive, alloc_id guards ABA + let key_alive = self + .key_ptr + .is_some_and(|p| unsafe { (*p.as_ptr().as_ptr()).0.alloc_id == self.key_alloc_id }); + if key_alive { + Some(Gc { + ptr: self.value_ptr, + _marker: PhantomData, + }) + } else { + None + } + } +} + +impl<'id, K: Trace, V: Trace> Clone for Ephemeron<'id, K, V> { + fn clone(&self) -> Self { + *self + } +} + +impl<'id, K: Trace, V: Trace> Copy for Ephemeron<'id, K, V> {} + +impl<'id, K: Trace, V: Trace> Finalize for Ephemeron<'id, K, V> {} + +impl<'id, K: Trace, V: Trace> Trace for Ephemeron<'id, K, V> { + fn trace(&mut self, _tracer: &mut Tracer) {} +} diff --git a/oscars/src/collectors/mark_sweep_branded/gc.rs b/oscars/src/collectors/mark_sweep_branded/gc.rs new file mode 100644 index 0000000..032d540 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/gc.rs @@ -0,0 +1,58 @@ +//! Core pointer types. + +use crate::{ + alloc::mempool3::PoolPointer, + collectors::mark_sweep_branded::{ + gc_box::GcBox, + trace::{Finalize, Trace}, + }, +}; +use core::fmt; +use core::marker::PhantomData; +use core::ops::Deref; + +/// A transient pointer to a GC-managed value. +#[derive(Debug)] +pub struct Gc<'gc, T: Trace + ?Sized + 'gc> { + pub(crate) ptr: PoolPointer<'static, GcBox>, + pub(crate) _marker: PhantomData<(&'gc T, *const ())>, +} + +impl<'gc, T: Trace + ?Sized + 'gc> Copy for Gc<'gc, T> {} +impl<'gc, T: Trace + ?Sized + 'gc> Clone for Gc<'gc, T> { + fn clone(&self) -> Self { + *self + } +} + +impl<'gc, T: Trace + 'gc> Gc<'gc, T> { + /// Returns a shared reference to the value. + #[inline] + pub fn get(&self) -> &T { + // SAFETY: `ptr` is non-null and valid for `'gc` by construction. + // The `'gc` lifetime is scoped to a `mutate()` closure, collection only occurs + // via `cx.collect()` within that same closure and `Gc<'gc, T>` can't + // escape the closure. + unsafe { &(*self.ptr.as_ptr().as_ptr()).0.value } + } +} + +impl<'gc, T: Trace + fmt::Display + 'gc> fmt::Display for Gc<'gc, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.get(), f) + } +} + +impl<'gc, T: Trace + 'gc> Deref for Gc<'gc, T> { + type Target = T; + fn deref(&self) -> &T { + self.get() + } +} + +impl Finalize for Gc<'_, T> {} +impl Trace for Gc<'_, T> { + fn trace(&mut self, tracer: &mut crate::collectors::mark_sweep_branded::trace::Tracer) { + tracer.mark(self); + } +} diff --git a/oscars/src/collectors/mark_sweep_branded/gc_box.rs b/oscars/src/collectors/mark_sweep_branded/gc_box.rs new file mode 100644 index 0000000..f4a300d --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/gc_box.rs @@ -0,0 +1,67 @@ +//! The heap header wrapping every GC-managed value. + +use core::cell::Cell; +use core::ptr::NonNull; + +use crate::alloc::mempool3::{PoolAllocator, PoolItem}; +use crate::collectors::mark_sweep_branded::trace::{Trace, TraceFn, Tracer}; + +pub(crate) type DropFn = unsafe fn(&mut PoolAllocator<'static>, NonNull); + +/// The tri-color marking state of a [`GcBox`] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub(crate) enum GcColor { + /// Not yet reached by mark phase + White = 0, + /// Reached and queued in the worklist, children not yet traced. + Gray = 1, + /// Reached and dequeued from the worklist, all children traced + Black = 2, +} + +/// Heap wrapper for a garbage-collected value. +/// +/// Allocated via [`PoolAllocator`]. +pub(crate) struct GcBox { + /// tricolor marking state, updated by the mark phase + pub(crate) color: Cell, + /// Type-erased trace function. + pub(crate) trace_fn: TraceFn, + /// Type-erased finalize and free fn + pub(crate) drop_fn: DropFn, + /// Allocation ID used to validate weak pointers. + pub(crate) alloc_id: usize, + /// The user value. + pub(crate) value: T, +} + +impl GcBox { + pub(crate) const FREED_ALLOC_ID: usize = usize::MAX; +} + +impl GcBox { + /// Create a [`GcBox`] for `value`, `color` starts as [`GcColor::White`] + pub(crate) fn new(value: T, trace_fn: TraceFn, drop_fn: DropFn, alloc_id: usize) -> Self { + Self { + color: Cell::new(GcColor::White), + trace_fn, + drop_fn, + alloc_id, + value, + } + } +} + +/// type-erased trace function for a `GcBox` slot. +/// +/// # Safety +/// +/// `ptr` must point to a live `PoolItem>` in the pool allocator +pub(crate) unsafe fn trace_value(ptr: NonNull, tracer: &mut Tracer<'_>) { + let pool_item_ptr = ptr.cast::>>(); + unsafe { + (*pool_item_ptr.as_ptr()).0.color.set(GcColor::Black); + (*pool_item_ptr.as_ptr()).0.value.trace(tracer); + } +} diff --git a/oscars/src/collectors/mark_sweep_branded/mod.rs b/oscars/src/collectors/mark_sweep_branded/mod.rs new file mode 100644 index 0000000..3f78a24 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/mod.rs @@ -0,0 +1,302 @@ +//! Lifetime-branded mark and sweep garbage collector +#![cfg_attr(not(any(test, feature = "std")), allow(unused_imports))] + +pub mod cell; +pub mod ephemeron; +pub mod gc; +pub mod gc_box; +pub mod mutation_ctx; +pub mod root; +pub mod trace; +pub mod weak; + +#[cfg(all(test, feature = "mark_sweep_branded"))] +mod tests; + +pub use cell::GcRefCell; +pub use ephemeron::Ephemeron; +pub use gc::Gc; +pub use mutation_ctx::MutationContext; +pub use root::Root; +pub use trace::{Finalize, Trace, Tracer}; +pub use weak::WeakGc; + +use crate::alloc::mempool3::{PoolAllocError, PoolAllocator, PoolPointer}; +use core::cell::{Cell, RefCell}; +use core::marker::PhantomData; +use core::ptr::NonNull; +use gc_box::{DropFn, GcBox, GcColor}; +use root::RootSentinel; +use rust_alloc::vec::Vec; + +/// Type-erased ephemeron registration. +pub(crate) struct EphemeronEntry { + pub(crate) key_ptr: Option>>, + pub(crate) value_ptr: PoolPointer<'static, GcBox<()>>, +} + +pub(crate) struct Collector { + // SAFETY: We use 'static here because the PoolAllocator owns its memory, + // and we ensure that `Gc` objects and pool allocations do not outlive + // the `Collector` instance + pub(crate) pool: RefCell>, + /// Dedicated pool for RootNode allocations + pub(crate) root_pool: RefCell>, + pub(crate) sentinel: RootSentinel, + pub(crate) generic_alloc_id: Cell, + pub(crate) ephemerons: RefCell>, +} + +impl Collector { + fn new() -> Self { + Self { + pool: RefCell::new(PoolAllocator::default()), + root_pool: RefCell::new(PoolAllocator::default()), + sentinel: RootSentinel::new(), + generic_alloc_id: Cell::new(0), + ephemerons: RefCell::new(Vec::new()), + } + } + + /// Registers an ephemeron key/value pair for processing during collection. + pub(crate) fn register_ephemeron( + &self, + key_ptr: PoolPointer<'static, GcBox<()>>, + value_ptr: PoolPointer<'static, GcBox<()>>, + ) { + self.ephemerons.borrow_mut().push(EphemeronEntry { + key_ptr: Some(key_ptr), + value_ptr, + }); + } + + /// Allocates a RootNode from the dedicated root pool and links it into the root list. + pub(crate) fn try_alloc_root_node<'id, T: trace::Trace>( + &self, + gc_ptr: PoolPointer<'static, GcBox>, + ) -> Result>, PoolAllocError> { + let mut pool = self.root_pool.borrow_mut(); + let ptr = pool.try_alloc(root::RootNode::new_in(gc_ptr, self))?; + // SAFETY: PoolItem is repr(transparent) over T; pointer address is identical. + let raw = ptr.as_ptr().cast::>(); + // SAFETY: `raw` points to a stable `RootNode` allocated in the pool. + unsafe { + root::RootLink::link_after(self.sentinel.as_ptr(), raw.cast::()); + } + Ok(raw) + } + + /// Frees a RootNode back to the root pool. + pub(crate) fn free_root_node(&self, ptr: NonNull, drop_fn: root::RootDropFn) { + let mut pool = self.root_pool.borrow_mut(); + unsafe { + (drop_fn)(&mut pool, ptr); + } + } + + /// Allocates a value from the pool. + /// + /// # Errors + /// + /// Returns `Err(PoolAllocError::AllocIdExhausted)` if the allocation ID counter + /// has reached `FREED_ALLOC_ID` (`usize::MAX`). This is a theoretical limit + /// that would require `usize::MAX - 1` allocations. + pub(crate) fn try_alloc<'gc, T: trace::Trace + trace::Finalize + 'gc>( + &'gc self, + value: T, + ) -> Result, PoolAllocError> { + let alloc_id = self.generic_alloc_id.get(); + + // Check for alloc_id wrap before incrementing. + // If alloc_id reaches FREED_ALLOC_ID (usize::MAX), weak reference validation + // would break because freed slots are marked with this sentinel value. + if alloc_id == GcBox::<()>::FREED_ALLOC_ID { + return Err(PoolAllocError::AllocIdExhausted); + } + + self.generic_alloc_id.set(alloc_id.wrapping_add(1)); + + unsafe fn drop_and_free( + pool: &mut PoolAllocator<'static>, + ptr: NonNull, + ) { + use crate::alloc::mempool3::PoolItem; + unsafe { + let typed_ptr = ptr.cast::>>(); + (*typed_ptr.as_ptr()).0.value.finalize(); + core::ptr::drop_in_place(typed_ptr.as_ptr()); + pool.free_slot(ptr); + } + } + + let mut pool = self.pool.borrow_mut(); + let ptr = pool.try_alloc(GcBox::new( + value, + gc_box::trace_value::, + drop_and_free::, + alloc_id, + ))?; + + drop(pool); + + Ok(Gc { + ptr: unsafe { ptr.extend_lifetime() }, + _marker: PhantomData, + }) + } + + /// Runs a collection cycle + pub(crate) fn collect(&self) { + let mut tracer = Tracer::new(); + + for link_ptr in self.sentinel.iter() { + unsafe { + // SAFETY: link_ptr points to the `link` field which is first in repr(C) RootNode. + // Casting to ErasedRootNode (also repr(C), same first two fields) lets us read + // gc_ptr without knowing T, avoiding manual offset arithmetic. + let erased = link_ptr.cast::(); + tracer.mark_raw((*erased.as_ptr()).gc_ptr.as_ptr().cast::()); + } + } + + tracer.drain(); + + // Phase 2: ephemeron fixpoint. + // If marking a value causes new keys of other ephemerons to become + // reachable, we must iterate until no further values are marked. + loop { + let mut any_newly_marked = false; + for entry in self.ephemerons.borrow().iter() { + let Some(key_ptr) = entry.key_ptr else { + continue; + }; + unsafe { + if (*key_ptr.as_ptr().as_ptr()).0.color.get() != GcColor::White { + any_newly_marked |= tracer.mark_raw(entry.value_ptr.as_ptr().cast::()); + } + } + } + if !any_newly_marked { + break; + } + tracer.drain(); + } + + // Phase 3: sweep all slots. Collect unmarked ones, then invalidate and free them. + use crate::alloc::mempool3::PoolItem; + let dead: Vec<(NonNull, DropFn)> = { + let pool = self.pool.borrow(); + pool.iter_live_slots() + .filter_map(|ptr| unsafe { + let gc_box = &(*ptr.cast::>>().as_ptr()).0; + if gc_box.color.get() == GcColor::Black { + gc_box.color.set(GcColor::White); + None + } else { + Some((ptr, gc_box.drop_fn)) + } + }) + .collect() + }; + { + let mut pool = self.pool.borrow_mut(); + for (ptr, drop_fn) in dead { + unsafe { + (*ptr.cast::>>().as_ptr()).0.alloc_id = + GcBox::<()>::FREED_ALLOC_ID; + (drop_fn)(&mut pool, ptr); + } + } + } + + // Phase 4: remove ephemeron entries whose key was swept this cycle. + // A swept key has alloc_id set to FREED_ALLOC_ID by the sweep above. + // Using the Option lets us express the invalid state without a stored alloc_id. + self.ephemerons.borrow_mut().retain(|entry| { + entry.key_ptr.is_some_and(|key_ptr| unsafe { + (*key_ptr.as_ptr().as_ptr()).0.alloc_id != GcBox::<()>::FREED_ALLOC_ID + }) + }); + } +} + +impl Drop for Collector { + /// Frees all remaining allocations + fn drop(&mut self) { + use crate::alloc::mempool3::PoolItem; + + // Free all root nodes first + let all_roots: Vec<(NonNull, root::RootDropFn)> = self + .root_pool + .borrow() + .iter_live_slots() + .map(|ptr| unsafe { + let drop_fn = (*ptr.cast::>>().as_ptr()) + .0 + .drop_fn; + (ptr, drop_fn) + }) + .collect(); + let mut root_pool = self.root_pool.borrow_mut(); + for (ptr, drop_fn) in all_roots { + unsafe { + (drop_fn)(&mut root_pool, ptr); + } + } + drop(root_pool); + + // Then free all GC allocations + let all: Vec<(NonNull, DropFn)> = self + .pool + .borrow() + .iter_live_slots() + .map(|ptr| unsafe { + let drop_fn = (*ptr.cast::>>().as_ptr()).0.drop_fn; + (ptr, drop_fn) + }) + .collect(); + let mut pool = self.pool.borrow_mut(); + for (ptr, drop_fn) in all { + unsafe { + (*ptr.cast::>>().as_ptr()).0.alloc_id = + GcBox::<()>::FREED_ALLOC_ID; + (drop_fn)(&mut pool, ptr); + } + } + } +} + +/// Owns the garbage collector and carries the `'id` context brand +pub struct GcContext<'id> { + collector: Collector, + _marker: PhantomData<*mut &'id ()>, +} + +impl<'id> GcContext<'id> { + /// Opens a mutation window and passes a [`MutationContext`] to `f`. + /// Triggers a gc cycle + pub fn collect(&self) { + self.collector.collect(); + } + + pub fn mutate(&self, f: impl for<'gc> FnOnce(&MutationContext<'id, 'gc>) -> R) -> R { + let cx = MutationContext { + collector: &self.collector, + _marker: PhantomData, + }; + f(&cx) + } + + #[cfg(test)] + pub(crate) fn ephemeron_count(&self) -> usize { + self.collector.ephemerons.borrow().len() + } +} + +/// Creates a new GC context. +pub fn with_gc FnOnce(GcContext<'id>) -> R>(f: F) -> R { + f(GcContext { + collector: Collector::new(), + _marker: PhantomData, + }) +} diff --git a/oscars/src/collectors/mark_sweep_branded/mutation_ctx.rs b/oscars/src/collectors/mark_sweep_branded/mutation_ctx.rs new file mode 100644 index 0000000..7c9d788 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/mutation_ctx.rs @@ -0,0 +1,81 @@ +//! `MutationContext<'id, 'gc>` handle. + +use crate::{ + alloc::mempool3::{PoolAllocError, PoolPointer}, + collectors::mark_sweep_branded::{ + Collector, + ephemeron::Ephemeron, + gc::Gc, + gc_box::GcBox, + root::Root, + trace::{Finalize, Trace}, + weak::WeakGc, + }, +}; +use core::marker::PhantomData; + +/// Handle for GC allocations +pub struct MutationContext<'id, 'gc> { + pub(crate) collector: &'gc Collector, + pub(crate) _marker: PhantomData<*mut &'id ()>, +} + +impl<'id, 'gc> MutationContext<'id, 'gc> { + /// Allocates a value on the GC heap. + pub fn try_alloc( + &self, + value: T, + ) -> Result, PoolAllocError> { + self.collector.try_alloc(value) + } + + /// Downgrades a `Gc` into a weak reference + pub fn alloc_weak(&self, gc: Gc<'gc, T>) -> WeakGc<'id, T> { + let alloc_id = unsafe { (*gc.ptr.as_ptr().as_ptr()).0.alloc_id }; + WeakGc { + ptr: gc.ptr, + alloc_id, + _marker: PhantomData, + } + } + + /// Promotes a `Gc` pointer to a `Root` + pub fn root( + &self, + gc: Gc<'gc, T>, + ) -> Result, PoolAllocError> { + let raw = self.collector.try_alloc_root_node(gc.ptr)?; + Ok(Root { raw }) + } + + /// Creates an ephemeron binding `key` to `value`. + /// + /// The value is kept alive by the collector as long as the key remains + /// reachable from a root. Once the key is collected, `get_value` returns + /// `None` and the value is eligible for collection on the next cycle. + pub fn alloc_ephemeron( + &self, + key: Gc<'gc, K>, + value: Gc<'gc, V>, + ) -> Ephemeron<'id, K, V> { + let key_alloc_id = unsafe { (*key.ptr.as_ptr().as_ptr()).0.alloc_id }; + // SAFETY: GcBox and GcBox are erased to GcBox<()>, the collector + // only reads the fixed size prefix fields via this pointer + let erased_key: PoolPointer<'static, GcBox<()>> = + unsafe { key.ptr.to_erased().to_typed_pool_pointer::>() }; + let erased_value: PoolPointer<'static, GcBox<()>> = + unsafe { value.ptr.to_erased().to_typed_pool_pointer::>() }; + self.collector.register_ephemeron(erased_key, erased_value); + Ephemeron { + key_ptr: Some(key.ptr), + key_alloc_id, + value_ptr: value.ptr, + _marker: core::marker::PhantomData, + } + } + + /// Triggers a gc cycle. + pub fn collect(&self) { + self.collector.collect(); + } +} diff --git a/oscars/src/collectors/mark_sweep_branded/root.rs b/oscars/src/collectors/mark_sweep_branded/root.rs new file mode 100644 index 0000000..a77fa1c --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/root.rs @@ -0,0 +1,195 @@ +use crate::{ + alloc::mempool3::{PoolAllocator, PoolPointer}, + collectors::mark_sweep_branded::{ + gc::Gc, gc_box::GcBox, mutation_ctx::MutationContext, trace::Trace, + }, +}; +use core::cell::Cell; +use core::marker::PhantomData; +use core::ptr::NonNull; + +pub(crate) type RootDropFn = unsafe fn(&mut PoolAllocator<'static>, NonNull); + +/// Intrusive link node +pub(crate) struct RootLink { + prev: Cell>>, + next: Cell>>, +} + +impl RootLink { + pub(crate) const fn new() -> Self { + Self { + prev: Cell::new(None), + next: Cell::new(None), + } + } + + /// Returns true if this node is currently part of a list + #[inline] + pub(crate) fn is_linked(&self) -> bool { + self.prev.get().is_some() + } + + /// Inserts `node` immediately after `anchor` + /// + /// # Safety + /// + /// Both `anchor` and `node` must remain at stable addresses until unlinked. + pub(crate) unsafe fn link_after(anchor: NonNull, node: NonNull) { + unsafe { + let anchor_ref = anchor.as_ref(); + let node_ref = node.as_ref(); + let old_next = anchor_ref.next.get(); + + node_ref.prev.set(Some(anchor)); + node_ref.next.set(old_next); + anchor_ref.next.set(Some(node)); + + if let Some(next) = old_next { + next.as_ref().prev.set(Some(node)); + } + } + } + + /// Removes `node` from the list. + /// + /// # Safety + /// + /// `node` must currently be linked. + pub(crate) unsafe fn unlink(node: NonNull) { + unsafe { + let node_ref = node.as_ref(); + let prev = node_ref.prev.get(); + let next = node_ref.next.get(); + + if let Some(p) = prev { + p.as_ref().next.set(next); + } + if let Some(n) = next { + n.as_ref().prev.set(prev); + } + + node_ref.prev.set(None); + node_ref.next.set(None); + } + } +} + +pub(crate) struct RootLinkIter { + current: Option>, +} + +impl Iterator for RootLinkIter { + type Item = NonNull; + + fn next(&mut self) -> Option { + let node = self.current?; + // SAFETY: nodes are pinned and valid during collection. + self.current = unsafe { node.as_ref().next.get() }; + Some(node) + } +} + +/// Sentinel node for the root list +#[repr(transparent)] +pub(crate) struct RootSentinel(core::pin::Pin>); + +impl RootSentinel { + pub(crate) fn new() -> Self { + Self(rust_alloc::boxed::Box::pin(RootLink::new())) + } + + /// Returns a pointer to the underlying RootLink + pub(crate) fn as_ptr(&self) -> NonNull { + // SAFETY: The sentinel is pinned and the pointer is derived from a valid Box. + unsafe { + NonNull::new_unchecked(self.0.as_ref().get_ref() as *const RootLink as *mut RootLink) + } + } + + /// Returns an iterator over all root nodes after this sentinel. + pub(crate) fn iter(&self) -> RootLinkIter { + let first = self.0.as_ref().next.get(); + RootLinkIter { current: first } + } +} + +/// Heap node backing a [`Root`] +#[repr(C)] +pub(crate) struct RootNode<'id, T: Trace> { + pub(crate) link: RootLink, + /// Pointer to the allocation + pub(crate) gc_ptr: PoolPointer<'static, GcBox>, + /// Type-erased drop function for freeing this RootNode + pub(crate) drop_fn: RootDropFn, + /// Raw pointer to the Collector for freeing this node + pub(crate) collector_ptr: *const crate::collectors::mark_sweep_branded::Collector, + pub(crate) _marker: PhantomData<*mut &'id ()>, +} + +impl<'id, T: Trace> RootNode<'id, T> { + /// Creates a new [`RootNode`] initialised for the given `gc_ptr` and `collector` + pub(crate) fn new_in( + gc_ptr: PoolPointer<'static, GcBox>, + collector: &crate::collectors::mark_sweep_branded::Collector, + ) -> Self { + unsafe fn drop_and_free(pool: &mut PoolAllocator<'static>, ptr: NonNull) { + use crate::alloc::mempool3::PoolItem; + unsafe { + let typed_ptr = ptr.cast::>>(); + core::ptr::drop_in_place(typed_ptr.as_ptr()); + pool.free_slot(ptr); + } + } + + Self { + link: RootLink::new(), + gc_ptr, + drop_fn: drop_and_free::, + collector_ptr: collector as *const _, + _marker: PhantomData, + } + } +} + +/// Type-erased version of [`RootNode`] for use during collection. +/// +/// Since [`RootNode`] is `repr(C)` and `link` is always the first field, +/// a `NonNull` from the sentinel iterator can be safely cast to +/// `NonNull` to read `gc_ptr` without knowing `T`. +#[repr(C)] +pub(crate) struct ErasedRootNode { + pub(crate) link: RootLink, + pub(crate) gc_ptr: PoolPointer<'static, GcBox<()>>, +} + +/// A handle that keeps a GC allocation live. +#[must_use = "dropping a root unregisters it from the GC"] +pub struct Root<'id, T: Trace> { + pub(crate) raw: NonNull>, +} + +impl<'id, T: Trace> Root<'id, T> { + /// Converts this root into a `Gc` pointer + pub fn get<'gc>(&self, _cx: &MutationContext<'id, 'gc>) -> Gc<'gc, T> { + Gc { + // SAFETY: `raw` is non null and valid + ptr: unsafe { self.raw.as_ref().gc_ptr }, + _marker: PhantomData, + } + } +} + +impl<'id, T: Trace> Drop for Root<'id, T> { + fn drop(&mut self) { + unsafe { + let node_ref = self.raw.as_ref(); + if node_ref.link.is_linked() { + RootLink::unlink(NonNull::from(&node_ref.link)); + } + // SAFETY: collector_ptr is valid for the lifetime of the GcContext + let collector = &*node_ref.collector_ptr; + collector.free_root_node(self.raw.cast::(), node_ref.drop_fn); + } + } +} diff --git a/oscars/src/collectors/mark_sweep_branded/tests/api_compliance.rs b/oscars/src/collectors/mark_sweep_branded/tests/api_compliance.rs new file mode 100644 index 0000000..4258d44 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/tests/api_compliance.rs @@ -0,0 +1,47 @@ +#[cfg(test)] +mod tests { + use crate::collectors::mark_sweep_branded::root::{RootLink, RootNode}; + + #[test] + fn root_link_is_thin() { + // According to the API redesign RFC, RootLink should just be two pointers (prev, next) + // with no virtual table overhead (no trace_fn fat pointers). + assert_eq!( + core::mem::size_of::(), + core::mem::size_of::<*const ()>() * 2, + "RootLink must be exactly two words (prev and next pointers)" + ); + } + + #[test] + fn root_node_layout_guarantees() { + // The RFC relies on offset_of!(RootNode<()>, gc_ptr) working exactly due to #[repr(C)] + // with `link` at offset 0. + // We verify that the gc_ptr is always immediately following the link, regardless of T. + + let link_size = core::mem::size_of::(); + + // Assert offset is correct via offset_of-like macro conceptually + let make_dummy = || RootNode:: { + link: RootLink::new(), + gc_ptr: unsafe { + crate::alloc::mempool3::PoolPointer::from_raw(core::ptr::NonNull::dangling()) + }, + drop_fn: |_, _| {}, + collector_ptr: core::ptr::null(), + _marker: core::marker::PhantomData, + }; + + let node = make_dummy(); + let base_ptr = &node as *const _ as usize; + let link_ptr = &node.link as *const _ as usize; + let gc_ptr_addr = &node.gc_ptr as *const _ as usize; + + assert_eq!(base_ptr, link_ptr, "RootLink must be at offset 0"); + assert_eq!( + gc_ptr_addr - base_ptr, + link_size, + "gc_ptr must immediately follow RootLink" + ); + } +} diff --git a/oscars/src/collectors/mark_sweep_branded/tests/ephemeron.rs b/oscars/src/collectors/mark_sweep_branded/tests/ephemeron.rs new file mode 100644 index 0000000..b793f95 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/tests/ephemeron.rs @@ -0,0 +1,109 @@ +use super::*; + +#[test] +fn ephemeron_value_survives_when_key_is_rooted() { + with_gc(|ctx| { + let (root_key, eph) = ctx.mutate(|cx| { + let key = cx.try_alloc(1u32).unwrap(); + let value = cx.try_alloc(42u32).unwrap(); + let root_key = cx.root(key).unwrap(); + let eph = cx.alloc_ephemeron(key, value); + (root_key, eph) + }); + + ctx.collect(); + + ctx.mutate(|cx| { + let val = eph + .get_value(cx) + .expect("value must be alive while key is rooted"); + assert_eq!(*val.get(), 42); + drop(root_key); + }); + }); +} + +#[test] +fn ephemeron_value_collected_when_key_unrooted() { + with_gc(|ctx| { + let eph = ctx.mutate(|cx| { + let key = cx.try_alloc(1u32).unwrap(); + let value = cx.try_alloc(99u32).unwrap(); + cx.alloc_ephemeron(key, value) + }); + + ctx.collect(); + + ctx.mutate(|cx| { + assert!( + eph.get_value(cx).is_none(), + "value must be gone after key is swept" + ); + }); + }); +} + +#[test] +fn ephemeron_chain_fixpoint() { + // Ephemeron(root_a -> b) and Ephemeron(b -> c): + // root_a alive -> b survives via first ephemeron. + // b then alive -> c survives via second ephemeron + // This requires the collector to run multiple fixpoint passes. + with_gc(|ctx| { + let (root_a, eph_ab, eph_bc) = ctx.mutate(|cx| { + let a = cx.try_alloc(1u32).unwrap(); + let b = cx.try_alloc(2u32).unwrap(); + let c = cx.try_alloc(3u32).unwrap(); + let root_a = cx.root(a).unwrap(); + let eph_ab = cx.alloc_ephemeron(a, b); + let eph_bc = cx.alloc_ephemeron(b, c); + (root_a, eph_ab, eph_bc) + }); + + ctx.collect(); + + ctx.mutate(|cx| { + let b_val = eph_ab.get_value(cx).expect("b must survive: a is rooted"); + assert_eq!(*b_val.get(), 2); + let c_val = eph_bc + .get_value(cx) + .expect("c must survive: b is kept alive by ephemeron"); + assert_eq!(*c_val.get(), 3); + }); + + drop(root_a); + ctx.collect(); + + ctx.mutate(|cx| { + assert!( + eph_ab.get_value(cx).is_none(), + "b must be gone after a is dropped" + ); + assert!( + eph_bc.get_value(cx).is_none(), + "c must be gone after b is gone" + ); + }); + }); +} + +#[test] +fn ephemeron_entry_cleaned_up_after_sweep() { + // Verify the collector's internal ephemeron list shrinks after dead entries are swept. + with_gc(|ctx| { + ctx.mutate(|cx| { + let key = cx.try_alloc(0u32).unwrap(); + let value = cx.try_alloc(0u32).unwrap(); + cx.alloc_ephemeron(key, value); + }); + assert_eq!(ctx.ephemeron_count(), 1); + + ctx.collect(); + + assert_eq!( + ctx.ephemeron_count(), + 0, + "dead ephemeron entry must be removed after sweep" + ); + }); +} diff --git a/oscars/src/collectors/mark_sweep_branded/tests/mod.rs b/oscars/src/collectors/mark_sweep_branded/tests/mod.rs new file mode 100644 index 0000000..f766a73 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/tests/mod.rs @@ -0,0 +1,147 @@ +use super::*; + +#[derive(Debug)] +struct JsObject { + name: rust_alloc::string::String, + value: i32, +} + +impl crate::collectors::mark_sweep_branded::Trace for JsObject { + fn trace(&mut self, _tracer: &mut crate::collectors::mark_sweep_branded::trace::Tracer) {} +} +impl crate::collectors::mark_sweep_branded::Finalize for JsObject {} + +#[test] +fn unrooted_alloc_is_swept() { + with_gc(|ctx| { + let weak = ctx.mutate(|cx| { + cx.alloc_weak( + cx.try_alloc(JsObject { + name: "ephemeral".into(), + value: 999, + }) + .unwrap(), + ) + }); + ctx.collect(); + ctx.mutate(|cx| { + assert!(weak.upgrade(cx).is_none()); + }); + }); +} + +#[test] +fn rooted_alloc_survives_collection() { + with_gc(|ctx| { + let root = ctx.mutate(|cx| { + cx.root( + cx.try_alloc(JsObject { + name: "pinned".into(), + value: 42, + }) + .unwrap(), + ) + .unwrap() + }); + ctx.collect(); + ctx.mutate(|cx| { + let gc = root.get(cx); + assert_eq!(gc.get().value, 42); + assert_eq!(gc.get().name, "pinned"); + }); + }); +} + +#[test] +fn weak_upgrade_after_collection_without_root_is_none() { + with_gc(|ctx| { + let weak = ctx.mutate(|cx| { + cx.alloc_weak( + cx.try_alloc(JsObject { + name: "weak".into(), + value: 10, + }) + .unwrap(), + ) + }); + ctx.collect(); + ctx.mutate(|cx| { + assert!(weak.upgrade(cx).is_none()); + }); + }); +} + +#[test] +fn weak_upgrade_with_live_root_is_some() { + with_gc(|ctx| { + let (root, weak) = ctx.mutate(|cx| { + let obj = cx + .try_alloc(JsObject { + name: "strong".into(), + value: 7, + }) + .unwrap(); + let root = cx.root(obj).unwrap(); + + let weak = cx.alloc_weak( + cx.try_alloc(JsObject { + name: "weak_entry".into(), + value: 77, + }) + .unwrap(), + ); + (root, weak) + }); + ctx.collect(); + ctx.mutate(|cx| { + assert!(weak.upgrade(cx).is_none()); + assert_eq!(root.get(cx).get().value, 7); + }); + }); +} + +#[test] +fn multiple_roots_are_independent() { + with_gc(|ctx| { + let (root1, root2) = ctx.mutate(|cx| { + let obj1 = cx.try_alloc(100i32).unwrap(); + let obj2 = cx.try_alloc(200i32).unwrap(); + (cx.root(obj1).unwrap(), cx.root(obj2).unwrap()) + }); + + ctx.collect(); + + ctx.mutate(|cx| { + assert_eq!(*root1.get(cx).get(), 100); + assert_eq!(*root2.get(cx).get(), 200); + }); + + drop(root1); + ctx.collect(); + + ctx.mutate(|cx| { + assert_eq!(*root2.get(cx).get(), 200); + }); + }); +} + +#[test] +fn root_escapes_closure_safely() { + with_gc(|ctx| { + let root = ctx.mutate(|cx| { + let obj = cx.try_alloc(555i32).unwrap(); + cx.root(obj).unwrap() + }); + + ctx.collect(); + + ctx.mutate(|cx| { + assert_eq!(*root.get(cx).get(), 555); + }); + }); +} + +mod api_compliance; +mod ephemeron; +mod uaf; +mod ui_tests; diff --git a/oscars/src/collectors/mark_sweep_branded/tests/uaf.rs b/oscars/src/collectors/mark_sweep_branded/tests/uaf.rs new file mode 100644 index 0000000..5eab874 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/tests/uaf.rs @@ -0,0 +1,31 @@ +use crate::collectors::mark_sweep::Finalize; +use crate::collectors::mark_sweep_branded::Trace; +use crate::collectors::mark_sweep_branded::with_gc; +use core::cell::Cell; + +struct DetectDrop<'a>(&'a Cell); + +impl<'a> Trace for DetectDrop<'a> { + fn trace(&mut self, _tracer: &mut crate::collectors::mark_sweep_branded::trace::Tracer) {} +} + +impl Finalize for DetectDrop<'_> {} + +impl Drop for DetectDrop<'_> { + fn drop(&mut self) { + self.0.set(true); + } +} + +#[test] +fn test_uaf() { + with_gc(|cx| { + let dropped = Cell::new(false); + cx.mutate(|mcx| { + let _gc = mcx.try_alloc(DetectDrop(&dropped)).unwrap(); + }); + cx.collect(); // Garbage collects 'gc' because it isn't rooted! + assert!(dropped.get(), "It wasn't collected!"); + // The pointer _gc is safely out of scope here! Compiler prevents accessing it. + }); +} diff --git a/oscars/src/collectors/mark_sweep_branded/tests/ui/gc_cannot_escape_mutate.rs b/oscars/src/collectors/mark_sweep_branded/tests/ui/gc_cannot_escape_mutate.rs new file mode 100644 index 0000000..afbbcd8 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/tests/ui/gc_cannot_escape_mutate.rs @@ -0,0 +1,18 @@ +//! Compile-fail: `Gc<'gc, T>` cannot escape the `mutate()` closure. +//! +//! `for<'gc>` in `GcContext::mutate` makes `'gc` universally quantified inside +//! the closure. The return type `R` cannot mention `'gc`, so the borrow checker +//! rejects any attempt to return a `Gc<'gc, T>` from `mutate`. + +use oscars::collectors::mark_sweep_branded::with_gc; + +fn main() { + with_gc(|ctx| { + // The closure must return R for some R that does not mention 'gc. + // Returning Gc<'gc, i32> directly attempts to leak a shorter lifetime + // into the outer scope. The compiler must reject this. + let _escaped = ctx.mutate(|cx| { + cx.try_alloc(42i32).unwrap() // ERROR: Gc<'gc, i32> cannot escape mutate() + }); + }); +} diff --git a/oscars/src/collectors/mark_sweep_branded/tests/ui/gc_cannot_escape_mutate.stderr b/oscars/src/collectors/mark_sweep_branded/tests/ui/gc_cannot_escape_mutate.stderr new file mode 100644 index 0000000..5c677b2 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/tests/ui/gc_cannot_escape_mutate.stderr @@ -0,0 +1,9 @@ +error: lifetime may not live long enough + --> src/collectors/mark_sweep_branded/tests/ui/gc_cannot_escape_mutate.rs:15:13 + | +14 | let _escaped = ctx.mutate(|cx| { + | --- return type of closure is oscars::collectors::mark_sweep_branded::Gc<'2, i32> + | | + | has type `&MutationContext<'_, '1>` +15 | cx.try_alloc(42i32).unwrap() // ERROR: Gc<'gc, i32> cannot escape mutate() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2` diff --git a/oscars/src/collectors/mark_sweep_branded/tests/ui/gc_cannot_store_outer_scopr.rs b/oscars/src/collectors/mark_sweep_branded/tests/ui/gc_cannot_store_outer_scopr.rs new file mode 100644 index 0000000..eb56a61 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/tests/ui/gc_cannot_store_outer_scopr.rs @@ -0,0 +1,28 @@ + +//! Compile-fail: `Gc<'gc, T>` cannot be stored in a location that outlives +//! the `mutate()` closure. +//! +//! Even if the caller does not return the `Gc` directly, storing it in an +//! outer `let` binding that outlives the closure is equally rejected: the +//! `'gc` lifetime of the `Gc` is shorter than the outer binding's scope. + +use oscars::collectors::mark_sweep_branded::{Gc, with_gc}; + +struct Holder<'a> { + gc: Gc<'a, i32>, +} + +fn main() { + with_gc(|ctx| { + let mut holder: Option> = None; + + ctx.mutate(|cx| { + let gc = cx.try_alloc(42i32).unwrap(); + // ERROR: `cx` (and therefore `gc`'s `'gc` lifetime) does not live + // long enough to be stored in `holder`. + holder = Some(Holder { gc }); + }); + + let _ = holder; + }); +} diff --git a/oscars/src/collectors/mark_sweep_branded/tests/ui/gc_cannot_store_outer_scopr.stderr b/oscars/src/collectors/mark_sweep_branded/tests/ui/gc_cannot_store_outer_scopr.stderr new file mode 100644 index 0000000..08e66af --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/tests/ui/gc_cannot_store_outer_scopr.stderr @@ -0,0 +1,11 @@ +error[E0521]: borrowed data escapes outside of closure + --> src/collectors/mark_sweep_branded/tests/ui/gc_cannot_store_outer_scopr.rs:23:13 + | +17 | let mut holder: Option> = None; + | ---------- `holder` declared here, outside of the closure body +18 | +19 | ctx.mutate(|cx| { + | -- `cx` is a reference that is only valid in the closure body +... +23 | holder = Some(Holder { gc }); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `cx` escapes the closure body here diff --git a/oscars/src/collectors/mark_sweep_branded/tests/ui/root_cross_context.rs b/oscars/src/collectors/mark_sweep_branded/tests/ui/root_cross_context.rs new file mode 100644 index 0000000..f4e3365 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/tests/ui/root_cross_context.rs @@ -0,0 +1,24 @@ + +//! Compile-fail: `Root<'id, T>` from one `with_gc` context cannot be used +//! inside a different `with_gc` context. +//! +//! `with_gc` has a `for<'id>` bound, so every call produces a fresh, unnamed +//! `'id` lifetime. `Root<'id1, T>` and `MutationContext<'id2, '_>` carry +//! distinct, non-unifiable `'id` variables, so the borrow checker rejects +//! `root.get(cx)` when `root` and `cx` come from different contexts. + +use oscars::collectors::mark_sweep_branded::with_gc; + +fn main() { + with_gc(|ctx1| { + with_gc(|ctx2| { + // root carries 'id of ctx1 + let root = ctx1.mutate(|cx| cx.root(cx.try_alloc(123i32).unwrap()).unwrap()); + + ctx2.mutate(|cx| { + // ERROR: `'id` of `root` (ctx1) != `'id` of `cx` (ctx2) + let _gc = root.get(cx); + }); + }); + }); +} diff --git a/oscars/src/collectors/mark_sweep_branded/tests/ui/root_cross_context.stderr b/oscars/src/collectors/mark_sweep_branded/tests/ui/root_cross_context.stderr new file mode 100644 index 0000000..e4dc38e --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/tests/ui/root_cross_context.stderr @@ -0,0 +1,19 @@ +error: lifetime may not live long enough + --> src/collectors/mark_sweep_branded/tests/ui/root_cross_context.rs:16:41 + | +13 | with_gc(|ctx1| { + | ---- lifetime `'2` appears in the type of `ctx1` +14 | with_gc(|ctx2| { + | ---- has type `GcContext<'1>` +15 | // root carries 'id of ctx1 +16 | let root = ctx1.mutate(|cx| cx.root(cx.try_alloc(123i32).unwrap()).unwrap()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2` + +error: lifetime may not live long enough + --> src/collectors/mark_sweep_branded/tests/ui/root_cross_context.rs:16:41 + | +13 | with_gc(|ctx1| { + | ---- has type `GcContext<'1>` +... +16 | let root = ctx1.mutate(|cx| cx.root(cx.try_alloc(123i32).unwrap()).unwrap()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'static` diff --git a/oscars/src/collectors/mark_sweep_branded/tests/ui_tests.rs b/oscars/src/collectors/mark_sweep_branded/tests/ui_tests.rs new file mode 100644 index 0000000..c17d420 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/tests/ui_tests.rs @@ -0,0 +1,6 @@ +#[test] +#[cfg(not(miri))] +fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("src/collectors/mark_sweep_branded/tests/ui/*.rs"); +} diff --git a/oscars/src/collectors/mark_sweep_branded/trace.rs b/oscars/src/collectors/mark_sweep_branded/trace.rs new file mode 100644 index 0000000..214a94a --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/trace.rs @@ -0,0 +1,312 @@ +//! Trace and Finalize traits for the lifetime branded GC + +use crate::{ + alloc::mempool3::PoolItem, + collectors::mark_sweep_branded::{gc::Gc, gc_box::GcColor}, +}; +use core::cell::{Cell, OnceCell}; +use core::marker::PhantomData; +use rust_alloc::borrow::{Cow, ToOwned}; +use rust_alloc::boxed::Box; +use rust_alloc::collections::{BTreeMap, BTreeSet, LinkedList, VecDeque}; +use rust_alloc::string::String; +use rust_alloc::vec::Vec; + +// Re-export the shared `Finalize` trait and standard library implementations. +pub use crate::collectors::common::Finalize; + +/// Trait for tracing garbage collected values. +/// +/// # Safety +/// +/// Use `Tracer::mark` for every reachable `Gc` pointer. +pub trait Trace { + /// Marks all `Gc` pointers reachable from `self`. + fn trace(&mut self, tracer: &mut Tracer); +} + +pub(crate) type TraceFn = unsafe fn(core::ptr::NonNull, &mut Tracer<'_>); + +/// Worklist-driven mark context for a stop-the-world collection cycle. +/// +/// Implements the classic tri-color marking invariant +/// (see `GcColor` for the per-object states): +/// +/// - `mark()` transitions `White → Gray` and enqueues the object. +/// - `drain()` dequeues each Gray entry; `gc_box::trace_value` transitions +/// it `Gray → Black` and recurses into its children. +/// - The sweep phase reclaims all remaining White objects and resets +/// Black → White, restoring the invariant for the next cycle. +/// +/// The worklist provides iterative traversal, preventing stack overflow on +/// deeply nested object graphs. +/// +/// The `'a` lifetime ties the tracer to the collection cycle, +/// preventing it from being stored or escaping the collector. +pub struct Tracer<'a> { + pub(crate) worklist: Vec<(core::ptr::NonNull, TraceFn)>, + pub(crate) _marker: PhantomData<&'a ()>, +} + +impl<'a> Tracer<'a> { + pub(crate) fn new() -> Self { + Self { + worklist: Vec::new(), + _marker: PhantomData, + } + } + + pub(crate) fn drain(&mut self) { + // Note: Using `pop()` processes the worklist in LIFO order (Depth-First Search). + // While correct, heap-allocated object graphs often exhibit better cache locality + // with Breadth-First Search. This could be evaluated with a `VecDeque` in the future. + while let Some((ptr, trace_fn)) = self.worklist.pop() { + // SAFETY: ptr is a live PoolItem> whose TraceFn was stored at allocation. + // pop() releases the borrow on self.worklist before the call, allowing mark() + // to push new entries re-entrantly. + unsafe { (trace_fn)(ptr, self) } + } + } + + /// Marks `gc` as reachable (White → Gray). + #[inline] + pub fn mark(&mut self, gc: &Gc<'_, T>) { + // SAFETY: `gc.ptr` is a valid `PoolItem>`. + unsafe { + let gc_box = &(*gc.ptr.as_ptr().as_ptr()).0; + if gc_box.color.get() == GcColor::White { + gc_box.color.set(GcColor::Gray); + self.worklist.push(( + gc.ptr.as_ptr().cast::(), + crate::collectors::mark_sweep_branded::gc_box::trace_value::, + )); + } + } + } + + /// Marks a raw allocation as reachable, returning `true` if newly marked. + /// + /// # Safety + /// + /// `ptr` must be a valid pointer to a `PoolItem>` managed by this collector. + #[inline] + pub(crate) fn mark_raw(&mut self, ptr: core::ptr::NonNull) -> bool { + let pool_item_ptr = + ptr.cast::>>(); + + unsafe { + let gc_box = &(*pool_item_ptr.as_ptr()).0; + if gc_box.color.get() == GcColor::White { + let trace_fn = gc_box.trace_fn; + gc_box.color.set(GcColor::Gray); + self.worklist.push((ptr, trace_fn)); + true + } else { + false + } + } + } +} + +impl Trace for &T { + #[inline] + fn trace(&mut self, _tracer: &mut Tracer) {} +} + +// primitive + std-lib Trace impls + +macro_rules! empty_trace { + ($($T:ty),* $(,)?) => { + $( + impl Trace for $T { + #[inline] + fn trace(&mut self, _tracer: &mut Tracer) {} + } + )* + }; +} + +empty_trace![ + (), + bool, + isize, + usize, + i8, + u8, + i16, + u16, + i32, + u32, + i64, + u64, + i128, + u128, + f32, + f64, + char, + String, + core::num::NonZeroIsize, + core::num::NonZeroUsize, + core::num::NonZeroI8, + core::num::NonZeroU8, + core::num::NonZeroI16, + core::num::NonZeroU16, + core::num::NonZeroI32, + core::num::NonZeroU32, + core::num::NonZeroI64, + core::num::NonZeroU64, + core::num::NonZeroI128, + core::num::NonZeroU128, +]; + +impl Trace for [T; N] { + fn trace(&mut self, tracer: &mut Tracer) { + for v in self.iter_mut() { + v.trace(tracer); + } + } +} + +impl Trace for Box { + fn trace(&mut self, tracer: &mut Tracer) { + (**self).trace(tracer); + } +} + +impl Trace for Option { + fn trace(&mut self, tracer: &mut Tracer) { + if let Some(v) = self { + v.trace(tracer); + } + } +} + +impl Trace for Result { + fn trace(&mut self, tracer: &mut Tracer) { + match self { + Ok(v) => v.trace(tracer), + Err(e) => e.trace(tracer), + } + } +} + +impl Trace for Vec { + fn trace(&mut self, tracer: &mut Tracer) { + for v in self.iter_mut() { + v.trace(tracer); + } + } +} + +impl Trace for VecDeque { + fn trace(&mut self, tracer: &mut Tracer) { + for v in self.iter_mut() { + v.trace(tracer); + } + } +} + +impl Trace for LinkedList { + fn trace(&mut self, tracer: &mut Tracer) { + for v in self.iter_mut() { + v.trace(tracer); + } + } +} + +impl Trace for PhantomData { + #[inline] + fn trace(&mut self, _tracer: &mut Tracer) {} +} + +// Cell> requires T: Copy to safely read the value via Cell::get(). +// For non-Copy types, use GcRefCell instead. +impl Trace for Cell> { + fn trace(&mut self, tracer: &mut Tracer) { + if let Some(mut v) = self.get() { + v.trace(tracer); + } + } +} + +impl Trace for OnceCell { + fn trace(&mut self, tracer: &mut Tracer) { + if let Some(v) = self.get_mut() { + v.trace(tracer); + } + } +} + +impl Trace for Cow<'static, T> +where + T::Owned: Trace, +{ + fn trace(&mut self, tracer: &mut Tracer) { + if let Cow::Owned(v) = self { + v.trace(tracer); + } + } +} + +impl Trace for (A,) { + #[inline] + fn trace(&mut self, tracer: &mut Tracer) { + self.0.trace(tracer); + } +} + +impl Trace for (A, B) { + #[inline] + fn trace(&mut self, tracer: &mut Tracer) { + self.0.trace(tracer); + self.1.trace(tracer); + } +} + +impl Trace for (A, B, C) { + #[inline] + fn trace(&mut self, tracer: &mut Tracer) { + self.0.trace(tracer); + self.1.trace(tracer); + self.2.trace(tracer); + } +} + +impl Trace for (A, B, C, D) { + #[inline] + fn trace(&mut self, tracer: &mut Tracer) { + self.0.trace(tracer); + self.1.trace(tracer); + self.2.trace(tracer); + self.3.trace(tracer); + } +} + +// Rc and Arc do not contain Gc pointers (they use reference counting, not GC). +// If you need to store Gc pointers inside Rc/Arc, wrap them in a GC-allocated +// struct instead. +impl Trace for rust_alloc::rc::Rc { + #[inline] + fn trace(&mut self, _tracer: &mut Tracer) {} +} + +impl Trace for rust_alloc::sync::Arc { + #[inline] + fn trace(&mut self, _tracer: &mut Tracer) {} +} + +impl Trace for BTreeMap { + fn trace(&mut self, tracer: &mut Tracer) { + for v in self.values_mut() { + v.trace(tracer); + } + } +} + +impl Trace for BTreeSet { + #[inline] + fn trace(&mut self, _tracer: &mut Tracer) { + // BTreeSet keys are immutable and cannot contain Gc pointers + // that need tracing (Gc requires &mut self to trace). + } +} diff --git a/oscars/src/collectors/mark_sweep_branded/weak.rs b/oscars/src/collectors/mark_sweep_branded/weak.rs new file mode 100644 index 0000000..bb60895 --- /dev/null +++ b/oscars/src/collectors/mark_sweep_branded/weak.rs @@ -0,0 +1,54 @@ +//! `WeakGc<'id, T>` for weak references. + +use crate::{ + alloc::mempool3::PoolPointer, + collectors::mark_sweep_branded::{ + gc::Gc, + gc_box::GcBox, + trace::{Finalize, Trace}, + }, +}; +use core::marker::PhantomData; + +/// A weak reference to a GC managed value +pub struct WeakGc<'id, T: Trace + ?Sized> { + pub(crate) ptr: PoolPointer<'static, GcBox>, + pub(crate) alloc_id: usize, + pub(crate) _marker: PhantomData<*mut &'id ()>, +} + +impl<'id, T: Trace> WeakGc<'id, T> { + /// Attempts to upgrade to a strong `Gc<'gc, T>`. + pub fn upgrade<'gc>( + &self, + _cx: &crate::collectors::mark_sweep_branded::MutationContext<'id, 'gc>, + ) -> Option> { + // SAFETY: `_cx` proves the `Collector` is alive. + // `alloc_id` confirms the allocation is still valid. + // The allocator does not unmap memory, so reading a recycled block's `alloc_id` is safe + let is_valid = unsafe { (*self.ptr.as_ptr().as_ptr()).0.alloc_id == self.alloc_id }; + + if is_valid { + Some(Gc { + ptr: self.ptr, + _marker: PhantomData, + }) + } else { + None + } + } +} + +impl<'id, T: Trace + ?Sized> Clone for WeakGc<'id, T> { + fn clone(&self) -> Self { + *self + } +} + +impl<'id, T: Trace + ?Sized> Copy for WeakGc<'id, T> {} + +impl<'id, T: Trace> Finalize for WeakGc<'id, T> {} +impl<'id, T: Trace> Trace for WeakGc<'id, T> { + // Weak references do not mark their target, upgrade() returning None after collection is the intended behaviour. + fn trace(&mut self, _tracer: &mut crate::collectors::mark_sweep_branded::trace::Tracer) {} +} diff --git a/oscars/src/collectors/mod.rs b/oscars/src/collectors/mod.rs index 0e53749..6172a2b 100644 --- a/oscars/src/collectors/mod.rs +++ b/oscars/src/collectors/mod.rs @@ -3,3 +3,6 @@ pub mod common; pub mod mark_sweep; pub mod mark_sweep_arena2; + +#[cfg(feature = "mark_sweep_branded")] +pub mod mark_sweep_branded; diff --git a/oscars_derive/src/mark_sweep_branded.rs b/oscars_derive/src/mark_sweep_branded.rs new file mode 100644 index 0000000..235ccfb --- /dev/null +++ b/oscars_derive/src/mark_sweep_branded.rs @@ -0,0 +1,45 @@ +use quote::quote; +use synstructure::{AddBounds, Structure, decl_derive}; + +decl_derive! { + [Trace, attributes(oscars_gc, unsafe_ignore_trace)] => + /// Derive the `Trace` trait for mark_sweep_branded collector. + derive_trace +} + +fn derive_trace(mut s: Structure<'_>) -> proc_macro2::TokenStream { + s.filter(|bi| { + !bi.ast() + .attrs + .iter() + .any(|attr| attr.path().is_ident("unsafe_ignore_trace")) + }); + + let trace_body = s.each(|bi| { + quote!(::oscars::collectors::mark_sweep_branded::Trace::trace(#bi, color)) + }); + + s.add_bounds(AddBounds::Fields); + s.bound_impl( + quote!(::oscars::collectors::mark_sweep_branded::Trace), + quote! { + #[inline] + fn trace(&self, color: &::oscars::collectors::mark_sweep_branded::TraceColor) { + match *self { #trace_body } + } + }, + ) +} + +decl_derive! { + [Finalize] => + /// Derive the `Finalize` trait for mark_sweep_branded collector + derive_finalize +} + +fn derive_finalize(s: Structure<'_>) -> proc_macro2::TokenStream { + s.unbound_impl( + quote!(::oscars::collectors::mark_sweep_branded::Finalize), + quote!(), + ) +}