Skip to main content

gc_lite/
scope.rs

1use {
2    core::ptr::NonNull,
3    std::{cell::UnsafeCell, marker::PhantomData, num::NonZeroU32},
4};
5
6#[cfg(debug_assertions)]
7use std::cell::Cell;
8
9use crate::{
10    heap::GcHeap,
11    helpers::GcError,
12    node::{Gc, GcHead, GcNode, GcNodeFlag},
13    partition::GcPartitionId,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[repr(transparent)]
18pub struct GcScopeStackId(u16);
19
20impl std::fmt::Display for GcScopeStackId {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        write!(f, "{}", self.0)
23    }
24}
25
26pub(crate) struct ScopeStack {
27    /// sequence of scopes
28    pub(crate) list: Vec<GcScopeState<'static>>,
29    /// which partition this stack is associated with, None means the stack is not used
30    pub(crate) partition: Option<GcPartitionId>,
31}
32
33impl ScopeStack {
34    pub(super) fn new(partition: Option<GcPartitionId>) -> Self {
35        Self {
36            list: Vec::with_capacity(8),
37            partition,
38        }
39    }
40}
41
42/// The GC scope state
43#[derive(Debug)]
44pub struct GcScopeState<'s> {
45    heap: NonNull<GcHeap>,
46    partition_id: GcPartitionId,
47    stack_id: GcScopeStackId,
48    depth: NonZeroU32,
49    cache: UnsafeCell<Option<Vec<NonNull<GcHead>>>>,
50    #[cfg(debug_assertions)]
51    borrow_flag: Cell<bool>,
52    _marker: PhantomData<&'s ()>,
53}
54
55impl<'s> Drop for GcScopeState<'s> {
56    fn drop(&mut self) {
57        self.clear();
58    }
59}
60
61impl<'s> GcScopeState<'s> {
62    pub fn new(
63        heap: &'s mut GcHeap,
64        stack_id: GcScopeStackId,
65        partition_id: GcPartitionId,
66    ) -> Self {
67        debug_assert!(
68            (stack_id.0 as usize) < heap.scope_stacks.len(),
69            "GcScopeState stack_id {} out of bounds (max {})",
70            stack_id.0,
71            heap.scope_stacks.len(),
72        );
73
74        let depth = heap.scope_max_depth(stack_id) + 1;
75        Self {
76            heap: NonNull::from_ref(heap),
77            stack_id,
78            partition_id,
79            depth: NonZeroU32::new(depth).unwrap(),
80            cache: UnsafeCell::new(None),
81            #[cfg(debug_assertions)]
82            borrow_flag: Cell::new(false),
83            _marker: PhantomData,
84        }
85    }
86
87    #[inline(always)]
88    pub fn partition_id(&self) -> GcPartitionId {
89        self.partition_id
90    }
91
92    #[inline(always)]
93    pub fn stack_id(&self) -> GcScopeStackId {
94        self.stack_id
95    }
96
97    #[inline(always)]
98    pub fn heap(&self) -> &GcHeap {
99        unsafe { self.heap.as_ref() }
100    }
101
102    #[inline(always)]
103    pub fn heap_mut(&mut self) -> &mut GcHeap {
104        unsafe { self.heap.as_mut() }
105    }
106
107    #[inline(always)]
108    pub fn depth(&self) -> u32 {
109        self.depth.get()
110    }
111
112    #[inline(always)]
113    pub fn count(&self) -> usize {
114        unsafe { (*self.cache.get()).as_ref().map_or(0, |c| c.len()) }
115    }
116
117    /// get parent scope, and its level.
118    pub fn parent(&self) -> Option<(&GcScopeState<'_>, u32)> {
119        let d = self.depth();
120        if d > 1 {
121            let parent_index = d - 2;
122            self.heap().scope_stacks[self.stack_id.0 as usize]
123                .list
124                .get(parent_index as usize)
125                .map(|s| (s, d - 1))
126        } else {
127            None
128        }
129    }
130
131    /// alloc a local node in scope.
132    pub fn alloc_local<T: GcNode>(&self, payload: T) -> Result<Gc<'s, T>, (GcError, T)> {
133        let r = unsafe { (*self.heap.as_ptr()).alloc_raw(self.partition_id, payload)? };
134        self.add_node(r.head_ptr);
135        // SAFETY: The node is now protected by this scope's LOCAL flag,
136        // and 's is tied to the GcScopeState's lifetime (which outlives
137        // this call and is typically tied to a &mut GcHeap borrow,
138        // preventing GC collection).
139        Ok(unsafe { Gc::from_raw(r) })
140    }
141
142    fn add_node(&self, mut node: NonNull<GcHead>) {
143        unsafe {
144            debug_assert!(
145                !node.as_ref().is_root_or_local(),
146                "add_node: node is already root or local",
147            );
148
149            node.as_mut().insert_flag(GcNodeFlag::LOCAL);
150
151            if let Some(par) = (*self.heap.as_ptr()).partition_mut(self.partition_id)
152                && par.is_marking()
153            {
154                par.add_gray_node(node);
155            }
156        }
157
158        #[cfg(debug_assertions)]
159        debug_assert!(!self.borrow_flag.get(), "GcScopeState: reentrant borrow");
160        #[cfg(debug_assertions)]
161        self.borrow_flag.set(true);
162
163        unsafe {
164            let cache = &mut *self.cache.get();
165            if cache.is_none() {
166                *cache = Some(Vec::with_capacity(1));
167            }
168            cache.as_mut().unwrap().push(node);
169        }
170
171        #[cfg(debug_assertions)]
172        self.borrow_flag.set(false);
173    }
174
175    // if node is neither root, nor local, then add it to `self` scope
176    pub fn add_non_local(&self, node: NonNull<GcHead>) -> bool {
177        if unsafe { node.as_ref().is_root_or_local() } {
178            false
179        } else {
180            self.add_node(node);
181            true
182        }
183    }
184
185    /// Remove a LOCAL node from this scope's cache and clear its LOCAL flag.
186    ///
187    /// This is the inverse of `add_node`: it undoes the LOCAL protection
188    /// that was granted when the node was allocated in this scope.
189    /// After this call, the node is no longer protected by this scope
190    /// and will be traced by GC solely through other GC references.
191    ///
192    /// Returns `true` if the node was found and removed from this scope's cache.
193    /// Returns `false` if the node was not in this scope's cache (e.g., it was
194    /// already promoted, or belongs to a different scope).
195    ///
196    /// # Safety
197    ///
198    /// The caller must ensure the node is still alive and valid.
199    /// This method does NOT check whether the node is the current promote node;
200    /// the caller (e.g., `ScopedContext::throw`) is responsible for ensuring
201    /// the node is not the promote target.
202    pub fn remove_node(&self, mut node: NonNull<GcHead>) -> bool {
203        #[cfg(debug_assertions)]
204        unsafe {
205            node.as_ref().debug_assert_node_valid_simple();
206        }
207        let cache_opt = unsafe { &mut *self.cache.get() };
208        if let Some(cache) = cache_opt.as_mut()
209            && let Some(pos) = cache.iter().position(|&n| n == node)
210        {
211            cache.swap_remove(pos);
212            unsafe {
213                node.as_mut().remove_flag(GcNodeFlag::LOCAL);
214            }
215            true
216        } else {
217            false
218        }
219    }
220
221    pub fn contains(&self, node: NonNull<GcHead>) -> bool {
222        unsafe {
223            (*self.cache.get())
224                .as_ref()
225                .map_or(false, |c| c.contains(&node))
226        }
227    }
228
229    /// Move a node from this scope's cache to the target scope's cache.
230    ///
231    /// The node retains its LOCAL flag — it is simply transferred from one
232    /// scope's protection to another's. This is used when a return value
233    /// needs to be moved from a child scope to its parent scope before
234    /// the child scope is destroyed.
235    ///
236    /// Returns `true` if the node was found in this scope and moved.
237    /// Returns `false` if the node was not in this scope's cache (e.g., it
238    /// is a root node, or already belongs to another scope).
239    pub fn promote_node_to(&self, node: NonNull<GcHead>, target: &GcScopeState<'_>) -> bool {
240        let cache_opt = unsafe { &mut *self.cache.get() };
241
242        if let Some(cache) = cache_opt.as_mut()
243            && let Some(pos) = cache.iter().position(|&n| n == node)
244        {
245            debug_assert!(
246                unsafe { node.as_ref().is_local() },
247                "promote_node_to: node is not LOCAL",
248            );
249            cache.swap_remove(pos);
250            let target_cache = unsafe { &mut *target.cache.get() };
251            if target_cache.is_none() {
252                *target_cache = Some(Vec::with_capacity(1));
253            }
254            target_cache.as_mut().unwrap().push(node);
255            true
256        } else {
257            // Node not found in this scope's cache. This is safe — the node
258            // must be reachable through some other GC root or LOCAL node,
259            // otherwise it would have been collected. Common scenarios:
260            //
261            //   - ROOT node: never in any scope cache, no protection needed.
262            //   - LOCAL in an ancestor scope: already protected by that scope.
263            //   - flags=0 (was LOCAL, scope was cleared): the node is still
264            //     alive because it is referenced by another GC root/LOCAL
265            //     (e.g., a Bytecode constant pool, an object property, an
266            //     array element, a closure variable). It does not need scope
267            //     protection — it is kept alive by its referrer.
268            false
269        }
270    }
271
272    // clear and unprotect locals nodes in this scope, remote LOCAL flag of each
273    pub fn clear(&self) {
274        let cache_opt = unsafe { &mut *self.cache.get() };
275        if let Some(lst) = cache_opt.take() {
276            for mut n in lst {
277                unsafe {
278                    n.as_mut().remove_flag(crate::node::GcNodeFlag::LOCAL);
279                }
280            }
281        }
282    }
283}
284
285/// Handle to a GC scope, used to:
286///
287/// 1. reference the scope state
288/// 2. manage lifetime of the scope, pop the scope when the handle is dropped
289#[derive(Debug)]
290pub struct GcScope<'s> {
291    heap: NonNull<GcHeap>,
292    stack_id: GcScopeStackId,
293    index: u32,
294    _marker: PhantomData<&'s ()>,
295}
296
297impl<'s> Drop for GcScope<'s> {
298    fn drop(&mut self) {
299        unsafe {
300            let heap = self.heap.as_mut();
301            let stack = &heap.scope_stacks[self.stack_id.0 as usize];
302
303            debug_assert!(
304                !stack.list.is_empty(),
305                "GcScope dropped but scope stack {} is empty",
306                self.stack_id.0,
307            );
308            debug_assert_eq!(
309                stack.list.len() as u32 - 1,
310                self.index as u32,
311                "GcScope dropped out of LIFO order: scope stack {} has {} entries, expected top index {}",
312                self.stack_id.0,
313                stack.list.len(),
314                self.index,
315            );
316
317            heap.pop_scope(self.stack_id);
318        }
319    }
320}
321
322impl<'s> std::ops::Deref for GcScope<'s> {
323    type Target = GcScopeState<'s>;
324
325    #[inline(always)]
326    fn deref(&self) -> &Self::Target {
327        unsafe {
328            let stack = &self.heap.as_ref().scope_stacks[self.stack_id.0 as usize];
329            debug_assert!(
330                (self.index as usize) < stack.list.len(),
331                "GcScope index {} out of bounds for stack {} (len {})",
332                self.index,
333                self.stack_id.0,
334                stack.list.len(),
335            );
336
337            std::mem::transmute::<&GcScopeState<'_>, &GcScopeState<'s>>(
338                stack.list.get(self.index as usize).unwrap_unchecked(),
339            )
340        }
341    }
342}
343
344impl<'s> std::ops::DerefMut for GcScope<'s> {
345    #[inline(always)]
346    fn deref_mut(&mut self) -> &mut Self::Target {
347        unsafe {
348            let stack = &mut self.heap.as_mut().scope_stacks[self.stack_id.0 as usize];
349            debug_assert!(
350                (self.index as usize) < stack.list.len(),
351                "GcScope index {} out of bounds for stack {} (len {})",
352                self.index,
353                self.stack_id.0,
354                stack.list.len(),
355            );
356
357            std::mem::transmute::<&mut GcScopeState<'_>, &mut GcScopeState<'s>>(
358                stack.list.get_mut(self.index as usize).unwrap_unchecked(),
359            )
360        }
361    }
362}
363
364impl GcHeap {
365    /// find an idle scope stack, or create a new if none available
366    pub fn acquire_scope_stack(&mut self, par: GcPartitionId) -> GcScopeStackId {
367        for (id, stack) in self.scope_stacks.iter_mut().enumerate() {
368            if stack.partition.is_none() {
369                debug_assert!(
370                    stack.list.is_empty(),
371                    "acquire_scope_stack: idle stack {} has non-empty scope list",
372                    id,
373                );
374                stack.partition = Some(par);
375                return GcScopeStackId(id as u16);
376            }
377        }
378
379        let id = u16::try_from(self.scope_stacks.len()).expect("too many scope stacks");
380        self.scope_stacks.push(ScopeStack::new(Some(par)));
381        GcScopeStackId(id)
382    }
383
384    pub fn release_scope_stack(&mut self, stack_id: GcScopeStackId) {
385        let stack = &mut self.scope_stacks[stack_id.0 as usize];
386        debug_assert!(
387            stack.list.is_empty(),
388            "release_scope_stack: stack {} has {} scopes still active",
389            stack_id.0,
390            stack.list.len(),
391        );
392        stack.partition.take();
393    }
394
395    /// get max depth of a scope stack
396    #[inline(always)]
397    pub fn scope_max_depth(&self, stack_id: GcScopeStackId) -> u32 {
398        self.scope_stacks[stack_id.0 as usize].list.len() as _
399    }
400
401    /// get scope state specified by (stack_id, depth).
402    /// where `depth` is 1-based (1 means index #0)
403    pub fn scope(&self, stack_id: GcScopeStackId, depth: u32) -> Option<&GcScopeState<'_>> {
404        if depth > 0 {
405            self.scope_stacks[stack_id.0 as usize]
406                .list
407                .get(depth as usize - 1)
408        } else {
409            None
410        }
411    }
412
413    /// Get scope state by 0-based index without bounds check.
414    ///
415    /// # Safety
416    ///
417    /// Caller must ensure `index < scope_max_depth(stack_id)`.
418    #[inline(always)]
419    pub unsafe fn scope_unchecked(
420        &self,
421        stack_id: GcScopeStackId,
422        index: u32,
423    ) -> &GcScopeState<'_> {
424        debug_assert!(
425            (index as usize) < self.scope_stacks[stack_id.0 as usize].list.len(),
426            "scope_unchecked: index {index} out of bounds for stack {} (len {})",
427            stack_id.0,
428            self.scope_stacks[stack_id.0 as usize].list.len(),
429        );
430        // SAFETY: caller ensures index < scope_max_depth(stack_id)
431        unsafe {
432            std::mem::transmute::<&GcScopeState<'static>, &GcScopeState<'_>>(
433                self.scope_stacks[stack_id.0 as usize]
434                    .list
435                    .get_unchecked(index as usize),
436            )
437        }
438    }
439
440    /// pop the last scope from the stack
441    #[inline(always)]
442    fn pop_scope(&mut self, stack_id: GcScopeStackId) -> Option<GcScopeState<'_>> {
443        self.scope_stacks[stack_id.0 as usize].list.pop()
444    }
445
446    /// push a new scope on the stack, returns new scope's handle.
447    /// the scope will be automatically popped when this handle is dropped.
448    ///
449    /// # Panics
450    ///
451    /// Panics if an earlier handle is dropped while a later handle is still alive
452    /// (LIFO order violation is enforced at runtime).
453    pub fn new_scope<'s>(&'s mut self, stack_id: GcScopeStackId) -> GcScope<'s> {
454        let heap = NonNull::from_ref(self);
455        let stack = &mut self.scope_stacks[stack_id.0 as usize];
456        debug_assert!(
457            stack.partition.is_some(),
458            "scope stack {stack_id:?} is not acquired"
459        );
460
461        let list_len = stack.list.len();
462        if list_len >= u32::MAX as usize {
463            panic!("scope stack overflow: depth {} exceeds u32::MAX", list_len);
464        }
465        let depth = list_len as u32 + 1;
466        let state = GcScopeState {
467            heap,
468            stack_id,
469            partition_id: stack.partition.unwrap(),
470            depth: NonZeroU32::new(depth).unwrap(),
471            cache: UnsafeCell::new(None),
472            #[cfg(debug_assertions)]
473            borrow_flag: Cell::new(false),
474            _marker: PhantomData,
475        };
476
477        stack.list.push(unsafe {
478            // SAFETY: It is safe because the GcHeap owns the GcScope, and we ensure that
479            // the GcScope does not outlive the GcHeap.
480            std::mem::transmute::<GcScopeState<'_>, GcScopeState<'static>>(state)
481        });
482
483        GcScope {
484            heap,
485            stack_id,
486            index: (depth - 1) as u32,
487            _marker: PhantomData,
488        }
489    }
490
491    /// get last (top-most) scope from the stack
492    #[inline]
493    pub fn current_scope(&self, stack_id: GcScopeStackId) -> Option<&GcScopeState<'_>> {
494        let s = self.scope_stacks[stack_id.0 as usize].list.last();
495        unsafe {
496            // SAFETY: It is safe because the GcHeap owns the GcScope, and we ensure that
497            // the GcScope does not outlive the GcHeap.
498            std::mem::transmute::<Option<&GcScopeState<'static>>, Option<&GcScopeState<'_>>>(s)
499        }
500    }
501
502    /// run closure with last (top-most) scope from the stack
503    #[inline]
504    pub fn with_current_scope_of<R>(
505        &mut self,
506        stack_id: GcScopeStackId,
507        f: impl FnOnce(&mut GcScopeState) -> R,
508    ) -> Option<R> {
509        self.scope_stacks[stack_id.0 as usize]
510            .list
511            .last_mut()
512            .map(f)
513    }
514
515    #[inline]
516    pub fn with_new_scope<R>(
517        &mut self,
518        stack_id: GcScopeStackId,
519        f: impl FnOnce(GcScope<'_>) -> R,
520    ) -> R {
521        let scope = self.new_scope(stack_id);
522        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(scope)));
523
524        match result {
525            Ok(r) => r,
526            Err(e) => std::panic::resume_unwind(e),
527        }
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use crate::{GcRef, GcTraceCtx, trace::GcTrace};
534
535    use super::*;
536
537    #[derive(Debug)]
538    struct Node {
539        next: Option<GcRef<Node>>,
540        value: i32,
541    }
542
543    impl GcTrace for Node {
544        fn trace(&self, tr: &mut GcTraceCtx) {
545            if let Some(next) = self.next {
546                tr.add(next);
547            }
548        }
549    }
550
551    crate::gc_type_register! {
552        Node, drop_pass = 0;
553    }
554
555    #[test]
556    fn test_gc_context_local_flag_set_and_cleared_on_commit() {
557        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
558        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
559        let stack_id = heap.acquire_scope_stack(partition_id);
560
561        let head;
562
563        {
564            let ctx = heap.new_scope(stack_id);
565            let node = ctx
566                .alloc_local(Node {
567                    next: None,
568                    value: 1,
569                })
570                .unwrap();
571
572            head = node.as_raw().node_ptr();
573
574            unsafe {
575                assert!(head.as_ref().is_local());
576            }
577
578            ctx.clear();
579
580            unsafe {
581                assert!(!head.as_ref().is_local());
582            }
583        }
584
585        unsafe {
586            assert!(!head.as_ref().is_local());
587        }
588    }
589
590    #[test]
591    fn test_gc_context_alloc_protects_and_unprotects_on_drop() {
592        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
593        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
594        let stack_id = heap.acquire_scope_stack(partition_id);
595
596        let head;
597        {
598            let mut ctx = heap.new_scope(stack_id);
599            let node = ctx
600                .alloc_local(Node {
601                    next: None,
602                    value: 1,
603                })
604                .unwrap();
605
606            head = node.as_raw().node_ptr();
607
608            unsafe {
609                assert!(head.as_ref().is_local());
610            }
611
612            while !ctx.heap_mut().mark(partition_id, 64) {}
613            let removed = ctx
614                .heap_mut()
615                .sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
616            assert_eq!(removed, 0);
617            ctx.clear();
618        }
619
620        unsafe {
621            assert!(!head.as_ref().is_local());
622        }
623
624        while !heap.mark(partition_id, 64) {}
625        let removed_after = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
626        assert!(removed_after > 0);
627    }
628
629    #[test]
630    fn test_gc_context_add_sets_local_and_clears_on_commit() {
631        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
632        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
633        let stack_id = heap.acquire_scope_stack(partition_id);
634
635        let node: GcRef<Node> = unsafe {
636            heap.alloc_raw(
637                partition_id,
638                Node {
639                    next: None,
640                    value: 1,
641                },
642            )
643        }
644        .unwrap();
645
646        let head = node.head_ptr;
647
648        unsafe {
649            assert!(!head.as_ref().is_local());
650        }
651
652        {
653            let ctx = heap.new_scope(stack_id);
654            let added = ctx.add_non_local(head);
655            assert!(added);
656
657            unsafe {
658                assert!(head.as_ref().is_local());
659            }
660
661            ctx.clear();
662
663            unsafe {
664                assert!(!head.as_ref().is_local());
665            }
666        }
667
668        unsafe {
669            assert!(!head.as_ref().is_local());
670        }
671
672        while !heap.mark(partition_id, 64) {}
673        let removed_after = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
674        assert!(removed_after > 0);
675    }
676
677    #[test]
678    fn test_gc_context_add_on_local_node_returns_false() {
679        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
680        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
681        let stack_id = heap.acquire_scope_stack(partition_id);
682
683        let ctx = heap.new_scope(stack_id);
684        let node = ctx
685            .alloc_local(Node {
686                next: None,
687                value: 1,
688            })
689            .unwrap();
690
691        let head = node.as_raw().node_ptr();
692
693        unsafe {
694            assert!(head.as_ref().is_local());
695        }
696        let added = ctx.add_non_local(head);
697        assert!(!added);
698
699        unsafe {
700            assert!(head.as_ref().is_local());
701        }
702
703        ctx.clear();
704
705        unsafe {
706            assert!(!head.as_ref().is_local());
707        }
708    }
709
710    #[test]
711    fn test_root_node_not_collected_by_sweep() {
712        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
713        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
714
715        let head;
716
717        unsafe {
718            let node = heap
719                .alloc_root_raw(
720                    partition_id,
721                    Node {
722                        next: None,
723                        value: 1,
724                    },
725                )
726                .unwrap();
727
728            head = node.head_ptr;
729
730            assert!(node.is_root());
731            assert!(head.as_ref().is_root());
732            assert!(!head.as_ref().is_local());
733        }
734
735        while !heap.mark(partition_id, 64) {}
736        let removed_after = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
737        assert_eq!(removed_after, 0);
738    }
739
740    #[test]
741    fn test_gc_context_alloc_multiple_nodes() {
742        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
743        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
744        let stack_id = heap.acquire_scope_stack(partition_id);
745
746        let head1;
747        let head2;
748
749        {
750            let mut ctx = heap.new_scope(stack_id);
751            let n1 = ctx
752                .alloc_local(Node {
753                    next: None,
754                    value: 1,
755                })
756                .unwrap();
757            let n2 = ctx
758                .alloc_local(Node {
759                    next: None,
760                    value: 2,
761                })
762                .unwrap();
763
764            head1 = n1.as_raw().node_ptr();
765            head2 = n2.as_raw().node_ptr();
766
767            unsafe {
768                assert!(head1.as_ref().is_local());
769                assert!(head2.as_ref().is_local());
770            }
771
772            while !ctx.heap_mut().mark(partition_id, 64) {}
773            let removed = ctx
774                .heap_mut()
775                .sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
776            assert_eq!(removed, 0);
777            ctx.clear();
778        }
779
780        unsafe {
781            assert!(!head1.as_ref().is_local());
782            assert!(!head2.as_ref().is_local());
783        }
784
785        while !heap.mark(partition_id, 64) {}
786        let removed_after = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
787        assert!(removed_after >= 2);
788    }
789
790    #[test]
791    fn test_gc_context_reset_unprotects_and_clears_cache() {
792        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
793        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
794        let stack_id = heap.acquire_scope_stack(partition_id);
795
796        let head;
797
798        {
799            let mut ctx = heap.new_scope(stack_id);
800            let node = ctx
801                .alloc_local(Node {
802                    next: None,
803                    value: 1,
804                })
805                .unwrap();
806
807            head = node.as_raw().node_ptr();
808
809            unsafe {
810                assert!(head.as_ref().is_local());
811            }
812
813            ctx.clear();
814
815            unsafe {
816                assert!(!head.as_ref().is_local());
817            }
818
819            while !ctx.heap_mut().mark(partition_id, 64) {}
820            let removed_before = ctx
821                .heap_mut()
822                .sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
823            assert!(removed_before > 0);
824        }
825    }
826
827    #[test]
828    fn test_gc_context_level_for_heap_scopes() {
829        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
830        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
831        let stack_id = heap.acquire_scope_stack(partition_id);
832
833        assert_eq!(heap.scope_max_depth(stack_id), 0);
834
835        let heap_ptr = &mut heap as *mut GcHeap;
836        unsafe {
837            let s1 = (&mut *heap_ptr).new_scope(stack_id);
838            let s2 = (&mut *heap_ptr).new_scope(stack_id);
839            let s3 = (&mut *heap_ptr).new_scope(stack_id);
840
841            assert_eq!(s3.depth(), 3);
842            assert_eq!(s2.depth(), 2);
843            assert_eq!(s1.depth(), 1);
844
845            let ctx3 = (*heap_ptr).scope(stack_id, 3).unwrap();
846            assert_eq!(ctx3.depth(), 3);
847            let ctx2 = (*heap_ptr).scope(stack_id, 2).unwrap();
848            assert_eq!(ctx2.depth(), 2);
849            let ctx1 = (*heap_ptr).scope(stack_id, 1).unwrap();
850            assert_eq!(ctx1.depth(), 1);
851        }
852    }
853
854    #[test]
855    fn test_gc_context_parent_mut_returns_parent_and_level() {
856        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
857        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
858        let stack_id = heap.acquire_scope_stack(partition_id);
859
860        let heap_ptr = &mut heap as *mut GcHeap;
861        unsafe {
862            let scope1 = (&mut *heap_ptr).new_scope(stack_id);
863            let scope2 = (&mut *heap_ptr).new_scope(stack_id);
864            let scope3 = (&mut *heap_ptr).new_scope(stack_id);
865
866            assert_eq!(scope3.depth(), 3);
867            let (parent, parent_level) = scope3.parent().unwrap();
868            assert_eq!(parent_level, 2);
869            assert_eq!(parent.depth(), 2);
870
871            drop(scope3);
872
873            assert_eq!(scope2.depth(), 2);
874            let (parent, parent_level) = scope2.parent().unwrap();
875            assert_eq!(parent_level, 1);
876            assert_eq!(parent.depth(), 1);
877
878            drop(scope2);
879
880            assert_eq!(scope1.depth(), 1);
881            assert!(scope1.parent().is_none());
882        }
883    }
884
885    #[test]
886    fn test_multi_scope_stacks_are_independent() {
887        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
888        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
889        let stack1 = heap.acquire_scope_stack(partition_id);
890        let stack2 = heap.acquire_scope_stack(partition_id);
891
892        assert_ne!(stack1, stack2);
893        assert_eq!(heap.scope_max_depth(stack1), 0);
894        assert_eq!(heap.scope_max_depth(stack2), 0);
895
896        let heap_ptr = &mut heap as *mut GcHeap;
897        unsafe {
898            let _s1a = (&mut *heap_ptr).new_scope(stack1);
899            let _s2a = (&mut *heap_ptr).new_scope(stack2);
900            let _s1b = (&mut *heap_ptr).new_scope(stack1);
901
902            assert_eq!((*heap_ptr).scope_max_depth(stack1), 2);
903            assert_eq!((*heap_ptr).scope_max_depth(stack2), 1);
904            assert_eq!((*heap_ptr).current_scope(stack1).unwrap().depth(), 2);
905            assert_eq!((*heap_ptr).current_scope(stack2).unwrap().depth(), 1);
906
907            (*heap_ptr).with_current_scope_of(stack1, |ctx| {
908                let (parent, parent_level) = ctx.parent().unwrap();
909                assert_eq!(parent_level, 1);
910                assert_eq!(parent.depth(), 1);
911                assert_eq!(parent.stack_id(), stack1);
912            });
913        }
914
915        assert_eq!(heap.scope_max_depth(stack1), 0);
916        assert_eq!(heap.scope_max_depth(stack2), 0);
917
918        heap.release_scope_stack(stack1);
919        heap.release_scope_stack(stack2);
920
921        let reused = heap.acquire_scope_stack(partition_id);
922        assert_eq!(reused, stack1);
923    }
924
925    #[test]
926    fn test_gc_scope_drop_is_lifo_per_stack() {
927        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
928        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
929        let stack1 = heap.acquire_scope_stack(partition_id);
930        let stack2 = heap.acquire_scope_stack(partition_id);
931        let heap_ptr = &mut heap as *mut GcHeap;
932
933        unsafe {
934            let scope1 = (&mut *heap_ptr).new_scope(stack1);
935            let scope2 = (&mut *heap_ptr).new_scope(stack2);
936
937            drop(scope1);
938            drop(scope2);
939        }
940
941        assert_eq!(heap.scope_max_depth(stack1), 0);
942        assert_eq!(heap.scope_max_depth(stack2), 0);
943    }
944}