Skip to main content

gc_lite/
scope.rs

1use std::{num::NonZeroU8, ops::DerefMut};
2
3use {core::ptr::NonNull, std::cell::RefCell, std::marker::PhantomData};
4
5use smallvec::SmallVec;
6
7use crate::{
8    heap::GcHeap,
9    helpers::GcError,
10    node::{GcHead, GcNode, GcRef},
11    partition::GcPartitionId,
12};
13
14#[derive(Debug)]
15pub struct GcScopeState<'s> {
16    heap: NonNull<GcHeap>,
17    partition_id: GcPartitionId,
18    depth: NonZeroU8,
19    cache: RefCell<SmallVec<[NonNull<GcHead>; 8]>>,
20    promote: RefCell<Option<(NonNull<GcHead>, bool)>>,
21    _marker: PhantomData<&'s mut GcHeap>,
22}
23
24impl<'s> Drop for GcScopeState<'s> {
25    fn drop(&mut self) {
26        self.flush();
27    }
28}
29
30impl<'s> GcScopeState<'s> {
31    pub fn new(heap: &'s mut GcHeap, partition_id: GcPartitionId) -> Self {
32        debug_assert!(!partition_id.is_null());
33        let depth = heap.scope_stack.len() + 1;
34
35        Self {
36            heap: NonNull::from_ref(heap),
37            partition_id,
38            depth: NonZeroU8::new(depth as u8).unwrap(),
39            cache: RefCell::new(SmallVec::new()),
40            promote: RefCell::new(None),
41            _marker: PhantomData,
42        }
43    }
44
45    #[inline(always)]
46    pub fn partition_id(&self) -> GcPartitionId {
47        self.partition_id
48    }
49
50    #[inline(always)]
51    pub fn heap(&self) -> &GcHeap {
52        unsafe { self.heap.as_ref() }
53    }
54
55    #[inline(always)]
56    pub fn heap_mut(&mut self) -> &mut GcHeap {
57        unsafe { self.heap.as_mut() }
58    }
59
60    #[inline(always)]
61    pub fn depth(&self) -> u8 {
62        self.depth.get()
63    }
64
65    pub fn count(&self) -> usize {
66        self.cache.borrow().len()
67    }
68
69    /// get parent scope, and its level.
70    pub fn parent(&self) -> Option<(&GcScopeState<'_>, u8)> {
71        let d = self.depth();
72        if d > 1 {
73            let parent_index = d - 2;
74
75            self.heap().scope_stack.get(parent_index as usize).map(|s| {
76                (
77                    unsafe { std::mem::transmute::<&GcScopeState<'static>, &GcScopeState<'_>>(s) },
78                    d - 1,
79                )
80            })
81        } else {
82            None
83        }
84    }
85
86    #[inline(always)]
87    pub fn alloc_root<T: GcNode>(&self, payload: T) -> Result<GcRef<T>, (GcError, T)> {
88        unsafe { (*self.heap.as_ptr()).alloc_root_raw(self.partition_id, payload) }
89    }
90
91    /// alloc a local node in scope.
92    pub fn alloc_local<T: GcNode>(&self, payload: T) -> Result<GcRef<T>, (GcError, T)> {
93        let r = unsafe { (*self.heap.as_ptr()).alloc_raw(self.partition_id, payload)? };
94        self.add_node(r.head_ptr);
95        Ok(r)
96    }
97
98    fn add_node(&self, mut node: NonNull<GcHead>) {
99        #[cfg(debug_assertions)]
100        unsafe {
101            debug_assert!(!node.as_ref().is_root_or_local());
102            node.as_mut().dbg_scope_depth = self.depth();
103        }
104
105        unsafe {
106            node.as_mut().insert_flag(crate::node::GcNodeFlag::LOCAL);
107
108            if let Some(par) = (*self.heap.as_ptr()).partition_mut(self.partition_id)
109                && par.is_marking()
110            {
111                par.add_gray_node(node);
112            }
113        }
114
115        self.cache.borrow_mut().push(node);
116    }
117
118    // if node is neither root, nor local, then add it to `self` scope
119    pub fn add_non_local(&self, node: NonNull<GcHead>) -> bool {
120        if unsafe { node.as_ref().is_root_or_local() } {
121            false
122        } else {
123            self.add_node(node);
124            true
125        }
126    }
127
128    pub fn get_promote(&self) -> Option<NonNull<GcHead>> {
129        self.promote.borrow().map(|(p, _)| p)
130    }
131
132    /// Set a node to be promoted.
133    ///
134    /// Promote means when the scope is dropped, the node will be added to upper scope.
135    pub fn set_promote(&self, node: Option<NonNull<GcHead>>) {
136        #[cfg(debug_assertions)]
137        {
138            debug_assert_eq!(
139                self.depth(),
140                self.heap().scope_max_depth(),
141                "only inner-most scope can set_promote"
142            );
143
144            if let Some(n) = node {
145                unsafe {
146                    n.as_ref().debug_assert_node_valid(self.heap());
147                }
148            }
149        }
150
151        // Note: scope depth==1 indicates the top scope, which don't promote any node
152        if self.depth() == 1 {
153            debug_assert!(
154                self.promote.borrow().is_none(),
155                "top scope don't promote node"
156            );
157            return;
158        }
159
160        // check current promote node
161        if let Some((mut p, was_non_local)) = self.promote.borrow_mut().take()
162            && was_non_local
163        {
164            unsafe {
165                p.as_mut().remove_flag(crate::node::GcNodeFlag::LOCAL);
166
167                let mut lst = self.cache.borrow_mut();
168                if let Some(i) = lst.iter().position(|&n| n == p) {
169                    lst.swap_remove(i);
170                }
171            }
172        }
173
174        if let Some(n) = node {
175            let h = unsafe { n.as_ref() };
176            if h.is_root() {
177                return; // root node don't need to be promoted
178            }
179
180            let is_non_local = if h.is_local() {
181                if !self
182                    .cache
183                    .borrow()
184                    .iter()
185                    .any(|p| std::ptr::eq(n.as_ptr(), p.as_ptr()))
186                {
187                    // node is already in some other scope, can't promote by this
188                    return;
189                }
190                false
191            } else {
192                self.add_node(n);
193                true
194            };
195
196            *self.promote.borrow_mut() = Some((n, is_non_local));
197        }
198    }
199
200    /// clear and unprotect locals nodes in this scope; promote the result node to upper scope.
201    pub fn flush(&self) {
202        let promote: Option<NonNull<GcHead>> = self.promote.borrow_mut().take().map(|(p, _)| p);
203
204        if let Some(mut p) = promote {
205            let (up, _lev) = self.parent().unwrap();
206            // put to upper scope cache
207            up.cache.borrow_mut().push(p);
208
209            #[cfg(debug_assertions)]
210            unsafe {
211                p.as_mut().dbg_scope_depth = _lev as _;
212            }
213        }
214
215        let lst = std::mem::take(self.cache.borrow_mut().deref_mut());
216        for mut n in lst.into_iter().filter(|&p| promote.is_none_or(|x| x != p)) {
217            unsafe {
218                n.as_mut().remove_flag(crate::node::GcNodeFlag::LOCAL);
219
220                #[cfg(debug_assertions)]
221                {
222                    n.as_mut().dbg_scope_depth = 0;
223                }
224            }
225        }
226    }
227
228    pub fn contains(&self, node: NonNull<GcHead>) -> bool {
229        self.cache.borrow().contains(&node)
230    }
231
232    /// # Safety
233    ///
234    /// 仅供 `GcHeap::drop` 在销毁事务栈时调用,用于跳过对
235    /// `nodes` 中节点的保护计数更新与根集合维护逻辑。
236    /// 调用方必须保证这些节点即将被整体释放,不再通过 GC 访问。
237    pub(super) unsafe fn abort(&self) {
238        self.promote.borrow_mut().take();
239
240        let nodes = std::mem::take(self.cache.borrow_mut().deref_mut());
241        for mut n in nodes {
242            unsafe {
243                n.as_mut().remove_flag(crate::node::GcNodeFlag::LOCAL);
244            }
245        }
246    }
247}
248
249#[derive(Debug)]
250pub struct GcScope<'s> {
251    heap: NonNull<GcHeap>,
252    index: u8,
253    _marker: PhantomData<&'s mut GcHeap>,
254}
255
256impl<'s> GcScope<'s> {
257    #[inline(always)]
258    fn state(&self) -> &GcScopeState<'s> {
259        unsafe {
260            debug_assert!((self.index as usize) < self.heap.as_ref().scope_stack.len());
261
262            std::mem::transmute::<&GcScopeState<'_>, &GcScopeState<'s>>(
263                self.heap
264                    .as_ref()
265                    .scope_stack
266                    .get(self.index as usize)
267                    .unwrap_unchecked(),
268            )
269        }
270    }
271
272    #[inline(always)]
273    fn state_mut(&mut self) -> &mut GcScopeState<'s> {
274        unsafe {
275            debug_assert!((self.index as usize) < self.heap.as_ref().scope_stack.len());
276
277            std::mem::transmute::<&mut GcScopeState<'_>, &mut GcScopeState<'s>>(
278                self.heap
279                    .as_mut()
280                    .scope_stack
281                    .get_mut(self.index as usize)
282                    .unwrap_unchecked(),
283            )
284        }
285    }
286
287    #[inline(always)]
288    pub fn new(heap: &'s mut GcHeap, partition_id: GcPartitionId) -> Self {
289        heap.new_scope(partition_id)
290    }
291
292    pub fn new_scope<'child>(&'child mut self) -> GcScope<'child>
293    where
294        's: 'child,
295    {
296        let partition_id = self.partition_id();
297        unsafe { self.heap.as_mut().new_scope(partition_id) }
298    }
299
300    pub fn with_new_scope<R>(&self, f: impl FnOnce(GcScope<'_>) -> R) -> R {
301        let partition_id = self.partition_id();
302        let heap = unsafe { &mut *self.heap.as_ptr() };
303        let scope = heap.new_scope(partition_id);
304        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(scope)));
305
306        match result {
307            Ok(r) => r,
308            Err(e) => std::panic::resume_unwind(e),
309        }
310    }
311}
312
313impl<'s> std::ops::Deref for GcScope<'s> {
314    type Target = GcScopeState<'s>;
315
316    #[inline(always)]
317    fn deref(&self) -> &Self::Target {
318        self.state()
319    }
320}
321
322impl<'s> std::ops::DerefMut for GcScope<'s> {
323    #[inline(always)]
324    fn deref_mut(&mut self) -> &mut Self::Target {
325        self.state_mut()
326    }
327}
328
329impl<'s> Drop for GcScope<'s> {
330    fn drop(&mut self) {
331        unsafe {
332            let heap = self.heap.as_mut();
333            debug_assert_eq!(heap.scope_stack.len() as u8 - 1, self.index);
334            heap.pop_gc_scope();
335        }
336    }
337}
338
339impl GcHeap {
340    #[inline(always)]
341    pub fn scope_max_depth(&self) -> u8 {
342        self.scope_stack.len() as _
343    }
344
345    /// get scope by depth (peek, without modifying stack)
346    pub fn scope(&self, depth: u8) -> Option<&GcScopeState<'_>> {
347        if depth == 0 {
348            return None;
349        }
350
351        self.scope_stack.get(depth as usize - 1)
352    }
353
354    pub(crate) fn push_gc_scope(&mut self, partition_id: GcPartitionId) -> &GcScopeState<'_> {
355        let ctx = GcScopeState::new(self, partition_id);
356        // SAFETY: It is safe because the GcHeap owns the GcScope, and we ensure that
357        // the GcScope does not outlive the GcHeap.
358        let static_ctx =
359            unsafe { std::mem::transmute::<GcScopeState<'_>, GcScopeState<'static>>(ctx) };
360        self.scope_stack.push(static_ctx);
361
362        self.scope_stack.last().unwrap()
363    }
364
365    #[inline(always)]
366    pub(crate) fn pop_gc_scope(&mut self) -> Option<GcScopeState<'_>> {
367        self.scope_stack.pop()
368    }
369
370    #[inline]
371    pub fn current_scope(&self) -> Option<&GcScopeState<'_>> {
372        let s = self.scope_stack.last();
373        // SAFETY: It is safe because the GcHeap owns the GcScope, and we ensure that
374        // the GcScope does not outlive the GcHeap.
375        unsafe {
376            std::mem::transmute::<Option<&GcScopeState<'static>>, Option<&GcScopeState<'_>>>(s)
377        }
378    }
379
380    #[inline]
381    pub fn with_current_scope<R>(&mut self, f: impl FnOnce(&mut GcScopeState) -> R) -> Option<R> {
382        self.scope_stack.last_mut().map(f)
383    }
384
385    #[inline]
386    pub fn new_scope<'s>(&'s mut self, partition_id: GcPartitionId) -> GcScope<'s> {
387        self.push_gc_scope(partition_id);
388        let index = self.scope_stack.len() as u8 - 1;
389        GcScope {
390            heap: NonNull::from(self),
391            index,
392            _marker: PhantomData,
393        }
394    }
395
396    #[inline]
397    pub fn with_new_scope<R>(
398        &mut self,
399        partition_id: GcPartitionId,
400        f: impl FnOnce(GcScope<'_>) -> R,
401    ) -> R {
402        let scope = self.new_scope(partition_id);
403        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(scope)));
404
405        match result {
406            Ok(r) => r,
407            Err(e) => std::panic::resume_unwind(e),
408        }
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use crate::{GcTraceCtx, trace::GcTrace};
415
416    use super::*;
417
418    #[derive(Debug)]
419    struct Node {
420        next: Option<GcRef<Node>>,
421        value: i32,
422    }
423
424    impl GcTrace for Node {
425        fn trace(&self, tr: &mut GcTraceCtx) {
426            if let Some(next) = self.next {
427                tr.add(next);
428            }
429        }
430    }
431
432    crate::gc_type_register! {
433        Node, drop_pass = 0;
434    }
435
436    #[test]
437    fn test_gc_context_local_flag_set_and_cleared_on_commit() {
438        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
439        let partition_id = heap.create_partition();
440
441        let head;
442
443        {
444            let ctx = GcScope::new(&mut heap, partition_id);
445            let node: GcRef<Node> = ctx
446                .alloc_local(Node {
447                    next: None,
448                    value: 1,
449                })
450                .unwrap();
451
452            head = node.head_ptr;
453
454            unsafe {
455                assert!(head.as_ref().is_local());
456            }
457
458            ctx.flush();
459
460            unsafe {
461                assert!(!head.as_ref().is_local());
462            }
463        }
464
465        unsafe {
466            assert!(!head.as_ref().is_local());
467        }
468    }
469
470    #[test]
471    fn test_gc_context_alloc_protects_and_unprotects_on_drop() {
472        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
473        let partition_id = heap.create_partition();
474
475        let head;
476        {
477            let mut ctx = GcScope::new(&mut heap, partition_id);
478            let node: GcRef<Node> = ctx
479                .alloc_local(Node {
480                    next: None,
481                    value: 1,
482                })
483                .unwrap();
484
485            head = node.head_ptr;
486
487            unsafe {
488                assert!(head.as_ref().is_local());
489            }
490
491            while !ctx.heap_mut().mark(partition_id, 64) {}
492            let removed = ctx
493                .heap_mut()
494                .sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
495            assert_eq!(removed, 0);
496            ctx.flush();
497        }
498
499        unsafe {
500            assert!(!head.as_ref().is_local());
501        }
502
503        while !heap.mark(partition_id, 64) {}
504        let removed_after = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
505        assert!(removed_after > 0);
506    }
507
508    #[test]
509    fn test_gc_context_add_sets_local_and_clears_on_commit() {
510        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
511        let partition_id = heap.create_partition();
512
513        let node: GcRef<Node> = unsafe {
514            heap.alloc_raw(
515                partition_id,
516                Node {
517                    next: None,
518                    value: 1,
519                },
520            )
521        }
522        .unwrap();
523
524        let head = node.head_ptr;
525
526        unsafe {
527            assert!(!head.as_ref().is_local());
528        }
529
530        {
531            let mut ctx = GcScope::new(&mut heap, partition_id);
532            let added = ctx.add_non_local(head);
533            assert!(added);
534
535            unsafe {
536                assert!(head.as_ref().is_local());
537            }
538
539            ctx.flush();
540
541            unsafe {
542                assert!(!head.as_ref().is_local());
543            }
544        }
545
546        unsafe {
547            assert!(!head.as_ref().is_local());
548        }
549
550        while !heap.mark(partition_id, 64) {}
551        let removed_after = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
552        assert!(removed_after > 0);
553    }
554
555    #[test]
556    fn test_gc_context_add_on_local_node_returns_false() {
557        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
558        let partition_id = heap.create_partition();
559
560        let ctx = GcScope::new(&mut heap, partition_id);
561        let node: GcRef<Node> = ctx
562            .alloc_local(Node {
563                next: None,
564                value: 1,
565            })
566            .unwrap();
567
568        let head = node.head_ptr;
569
570        unsafe {
571            assert!(head.as_ref().is_local());
572        }
573        let added = ctx.add_non_local(head);
574        assert!(!added);
575
576        unsafe {
577            assert!(head.as_ref().is_local());
578        }
579
580        ctx.flush();
581
582        unsafe {
583            assert!(!head.as_ref().is_local());
584        }
585    }
586
587    #[test]
588    fn test_gc_context_alloc_root_creates_root_without_protection() {
589        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
590        let partition_id = heap.create_partition();
591
592        let head;
593
594        {
595            let ctx = GcScope::new(&mut heap, partition_id);
596            let node: GcRef<Node> = ctx
597                .alloc_root(Node {
598                    next: None,
599                    value: 1,
600                })
601                .unwrap();
602
603            head = node.head_ptr;
604
605            unsafe {
606                assert!(node.is_root());
607                assert!(head.as_ref().is_root());
608                assert!(!head.as_ref().is_local());
609            }
610        }
611
612        while !heap.mark(partition_id, 64) {}
613        let removed_after = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
614        assert_eq!(removed_after, 0);
615    }
616
617    #[test]
618    fn test_gc_context_alloc_multiple_nodes() {
619        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
620        let partition_id = heap.create_partition();
621
622        let head1;
623        let head2;
624
625        {
626            let mut ctx = GcScope::new(&mut heap, partition_id);
627            let n1: GcRef<Node> = ctx
628                .alloc_local(Node {
629                    next: None,
630                    value: 1,
631                })
632                .unwrap();
633            let n2: GcRef<Node> = ctx
634                .alloc_local(Node {
635                    next: None,
636                    value: 2,
637                })
638                .unwrap();
639
640            head1 = n1.head_ptr;
641            head2 = n2.head_ptr;
642
643            unsafe {
644                assert!(head1.as_ref().is_local());
645                assert!(head2.as_ref().is_local());
646            }
647
648            while !ctx.heap_mut().mark(partition_id, 64) {}
649            let removed = ctx
650                .heap_mut()
651                .sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
652            assert_eq!(removed, 0);
653            ctx.flush();
654        }
655
656        unsafe {
657            assert!(!head1.as_ref().is_local());
658            assert!(!head2.as_ref().is_local());
659        }
660
661        while !heap.mark(partition_id, 64) {}
662        let removed_after = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
663        assert!(removed_after >= 2);
664    }
665
666    #[test]
667    fn test_gc_context_reset_unprotects_and_clears_cache() {
668        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
669        let partition_id = heap.create_partition();
670
671        let head;
672
673        {
674            let mut ctx = GcScope::new(&mut heap, partition_id);
675            let node: GcRef<Node> = ctx
676                .alloc_local(Node {
677                    next: None,
678                    value: 1,
679                })
680                .unwrap();
681
682            head = node.head_ptr;
683
684            unsafe {
685                assert!(head.as_ref().is_local());
686            }
687
688            ctx.flush();
689
690            unsafe {
691                assert!(!head.as_ref().is_local());
692            }
693
694            while !ctx.heap_mut().mark(partition_id, 64) {}
695            let removed_before = ctx
696                .heap_mut()
697                .sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
698            assert!(removed_before > 0);
699        }
700    }
701
702    #[test]
703    fn test_gc_context_level_for_heap_scopes() {
704        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
705        let partition_id = heap.create_partition();
706
707        assert_eq!(heap.scope_max_depth(), 0);
708
709        heap.push_gc_scope(partition_id);
710        heap.push_gc_scope(partition_id);
711        heap.push_gc_scope(partition_id);
712
713        assert_eq!(heap.scope_max_depth(), 3);
714
715        for level in 1..=3 {
716            let ctx = heap.scope(level).unwrap();
717            assert_eq!(ctx.depth(), level);
718        }
719
720        heap.pop_gc_scope();
721        assert_eq!(heap.scope_max_depth(), 2);
722        for level in 1..=2 {
723            let ctx = heap.scope(level).unwrap();
724            assert_eq!(ctx.depth(), level);
725        }
726
727        heap.pop_gc_scope();
728        assert_eq!(heap.scope_max_depth(), 1);
729        let ctx = heap.scope(1).unwrap();
730        assert_eq!(ctx.depth(), 1);
731    }
732
733    #[test]
734    fn test_gc_context_parent_mut_returns_parent_and_level() {
735        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
736        let partition_id = heap.create_partition();
737
738        heap.push_gc_scope(partition_id);
739        heap.push_gc_scope(partition_id);
740        heap.push_gc_scope(partition_id);
741
742        heap.with_current_scope(|ctx| {
743            assert_eq!(ctx.depth(), 3);
744            let (parent, parent_level) = ctx.parent().unwrap();
745            assert_eq!(parent_level, 2);
746            assert_eq!(parent.depth(), 2);
747        });
748
749        heap.pop_gc_scope();
750
751        heap.with_current_scope(|ctx| {
752            assert_eq!(ctx.depth(), 2);
753            let (parent, parent_level) = ctx.parent().unwrap();
754            assert_eq!(parent_level, 1);
755            assert_eq!(parent.depth(), 1);
756        });
757
758        heap.pop_gc_scope();
759
760        heap.with_current_scope(|ctx| {
761            assert_eq!(ctx.depth(), 1);
762            assert!(ctx.parent().is_none());
763        });
764    }
765
766    #[test]
767    fn test_set_promote_moves_node_to_parent_scope_and_keeps_protection() {
768        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
769        let partition_id = heap.create_partition();
770
771        heap.push_gc_scope(partition_id);
772        heap.push_gc_scope(partition_id);
773
774        let promoted_head;
775        let other_head;
776
777        {
778            let ctx = heap.scope_stack.last_mut().unwrap();
779            let promoted: GcRef<Node> = ctx
780                .alloc_local(Node {
781                    next: None,
782                    value: 1,
783                })
784                .unwrap();
785            let other: GcRef<Node> = ctx
786                .alloc_local(Node {
787                    next: None,
788                    value: 2,
789                })
790                .unwrap();
791
792            promoted_head = promoted.head_ptr;
793            other_head = other.head_ptr;
794
795            unsafe {
796                assert!(promoted_head.as_ref().is_local());
797                assert!(other_head.as_ref().is_local());
798            }
799
800            ctx.set_promote(Some(promoted_head));
801        }
802
803        heap.with_current_scope(|ctx| ctx.flush());
804
805        unsafe {
806            assert!(promoted_head.as_ref().is_local());
807            assert!(!other_head.as_ref().is_local());
808        }
809
810        {
811            let parent_ctx = heap.scope_stack.first().unwrap();
812            assert!(parent_ctx.contains(promoted_head));
813        }
814
815        {
816            let child_ctx = heap.scope_stack.last().unwrap();
817            assert!(!child_ctx.contains(promoted_head));
818        }
819
820        {
821            let parent_ctx = heap.scope_stack.first_mut().unwrap();
822            parent_ctx.flush();
823        }
824
825        unsafe {
826            assert!(!promoted_head.as_ref().is_local());
827        }
828    }
829
830    #[test]
831    fn test_set_promote_in_top_level_scope_is_noop() {
832        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
833        let partition_id = heap.create_partition();
834
835        heap.push_gc_scope(partition_id);
836
837        let head;
838
839        {
840            let ctx = heap.scope_stack.last_mut().unwrap();
841            let node: GcRef<Node> = ctx
842                .alloc_local(Node {
843                    next: None,
844                    value: 1,
845                })
846                .unwrap();
847
848            head = node.head_ptr;
849
850            unsafe {
851                assert!(head.as_ref().is_local());
852            }
853
854            ctx.set_promote(Some(head));
855            ctx.flush();
856        }
857
858        unsafe {
859            assert!(!head.as_ref().is_local());
860        }
861    }
862
863    #[test]
864    fn test_set_promote_reset_on_non_local_restores_state() {
865        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
866        let partition_id = heap.create_partition();
867
868        let node: GcRef<Node> = unsafe {
869            heap.alloc_raw(
870                partition_id,
871                Node {
872                    next: None,
873                    value: 1,
874                },
875            )
876        }
877        .unwrap();
878
879        let head = node.head_ptr;
880
881        unsafe {
882            assert!(!head.as_ref().is_local());
883        }
884
885        heap.push_gc_scope(partition_id);
886        heap.push_gc_scope(partition_id);
887
888        {
889            let ctx = heap.scope_stack.last_mut().unwrap();
890            ctx.set_promote(Some(head));
891
892            unsafe {
893                assert!(head.as_ref().is_local());
894            }
895
896            ctx.set_promote(None);
897            ctx.flush();
898        }
899
900        {
901            let parent_ctx = heap.scope_stack.first().unwrap();
902            assert!(!parent_ctx.contains(head));
903        }
904
905        unsafe {
906            assert!(!head.as_ref().is_local());
907        }
908    }
909}