aiscript_arena/
arena.rs

1use alloc::boxed::Box;
2use core::{f64, marker::PhantomData};
3
4use crate::{
5    Collect,
6    context::{Context, EarlyStop, Finalization, Mutation, Phase},
7    metrics::Metrics,
8};
9
10/// A trait that produces a [`Collect`]-able type for the given lifetime. This is used to produce
11/// the root [`Collect`] instance in an [`Arena`].
12///
13/// In order to use an implementation of this trait in an [`Arena`], it must implement
14/// `Rootable<'a>` for *any* possible `'a`. This is necessary so that the `Root` types can be
15/// branded by the unique, invariant lifetimes that makes an `Arena` sound.
16pub trait Rootable<'a> {
17    type Root: ?Sized + 'a;
18}
19
20/// A marker type used by the `Rootable!` macro instead of a bare trait object.
21///
22/// Prevents having to include extra ?Sized bounds on every `for<'a> Rootable<'a>`.
23#[doc(hidden)]
24pub struct __DynRootable<T: ?Sized>(PhantomData<T>);
25
26impl<'a, T: ?Sized + Rootable<'a>> Rootable<'a> for __DynRootable<T> {
27    type Root = <T as Rootable<'a>>::Root;
28}
29
30/// A convenience macro for quickly creating a type that implements `Rootable`.
31///
32/// The macro takes a single argument, which should be a generic type with elided lifetimes.
33/// When used as a root object, every instance of the elided lifetime will be replaced with
34/// the branding lifetime.
35///
36/// ```
37/// # use aiscript_arena::{Arena, Collect, Gc, Rootable};
38/// #
39/// # fn main() {
40/// #[derive(Collect)]
41/// #[collect(no_drop)]
42/// struct MyRoot<'gc> {
43///     ptr: Gc<'gc, i32>,
44/// }
45///
46/// type MyArena = Arena<Rootable![MyRoot<'_>]>;
47///
48/// // If desired, the branding lifetime can also be explicitely named:
49/// type MyArena2 = Arena<Rootable!['gc => MyRoot<'gc>]>;
50/// # }
51/// ```
52///
53/// The macro can also be used to create implementations of `Rootable` that use other generic
54/// parameters, though in complex cases it may be better to implement `Rootable` directly.
55///
56/// ```
57/// # use aiscript_arena::{Arena, Collect, Gc, Rootable};
58/// #
59/// # fn main() {
60/// #[derive(Collect)]
61/// #[collect(no_drop)]
62/// struct MyGenericRoot<'gc, T> {
63///     ptr: Gc<'gc, T>,
64/// }
65///
66/// type MyGenericArena<T> = Arena<Rootable![MyGenericRoot<'_, T>]>;
67/// # }
68/// ```
69#[macro_export]
70macro_rules! Rootable {
71    ($gc:lifetime => $root:ty) => {
72        // Instead of generating an impl of `Rootable`, we use a trait object. Thus, we avoid the
73        // need to generate a new type for each invocation of this macro.
74        $crate::__DynRootable::<dyn for<$gc> $crate::Rootable<$gc, Root = $root>>
75    };
76    ($root:ty) => {
77        $crate::Rootable!['__gc => $crate::__unelide_lifetimes!('__gc; $root)]
78    };
79}
80
81/// A helper type alias for a `Rootable::Root` for a specific lifetime.
82pub type Root<'a, R> = <R as Rootable<'a>>::Root;
83
84#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
85pub enum CollectionPhase {
86    /// The arena is done with a collection cycle and is waiting to be restarted.
87    Sleeping,
88    /// The arena is currently tracing objects from the root to determine reachability.
89    Marking,
90    /// The arena has finished tracing, all reachable objects are marked. This may transition
91    /// back to `Marking` if write barriers occur.
92    Marked,
93    /// The arena has determined a set of unreachable objects and has started freeing them. At this
94    /// point, marking is no longer taking place so the root may have reachable, unmarked pointers.
95    Sweeping,
96}
97
98/// A generic, garbage collected arena.
99///
100/// Garbage collected arenas allow for isolated sets of garbage collected objects with zero-overhead
101/// garbage collected pointers. It provides incremental mark and sweep garbage collection which
102/// must be manually triggered outside the `mutate` method, and works best when units of work inside
103/// `mutate` can be kept relatively small. It is designed primarily to be a garbage collector for
104/// scripting language runtimes.
105///
106/// The arena API is able to provide extremely cheap Gc pointers because it is based around
107/// "generativity". During construction and access, the root type is branded by a unique, invariant
108/// lifetime `'gc` which ensures that `Gc` pointers must be contained inside the root object
109/// hierarchy and cannot escape the arena callbacks or be smuggled inside another arena. This way,
110/// the arena can be sure that during mutation, all `Gc` pointers come from the arena we expect
111/// them to come from, and that they're all either reachable from root or have been allocated during
112/// the current `mutate` call. When not inside the `mutate` callback, the arena knows that all `Gc`
113/// pointers must be either reachable from root or they are unreachable and safe to collect. In
114/// this way, incremental garbage collection can be achieved (assuming "sufficiently small" calls
115/// to `mutate`) that is both extremely safe and zero overhead vs what you would write in C with raw
116/// pointers and manually ensuring that invariants are held.
117pub struct Arena<R>
118where
119    R: for<'a> Rootable<'a>,
120{
121    context: Box<Context>,
122    root: Root<'static, R>,
123}
124
125impl<R> Arena<R>
126where
127    R: for<'a> Rootable<'a>,
128    for<'a> Root<'a, R>: Sized,
129{
130    /// Create a new arena with the given garbage collector tuning parameters. You must provide a
131    /// closure that accepts a `&Mutation<'gc>` and returns the appropriate root.
132    pub fn new<F>(f: F) -> Arena<R>
133    where
134        F: for<'gc> FnOnce(&'gc Mutation<'gc>) -> Root<'gc, R>,
135    {
136        unsafe {
137            let context = Box::new(Context::new());
138            // Note - we cast the `&Mutation` to a `'static` lifetime here,
139            // instead of transmuting the root type returned by `f`. Transmuting the root
140            // type is allowed in nightly versions of rust
141            // (see https://github.com/rust-lang/rust/pull/101520#issuecomment-1252016235)
142            // but is not yet stable. Casting the `&Mutation` is completely invisible
143            // to the callback `f` (since it needs to handle an arbitrary lifetime),
144            // and lets us stay compatible with older versions of Rust
145            let mc: &'static Mutation<'_> = &*(context.mutation_context() as *const _);
146            let root: Root<'static, R> = f(mc);
147            Arena { context, root }
148        }
149    }
150
151    /// Similar to `new`, but allows for constructor that can fail.
152    pub fn try_new<F, E>(f: F) -> Result<Arena<R>, E>
153    where
154        F: for<'gc> FnOnce(&'gc Mutation<'gc>) -> Result<Root<'gc, R>, E>,
155    {
156        unsafe {
157            let context = Box::new(Context::new());
158            let mc: &'static Mutation<'_> = &*(context.mutation_context() as *const _);
159            let root: Root<'static, R> = f(mc)?;
160            Ok(Arena { context, root })
161        }
162    }
163
164    #[inline]
165    pub fn map_root<R2>(
166        self,
167        f: impl for<'gc> FnOnce(&'gc Mutation<'gc>, Root<'gc, R>) -> Root<'gc, R2>,
168    ) -> Arena<R2>
169    where
170        R2: for<'a> Rootable<'a>,
171        for<'a> Root<'a, R2>: Sized,
172    {
173        self.context.root_barrier();
174        let new_root: Root<'static, R2> = unsafe {
175            let mc: &'static Mutation<'_> = &*(self.context.mutation_context() as *const _);
176            f(mc, self.root)
177        };
178        Arena {
179            context: self.context,
180            root: new_root,
181        }
182    }
183
184    #[inline]
185    pub fn try_map_root<R2, E>(
186        self,
187        f: impl for<'gc> FnOnce(&'gc Mutation<'gc>, Root<'gc, R>) -> Result<Root<'gc, R2>, E>,
188    ) -> Result<Arena<R2>, E>
189    where
190        R2: for<'a> Rootable<'a>,
191        for<'a> Root<'a, R2>: Sized,
192    {
193        self.context.root_barrier();
194        let new_root: Root<'static, R2> = unsafe {
195            let mc: &'static Mutation<'_> = &*(self.context.mutation_context() as *const _);
196            f(mc, self.root)?
197        };
198        Ok(Arena {
199            context: self.context,
200            root: new_root,
201        })
202    }
203}
204
205impl<R> Arena<R>
206where
207    R: for<'a> Rootable<'a>,
208{
209    /// The primary means of interacting with a garbage collected arena. Accepts a callback which
210    /// receives a `&Mutation<'gc>` and a reference to the root, and can return any non garbage
211    /// collected value. The callback may "mutate" any part of the object graph during this call,
212    /// but no garbage collection will take place during this method.
213    #[inline]
214    pub fn mutate<F, T>(&self, f: F) -> T
215    where
216        F: for<'gc> FnOnce(&'gc Mutation<'gc>, &'gc Root<'gc, R>) -> T,
217    {
218        unsafe {
219            let mc: &'static Mutation<'_> = &*(self.context.mutation_context() as *const _);
220            let root: &'static Root<'_, R> = &*(&self.root as *const _);
221            f(mc, root)
222        }
223    }
224
225    /// An alternative version of [`Arena::mutate`] which allows mutating the root set, at the
226    /// cost of an extra write barrier.
227    #[inline]
228    pub fn mutate_root<F, T>(&mut self, f: F) -> T
229    where
230        F: for<'gc> FnOnce(&'gc Mutation<'gc>, &'gc mut Root<'gc, R>) -> T,
231    {
232        self.context.root_barrier();
233        unsafe {
234            let mc: &'static Mutation<'_> = &*(self.context.mutation_context() as *const _);
235            let root: &'static mut Root<'_, R> = &mut *(&mut self.root as *mut _);
236            f(mc, root)
237        }
238    }
239
240    #[inline]
241    pub fn metrics(&self) -> &Metrics {
242        self.context.metrics()
243    }
244
245    #[inline]
246    pub fn collection_phase(&self) -> CollectionPhase {
247        match self.context.phase() {
248            Phase::Mark => {
249                if self.context.gray_remaining() {
250                    CollectionPhase::Marking
251                } else {
252                    CollectionPhase::Marked
253                }
254            }
255            Phase::Sweep => CollectionPhase::Sweeping,
256            Phase::Sleep => CollectionPhase::Sleeping,
257            Phase::Drop => unreachable!(),
258        }
259    }
260}
261
262impl<R> Arena<R>
263where
264    R: for<'a> Rootable<'a>,
265    for<'a> Root<'a, R>: Collect,
266{
267    /// Run incremental garbage collection until the allocation debt is <= 0.0.
268    ///
269    /// There is no minimum unit of work enforced here, so it may be faster to only call this method
270    /// when the allocation debt is above some threshold.
271    ///
272    /// This method will always return at least once when collection enters the `Sleeping` phase,
273    /// i.e. it will never transition from the `Sweeping` phase to the `Marking` phase without
274    /// returning in-between.
275    #[inline]
276    pub fn collect_debt(&mut self) {
277        unsafe {
278            self.context.do_collection(&self.root, 0.0, None);
279        }
280    }
281
282    /// Run only the *marking* part of incremental garbage collection until allocation debt is
283    /// <= 0.0.
284    ///
285    /// This does *not* transition collection past the `Marked` phase. Does nothing if the
286    /// collection phase is `Marked` or `Sweeping`, otherwise acts like `Arena::collect_debt`.
287    #[inline]
288    pub fn mark_debt(&mut self) -> Option<MarkedArena<'_, R>> {
289        if matches!(self.context.phase(), Phase::Mark | Phase::Sleep) {
290            unsafe {
291                self.context
292                    .do_collection(&self.root, 0.0, Some(EarlyStop::BeforeSweep));
293            }
294        }
295
296        if self.context.phase() == Phase::Mark && !self.context.gray_remaining() {
297            Some(MarkedArena(self))
298        } else {
299            None
300        }
301    }
302
303    /// Run the current garbage collection cycle to completion, stopping once garbage collection
304    /// has restarted in the sleep phase. If the collector is currently in the sleep phase, this
305    /// restarts the collection and performs a full collection before transitioning back to the
306    /// sleep phase.
307    #[inline]
308    pub fn collect_all(&mut self) {
309        unsafe {
310            self.context
311                .do_collection(&self.root, f64::NEG_INFINITY, None);
312        }
313    }
314
315    /// Runs all of the remaining *marking* part of the current garbage collection cycle.
316    ///
317    /// Similarly to `Arena::mark_debt`, this does not transition collection  past the `Marked`
318    /// phase, and does nothing if the collector is currently in the `Marked` phase or the
319    /// `Sweeping` phase.
320    #[inline]
321    pub fn mark_all(&mut self) -> Option<MarkedArena<'_, R>> {
322        if matches!(self.context.phase(), Phase::Mark | Phase::Sleep) {
323            unsafe {
324                self.context.do_collection(
325                    &self.root,
326                    f64::NEG_INFINITY,
327                    Some(EarlyStop::BeforeSweep),
328                );
329            }
330        }
331
332        if self.context.phase() == Phase::Mark && !self.context.gray_remaining() {
333            Some(MarkedArena(self))
334        } else {
335            None
336        }
337    }
338}
339
340pub struct MarkedArena<'a, R: for<'b> Rootable<'b>>(&'a mut Arena<R>);
341
342impl<R> MarkedArena<'_, R>
343where
344    R: for<'b> Rootable<'b>,
345    for<'b> Root<'b, R>: Collect,
346{
347    /// Examine the state of a fully marked arena.
348    ///
349    /// Allows you to determine whether `GcWeak` pointers are "dead" (aka, soon-to-be-dropped) and
350    /// potentially resurrect them for this cycle.
351    ///
352    /// Note that the arena is guaranteed to be *fully marked* only at the *beginning* of this
353    /// callback, any mutation that resurrects a pointer or triggers a write barrier can immediately
354    /// invalidate this.
355    #[inline]
356    pub fn finalize<F, T>(self, f: F) -> T
357    where
358        F: for<'gc> FnOnce(&'gc Finalization<'gc>, &'gc Root<'gc, R>) -> T,
359    {
360        unsafe {
361            let mc: &'static Finalization<'_> =
362                &*(self.0.context.finalization_context() as *const _);
363            let root: &'static Root<'_, R> = &*(&self.0.root as *const _);
364            f(mc, root)
365        }
366    }
367
368    /// Immediately transition the arena out of [`CollectionPhase::Marked`] to
369    /// [`CollectionPhase::Sweeping`].
370    #[inline]
371    pub fn start_sweeping(self) {
372        unsafe {
373            self.0.context.do_collection(
374                &self.0.root,
375                f64::NEG_INFINITY,
376                Some(EarlyStop::AfterSweep),
377            );
378        }
379        assert_eq!(self.0.context.phase(), Phase::Sweep);
380    }
381}
382
383/// Create a temporary arena without a root object and perform the given operation on it.
384///
385/// No garbage collection will be done until the very end of the call, at which point all
386/// allocations will be collected.
387///
388/// This is a convenience function that makes it a little easier to quickly test code that uses
389/// `gc-arena`, it is not very useful on its own.
390pub fn rootless_mutate<F, R>(f: F) -> R
391where
392    F: for<'gc> FnOnce(&'gc Mutation<'gc>) -> R,
393{
394    unsafe {
395        let context = Context::new();
396        f(context.mutation_context())
397    }
398}