Skip to main content

gc_lite/
trace.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright (c) 2025-2026 John Ray <996351336@qq.com>
3
4use std::{collections::VecDeque, marker::PhantomData, ptr::NonNull};
5
6use crate::{GcHeap, GcNode, GcPartitionId, GcRef, node::GcHead};
7
8/// Gc trace function trait
9pub trait GcTraceFn: Fn(&mut GcTraceCtx) {}
10impl<C: Fn(&mut GcTraceCtx)> GcTraceFn for C {}
11
12pub trait GcTrace: 'static {
13    /// Collect directly referenced children gc nodes
14    fn trace(&self, gcx: &mut GcTraceCtx);
15
16    /// Get direct referencing children nodes
17    fn gc_children(&self, heap: &GcHeap) -> Vec<NonNull<GcHead>> {
18        let mut gcx = heap.create_trace_ctx(64);
19        self.trace(&mut gcx);
20        gcx.traced_nodes
21    }
22}
23
24pub struct GcTraceCtx<'a> {
25    pub(crate) traced_nodes: Vec<NonNull<GcHead>>,
26    pub(crate) opaque: *mut u8,
27    pub(crate) _mark: PhantomData<&'a ()>,
28}
29
30impl<'a> GcTraceCtx<'a> {
31    #[inline(always)]
32    pub const fn opaque(&self) -> *mut u8 {
33        self.opaque
34    }
35
36    /// Submit a node to collected list regardless its color state.
37    pub fn add_node(&mut self, node: NonNull<GcHead>) {
38        #[cfg(debug_assertions)]
39        unsafe {
40            node.as_ref().debug_assert_node_valid_simple();
41        }
42
43        if !self.traced_nodes.contains(&node) {
44            self.traced_nodes.push(node);
45        }
46    }
47
48    /// Submit a GcRef to collected list
49    #[inline(always)]
50    pub fn add<T: GcNode>(&mut self, gc_ref: GcRef<T>) {
51        self.add_node(gc_ref.head_ptr);
52    }
53
54    #[inline(always)]
55    pub fn take_nodes(&mut self) -> Vec<NonNull<GcHead>> {
56        std::mem::take(&mut self.traced_nodes)
57    }
58}
59
60impl GcHeap {
61    pub fn create_trace_ctx(&self, cap: usize) -> GcTraceCtx<'_> {
62        GcTraceCtx {
63            traced_nodes: Vec::with_capacity(cap),
64            opaque: self.opaque(),
65            _mark: PhantomData,
66        }
67    }
68
69    /// Trace direct children of a node into the given trace context
70    pub fn trace_node(&self, node: NonNull<GcHead>, gcx: &mut GcTraceCtx) {
71        let dtype = unsafe { node.as_ref().dtype() } as usize;
72
73        #[cfg(debug_assertions)]
74        let info = self
75            .node_dtypes
76            .type_info_list
77            .get(dtype)
78            .unwrap_or_else(|| {
79                panic!(
80                    "trace_node: invalid dtype {} (max {})",
81                    dtype,
82                    self.node_dtypes.type_info_list.len().saturating_sub(1),
83                )
84            });
85
86        #[cfg(not(debug_assertions))]
87        let info = unsafe { self.node_dtypes.type_info_list.get_unchecked(dtype) };
88
89        (info.trace_fn)(node, gcx);
90    }
91
92    pub fn traverse_start(&mut self, partition_id: GcPartitionId) {
93        for mut node in self.nodes(partition_id) {
94            unsafe {
95                node.as_mut().set_traverse_visited(false);
96            }
97        }
98    }
99
100    /// Traverses the node tree starting at `node` in depth-first order,
101    /// invoking `callback` on each visited node with its optional parent.
102    /// If `filter` is `Some`, only nodes in the specified partition are visited.
103    pub fn traverse(
104        &mut self,
105        node: NonNull<GcHead>,
106        filter: Option<GcPartitionId>,
107        mut callback: impl FnMut(NonNull<GcHead>, Option<NonNull<GcHead>>),
108    ) {
109        let mut stack: VecDeque<(NonNull<GcHead>, Option<NonNull<GcHead>>)> =
110            vec![(node, None)].into();
111
112        let mut gcx = self.create_trace_ctx(64);
113
114        while let Some((mut current, parent)) = stack.pop_front() {
115            unsafe {
116                #[cfg(debug_assertions)]
117                current.as_ref().debug_assert_node_valid(self);
118
119                if current.as_ref().traverse_visited() {
120                    continue;
121                }
122
123                current.as_mut().set_traverse_visited(true);
124
125                if filter.is_none() || filter == Some(current.as_ref().partition_id()) {
126                    callback(current, parent);
127                }
128
129                self.trace_node(current, &mut gcx);
130
131                while let Some(child) = gcx.traced_nodes.pop() {
132                    if !child.as_ref().traverse_visited() {
133                        stack.push_back((child, Some(current)));
134                    }
135                }
136            }
137        }
138    }
139}
140
141macro_rules! impl_dummy_trace_for_primitive {
142    ($($ty:ty),*) => {
143        $(
144            impl GcTrace for $ty {
145                #[inline(always)]
146                fn trace(&self, _: &mut GcTraceCtx) { }
147            }
148
149            impl GcTrace for [$ty] {
150                #[inline(always)]
151                fn trace(&self, _: &mut GcTraceCtx) { }
152            }
153
154            impl GcTrace for Vec<$ty> {
155                #[inline(always)]
156                fn trace(&self, _: &mut GcTraceCtx) { }
157            }
158
159            impl GcTrace for Box<[$ty]> {
160                #[inline(always)]
161                fn trace(&self, _: &mut GcTraceCtx) { }
162            }
163        )*
164    };
165}
166
167// Implement GcTrace for basic types
168impl_dummy_trace_for_primitive!(
169    u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64, usize, isize, bool, char
170);
171
172impl GcTrace for str {
173    #[inline(always)]
174    fn trace(&self, _: &mut GcTraceCtx) {}
175}
176
177impl GcTrace for &'static str {
178    #[inline(always)]
179    fn trace(&self, _: &mut GcTraceCtx) {}
180}
181
182impl GcTrace for String {
183    #[inline(always)]
184    fn trace(&self, _: &mut GcTraceCtx) {}
185}
186
187impl GcTrace for &'static String {
188    #[inline(always)]
189    fn trace(&self, _: &mut GcTraceCtx) {}
190}
191
192#[cfg(test)]
193mod tests {
194
195    use super::*;
196    use crate::{GcHeap, GcRef, node::GcTriColor};
197
198    /// Test node structure for tracing tests
199    #[derive(Debug)]
200    struct TestNode {
201        id: u32,
202        children: Vec<GcRef<TestNode>>,
203    }
204
205    impl TestNode {
206        fn new(id: u32) -> Self {
207            Self {
208                id,
209                children: Vec::new(),
210            }
211        }
212
213        fn add_child(&mut self, child: GcRef<TestNode>) {
214            self.children.push(child);
215        }
216    }
217
218    impl GcTrace for TestNode {
219        fn trace(&self, tr: &mut GcTraceCtx) {
220            println!(
221                "TestNode::trace({self:p}), {} children",
222                self.children.len()
223            );
224
225            for (i, child) in self.children.iter().enumerate() {
226                println!("  Tracing child {}: {:?}", i, child.node_ptr());
227                tr.add(*child);
228            }
229        }
230    }
231
232    crate::gc_type_register! {
233        TestNode, drop_pass = 0;
234        Align32Node, drop_pass = 0;
235    }
236
237    /// Helper function to count marked nodes in a partition
238    fn count_non_white_nodes(heap: &GcHeap, partition_id: GcPartitionId) -> usize {
239        let mut count = 0;
240        for node in heap.nodes(partition_id) {
241            unsafe {
242                if node.as_ref().color() != GcTriColor::White {
243                    count += 1;
244                }
245            }
246        }
247        count
248    }
249
250    /// Helper function to get all node IDs in a partition
251    fn get_all_node_ids(heap: &GcHeap, partition_id: GcPartitionId) -> Vec<u32> {
252        let mut ids = Vec::new();
253        let info = &GC_TYPE_REGISTRY.type_info_list[TestNode::GC_TYPE_ID as usize];
254        for node in heap.nodes(partition_id) {
255            unsafe {
256                let payload_ptr = info.payload_ptr(node);
257                let id = payload_ptr.cast::<TestNode>().as_ref().id;
258                ids.push(id);
259            }
260        }
261        ids
262    }
263
264    /// Test 1: Simple tree structure with Propagate (depth-first)
265    #[test]
266    fn test_trace_propagate_simple_tree() {
267        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
268        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
269
270        let child1 = unsafe { heap.alloc_raw(partition_id, TestNode::new(1)) }.unwrap();
271        let child2 = unsafe { heap.alloc_raw(partition_id, TestNode::new(2)) }.unwrap();
272
273        let mut root = TestNode::new(0);
274        root.add_child(child1);
275        root.add_child(child2);
276        let root_ref = unsafe { heap.alloc_root_raw(partition_id, root) }.unwrap();
277
278        // Debug: print node pointers
279        println!("Root: {:?}", root_ref.node_ptr());
280        println!("Child1: {:?}", child1.node_ptr());
281        println!("Child2: {:?}", child2.node_ptr());
282
283        // Mark reachable nodes using GC mark algorithm
284        while !heap.mark(partition_id, 16) {}
285
286        // check marks after tracing
287        println!(
288            "Marks after tracing: {}",
289            count_non_white_nodes(&heap, partition_id)
290        );
291
292        // Verify all nodes are marked
293        assert_eq!(count_non_white_nodes(&heap, partition_id), 3);
294
295        // Verify all node IDs are present
296        let ids = get_all_node_ids(&heap, partition_id);
297        assert!(ids.contains(&0));
298        assert!(ids.contains(&1));
299        assert!(ids.contains(&2));
300    }
301
302    /// Test 2: Simple tree structure with Continue (breadth-first)
303    #[test]
304    fn test_trace_continue_simple_tree() {
305        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
306        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
307
308        let child1 = unsafe { heap.alloc_raw(partition_id, TestNode::new(1)) }.unwrap();
309        let child2 = unsafe { heap.alloc_raw(partition_id, TestNode::new(2)) }.unwrap();
310
311        let mut root = TestNode::new(0);
312        root.add_child(child1);
313        root.add_child(child2);
314        let root_ref = unsafe { heap.alloc_root_raw(partition_id, root) }.unwrap();
315        while !heap.mark(partition_id, 1) {}
316
317        // Verify all nodes are marked
318        assert_eq!(count_non_white_nodes(&heap, partition_id), 3);
319    }
320
321    /// Test 3: Deep nested tree with both algorithms
322    #[test]
323    fn test_trace_deep_nested_tree() {
324        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
325        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
326
327        let level3 = unsafe { heap.alloc_raw(partition_id, TestNode::new(3)) }.unwrap();
328
329        let mut level2 = TestNode::new(2);
330        level2.add_child(level3);
331        let level2_ref = unsafe { heap.alloc_raw(partition_id, level2) }.unwrap();
332
333        let mut level1 = TestNode::new(1);
334        level1.add_child(level2_ref);
335        let level1_ref = unsafe { heap.alloc_raw(partition_id, level1) }.unwrap();
336
337        let mut level0 = TestNode::new(0);
338        level0.add_child(level1_ref);
339        let level0_ref = unsafe { heap.alloc_root_raw(partition_id, level0) }.unwrap();
340
341        // Mark reachable nodes
342        while !heap.mark(partition_id, 4) {}
343        assert_eq!(count_non_white_nodes(&heap, partition_id), 4);
344    }
345
346    /// Test 4: Complex tree with multiple branches
347    #[test]
348    fn test_trace_complex_tree() {
349        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
350        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
351
352        // Create a complex tree:
353        //        root
354        //       /    \
355        //      a      b
356        //     / \    / \
357        //    c   d  e   f
358
359        let c = unsafe { heap.alloc_raw(partition_id, TestNode::new(3)) }.unwrap();
360        let d = unsafe { heap.alloc_raw(partition_id, TestNode::new(4)) }.unwrap();
361        let e = unsafe { heap.alloc_raw(partition_id, TestNode::new(5)) }.unwrap();
362        let f = unsafe { heap.alloc_raw(partition_id, TestNode::new(6)) }.unwrap();
363
364        let mut a = TestNode::new(1);
365        a.add_child(c);
366        a.add_child(d);
367        let a_ref = unsafe { heap.alloc_raw(partition_id, a) }.unwrap();
368
369        let mut b = TestNode::new(2);
370        b.add_child(e);
371        b.add_child(f);
372        let b_ref = unsafe { heap.alloc_raw(partition_id, b) }.unwrap();
373
374        let mut root = TestNode::new(0);
375        root.add_child(a_ref);
376        root.add_child(b_ref);
377        unsafe { heap.alloc_root_raw(partition_id, root) }.unwrap();
378
379        // Mark reachable nodes
380        while !heap.mark(partition_id, 8) {}
381        assert_eq!(count_non_white_nodes(&heap, partition_id), 7);
382    }
383
384    /// Test 5: Verify both algorithms produce same result
385    #[test]
386    fn test_trace_algorithms_equivalence() {
387        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
388        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
389
390        // Create a tree with 10 nodes in a balanced structure
391        let mut nodes = Vec::new();
392        for i in 0..10 {
393            nodes.push(unsafe { heap.alloc_raw(partition_id, TestNode::new(i as u32)) }.unwrap());
394        }
395
396        // Build tree: 0 -> 1,2; 1 -> 3,4; 2 -> 5,6; 3 -> 7,8; 4 -> 9
397        unsafe {
398            let mut nodes = nodes.clone();
399            let n = nodes[1];
400            unsafe {
401                nodes[0].with_write_barrier(&mut heap, |node| node.add_child(n));
402            }
403
404            let n = nodes[2];
405            unsafe {
406                nodes[0].with_write_barrier(&mut heap, |node| node.add_child(n));
407            }
408
409            let n = nodes[3];
410            nodes[1].with_write_barrier(&mut heap, |node| node.add_child(n));
411
412            let n = nodes[4];
413            nodes[1].with_write_barrier(&mut heap, |node| node.add_child(n));
414
415            let n = nodes[5];
416            unsafe {
417                nodes[2].with_write_barrier(&mut heap, |node| node.add_child(n));
418            }
419
420            let n = nodes[6];
421            nodes[2].with_write_barrier(&mut heap, |node| node.add_child(n));
422
423            let n = nodes[7];
424            nodes[3].with_write_barrier(&mut heap, |node| node.add_child(n));
425
426            let n = nodes[8];
427            nodes[3].with_write_barrier(&mut heap, |node| node.add_child(n));
428
429            let n = nodes[9];
430            nodes[4].with_write_barrier(&mut heap, |node| node.add_child(n));
431        }
432
433        let mut root = TestNode::new(100);
434        root.add_child(nodes[0]);
435        let _ = unsafe { heap.alloc_root_raw(partition_id, root) }.unwrap();
436        while !heap.mark(partition_id, 16) {}
437        let marks1 = count_non_white_nodes(&heap, partition_id);
438
439        // Reset colors and mark again with smaller step limit
440        heap.mark_reset(partition_id);
441        while !heap.mark(partition_id, 1) {}
442        let marks2 = count_non_white_nodes(&heap, partition_id);
443
444        // Both algorithms should mark the same number of nodes
445        assert_eq!(marks1, marks2);
446        assert_eq!(marks1, 11);
447    }
448
449    /// Test 6: Circular reference handling
450    #[test]
451    fn test_trace_circular_reference() {
452        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
453        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
454
455        let mut node1 = unsafe { heap.alloc_raw(partition_id, TestNode::new(1)) }.unwrap();
456        let mut node2 = unsafe { heap.alloc_raw(partition_id, TestNode::new(2)) }.unwrap();
457
458        {
459            unsafe {
460                node1.with_write_barrier(&mut heap, |n| n.add_child(node2));
461            }
462            unsafe {
463                node2.with_write_barrier(&mut heap, |n| n.add_child(node1));
464            }
465        }
466
467        // Mark reachable nodes - should handle circular reference without infinite loop
468        let mut root = TestNode::new(100);
469        root.add_child(node1);
470        let _ = unsafe { heap.alloc_root_raw(partition_id, root) }.unwrap();
471        while !heap.mark(partition_id, 4) {}
472
473        // Both nodes should be marked
474        assert_eq!(
475            count_non_white_nodes(&heap, partition_id),
476            3,
477            "Propagate should handle circular reference"
478        );
479
480        heap.mark_reset(partition_id);
481        while !heap.mark(partition_id, 1) {}
482        assert_eq!(count_non_white_nodes(&heap, partition_id), 3);
483    }
484
485    // ============ Write barrier tests ============
486
487    /// Test that the write barrier correctly re-grays a black node when a new
488    /// white child is added during marking, preventing the child from being
489    /// incorrectly swept.
490    #[test]
491    fn test_write_barrier_black_node_adds_white_child() {
492        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
493        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
494
495        // Allocate a white child node (not reachable from root yet)
496        let child = unsafe { heap.alloc_raw(partition_id, TestNode::new(1)) }.unwrap();
497
498        // Allocate root with no children initially
499        let mut root = unsafe { heap.alloc_root_raw(partition_id, TestNode::new(0)) }.unwrap();
500
501        // Run mark until root is black but child is still white.
502        // Since child is not reachable from root, only root should be marked.
503        while !heap.mark(partition_id, 1) {}
504        assert_eq!(count_non_white_nodes(&heap, partition_id), 1);
505
506        // Now add the white child to the black root via with_mut().
507        // The write barrier should re-gray the root and enqueue it.
508        unsafe {
509            root.with_write_barrier(&mut heap, |node| node.add_child(child));
510        }
511
512        // Continue marking. The root should be traced again, discovering the child.
513        while !heap.mark(partition_id, 1) {}
514
515        // Both root and child should now be black (marked).
516        assert_eq!(
517            count_non_white_nodes(&heap, partition_id),
518            2,
519            "Write barrier should have re-grayed root and discovered child"
520        );
521
522        // Sweep should not free any nodes since all are marked.
523        let freed = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
524        assert_eq!(freed, 0, "No nodes should be freed after write barrier");
525
526        // Verify both nodes are still accessible
527        let ids = get_all_node_ids(&heap, partition_id);
528        assert!(ids.contains(&0));
529        assert!(ids.contains(&1));
530    }
531
532    /// Test that the write barrier works correctly with incremental marking:
533    /// multiple black nodes add white children across several mark steps.
534    #[test]
535    fn test_write_barrier_incremental_black_adds_white() {
536        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
537        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
538
539        // Create a chain: root -> a -> b -> c
540        let c = unsafe { heap.alloc_raw(partition_id, TestNode::new(3)) }.unwrap();
541        let mut b = unsafe { heap.alloc_raw(partition_id, TestNode::new(2)) }.unwrap();
542        unsafe {
543            b.with_write_barrier(&mut heap, |node| node.add_child(c));
544        }
545
546        let mut a = unsafe { heap.alloc_raw(partition_id, TestNode::new(1)) }.unwrap();
547        unsafe {
548            a.with_write_barrier(&mut heap, |node| node.add_child(b));
549        }
550
551        let mut root = unsafe { heap.alloc_root_raw(partition_id, TestNode::new(0)) }.unwrap();
552        unsafe {
553            root.with_write_barrier(&mut heap, |node| node.add_child(a));
554        }
555
556        // Mark partially: only 1 node per step, so root becomes black,
557        // a becomes gray, b and c stay white.
558        while !heap.mark(partition_id, 1) {}
559
560        // At this point root is black, a is black, b is gray/black, c is white.
561        // Now add a NEW white child to the black root.
562        let new_child = unsafe { heap.alloc_raw(partition_id, TestNode::new(10)) }.unwrap();
563        unsafe {
564            root.with_write_barrier(&mut heap, |node| node.add_child(new_child));
565        }
566
567        // Continue marking to completion.
568        while !heap.mark(partition_id, 1) {}
569
570        // All 5 nodes should be marked.
571        assert_eq!(
572            count_non_white_nodes(&heap, partition_id),
573            5,
574            "Write barrier should have preserved the new child added during marking"
575        );
576
577        // Sweep should not free anything.
578        let freed = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
579        assert_eq!(freed, 0);
580    }
581
582    /// Test that bypassing the write barrier (via direct payload mutation) causes
583    /// incorrect collection of a white child added to a black node during marking.
584    /// This test documents the known soundness hole when write barrier is bypassed.
585    #[test]
586    fn test_write_barrier_bypass_leaks_white_child() {
587        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
588        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
589
590        let child = unsafe { heap.alloc_raw(partition_id, TestNode::new(1)) }.unwrap();
591        let mut root = unsafe { heap.alloc_root_raw(partition_id, TestNode::new(0)) }.unwrap();
592
593        // Mark until root is black, child is white.
594        while !heap.mark(partition_id, 1) {}
595        assert_eq!(count_non_white_nodes(&heap, partition_id), 1);
596
597        // Bypass the write barrier by directly mutating the payload through
598        // the raw head_ptr. This is the unsafe pattern we want to prevent.
599        unsafe {
600            let payload = root
601                .head_ptr
602                .as_mut()
603                .payload_for::<TestNode>()
604                .cast::<TestNode>()
605                .as_mut();
606            payload.children.push(child);
607        }
608
609        // Continue marking. Since the write barrier was bypassed,
610        // root stays black and the white child is never discovered.
611        while !heap.mark(partition_id, 1) {}
612
613        // Only root should be marked; child remains white.
614        assert_eq!(
615            count_non_white_nodes(&heap, partition_id),
616            1,
617            "Without write barrier, the white child should remain unmarked"
618        );
619
620        // Sweep will free the white child (freed > 0 indicates at least one node was collected).
621        let freed = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
622        assert!(
623            freed > 0,
624            "The white child should be swept without write barrier"
625        );
626
627        // Only root should remain.
628        let ids = get_all_node_ids(&heap, partition_id);
629        assert!(ids.contains(&0));
630        assert!(!ids.contains(&1), "Child should have been collected");
631    }
632
633    // ============ High-alignment payload tests ============
634
635    #[repr(align(32))]
636    #[derive(Debug)]
637    struct Align32Node {
638        id: u64,
639        data: [u8; 64],
640    }
641
642    impl GcTrace for Align32Node {
643        fn trace(&self, _: &mut GcTraceCtx) {}
644    }
645
646    #[test]
647    fn test_high_alignment_payload_alloc_and_access() {
648        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
649        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
650
651        let node: GcRef<Align32Node> = unsafe {
652            heap.alloc_root_raw(
653                partition_id,
654                Align32Node {
655                    id: 42,
656                    data: [0xAB; 64],
657                },
658            )
659        }
660        .unwrap();
661
662        // Verify payload is accessible and values are correct
663        let n = unsafe { node.as_ref() };
664        assert_eq!(n.id, 42);
665        assert_eq!(n.data[0], 0xAB);
666        assert_eq!(n.data[63], 0xAB);
667
668        // Verify alignment via pointer arithmetic
669        let payload_ptr = unsafe { node.as_ptr() }.as_ptr() as usize;
670        assert_eq!(
671            payload_ptr % 32,
672            0,
673            "Align32Node payload must be 32-byte aligned, got offset {}",
674            payload_ptr % 32
675        );
676
677        // GC should not collect root nodes
678        while !heap.mark(partition_id, 64) {}
679        let freed = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
680        assert_eq!(freed, 0);
681
682        // Values still accessible after GC
683        assert_eq!(unsafe { node.as_ref() }.id, 42);
684    }
685
686    #[test]
687    fn test_high_alignment_payload_multiple_nodes() {
688        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
689        let partition_id = heap.create_partition(64 * 1024, 16 * 1024);
690
691        let nodes: Vec<GcRef<Align32Node>> = (0..10)
692            .map(|i| {
693                unsafe {
694                    heap.alloc_root_raw(
695                        partition_id,
696                        Align32Node {
697                            id: i as u64,
698                            data: [i as u8; 64],
699                        },
700                    )
701                }
702                .unwrap()
703            })
704            .collect();
705
706        // Verify all nodes are accessible and correctly aligned
707        for (i, node) in nodes.iter().enumerate() {
708            let n = unsafe { node.as_ref() };
709            assert_eq!(n.id, i as u64);
710            assert_eq!(n.data[0], i as u8);
711            assert_eq!(n.data[63], i as u8);
712
713            let payload_ptr = unsafe { node.as_ptr() }.as_ptr() as usize;
714            assert_eq!(
715                payload_ptr % 32,
716                0,
717                "node[{}] payload must be 32-byte aligned",
718                i
719            );
720        }
721
722        // GC should not collect root nodes
723        while !heap.mark(partition_id, 64) {}
724        let freed = heap.sweep(partition_id, GcHeap::DUMMY_DISPOSE_CALLBACK);
725        assert_eq!(freed, 0);
726
727        // All nodes still accessible after GC
728        for (i, node) in nodes.iter().enumerate() {
729            assert_eq!(unsafe { node.as_ref() }.id, i as u64);
730        }
731    }
732
733    // ── Cross-partition reference marking tests ──────────────────────────
734    //
735    // These tests verify that the GC correctly handles references between
736    // objects in different partitions during the mark phase. The key behaviors
737    // tested are:
738    //
739    // 1. When a node in partition A references a node in partition B, the
740    //    referenced node is pushed to partition B's gray_list (not A's).
741    // 2. mark_prepare seeds roots from ALL partitions, so cross-partition
742    //    root chains are discovered regardless of which partition triggers GC.
743    // 3. Cross-partition marking correctly protects reachable sub-graphs
744    //    across partition boundaries.
745
746    /// Helper: count total nodes in a partition's node chain.
747    fn count_nodes_in_partition(heap: &GcHeap, pid: GcPartitionId) -> usize {
748        heap.nodes(pid).count()
749    }
750
751    /// Test 1: Basic cross-partition reference.
752    ///
753    ///   p0: Root(0) → A(1) → B(2)
754    ///   p1:                       B(2)
755    ///
756    /// Mark from p0. B (in p1) should be pushed to p1's gray_list and
757    /// survive.
758    #[test]
759    fn test_cross_partition_basic_ref() {
760        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
761        let p0 = heap.create_partition(64 * 1024, 16 * 1024);
762        let p1 = heap.create_partition(64 * 1024, 16 * 1024);
763
764        let node_b = unsafe { heap.alloc_raw(p1, TestNode::new(2)) }.unwrap();
765
766        let mut node_a = TestNode::new(1);
767        node_a.add_child(node_b);
768        let node_a_ref = unsafe { heap.alloc_raw(p0, node_a) }.unwrap();
769
770        let mut root = TestNode::new(0);
771        root.add_child(node_a_ref);
772        let _root_ref = unsafe { heap.alloc_root_raw(p0, root) }.unwrap();
773
774        assert_eq!(count_nodes_in_partition(&heap, p0), 2);
775        assert_eq!(count_nodes_in_partition(&heap, p1), 1);
776
777        // Mark from p0 — the cross-partition reference should
778        // correctly push B to p1's gray_list.
779        while !heap.mark(p0, 16) {}
780
781        assert_eq!(
782            count_non_white_nodes(&heap, p0),
783            2,
784            "p0: Root + NodeA should be marked"
785        );
786        assert_eq!(
787            count_non_white_nodes(&heap, p1),
788            1,
789            "p1: NodeB should be marked via cross-partition push"
790        );
791
792        // Sweep p0 — only p0's White nodes are removed.
793        heap.sweep(p0, |_, _| {});
794
795        // NodeB still present in p1's node chain.
796        assert_eq!(count_nodes_in_partition(&heap, p1), 1);
797
798        // Now GC p1 to verify NodeB is properly traced and survives.
799        while !heap.mark(p1, 16) {}
800        let freed = heap.sweep(p1, |_, _| {});
801        assert_eq!(freed, 0, "p1 should have no garbage to collect");
802
803        let ids1 = get_all_node_ids(&heap, p1);
804        assert_eq!(ids1, vec![2], "NodeB should survive p1's sweep");
805    }
806
807    /// Test 2: Cross-partition chain with isolated node collected.
808    ///
809    ///   p0: Root(0) → A(1) ─────────────────┐
810    ///                                       ↓
811    ///   p1:                               B(2) → C(3) ,  D(4)[isolated]
812    ///
813    /// Mark from p0. B and C are pushed to p1's gray_list.
814    /// D (isolated, no incoming cross-ref) stays White.
815    /// After GC(p1), D should be collected; B and C survive.
816    #[test]
817    fn test_cross_partition_chain_with_garbage() {
818        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
819        let p0 = heap.create_partition(64 * 1024, 16 * 1024);
820        let p1 = heap.create_partition(64 * 1024, 16 * 1024);
821
822        let node_c = unsafe { heap.alloc_raw(p1, TestNode::new(3)) }.unwrap();
823
824        let mut node_b = TestNode::new(2);
825        node_b.add_child(node_c);
826        let node_b_ref = unsafe { heap.alloc_raw(p1, node_b) }.unwrap();
827
828        let mut node_a = TestNode::new(1);
829        node_a.add_child(node_b_ref);
830        let node_a_ref = unsafe { heap.alloc_raw(p0, node_a) }.unwrap();
831
832        let mut root = TestNode::new(0);
833        root.add_child(node_a_ref);
834        let _root_ref = unsafe { heap.alloc_root_raw(p0, root) }.unwrap();
835
836        // Isolated node in p1 — no incoming references.
837        let _node_d = unsafe { heap.alloc_raw(p1, TestNode::new(4)) }.unwrap();
838
839        assert_eq!(count_nodes_in_partition(&heap, p0), 2);
840        assert_eq!(count_nodes_in_partition(&heap, p1), 3);
841
842        // ── Mark from p0 ─────────────────────────────────────────────
843        while !heap.mark(p0, 16) {}
844
845        assert_eq!(
846            count_non_white_nodes(&heap, p0),
847            2,
848            "p0: Root + NodeA marked"
849        );
850        // Cross-partition push makes B(Gray) in p1's gray_list, but
851        // B's children (C) are not traced until p1 processes its own
852        // gray_list.
853        assert_eq!(
854            count_non_white_nodes(&heap, p1),
855            1,
856            "p1: NodeB marked via cross-partition push; C needs p1's own mark"
857        );
858
859        heap.sweep(p0, |_, _| {});
860
861        // ── GC p1 — traces B → discovers C; B + C survive, D collected
862        while !heap.mark(p1, 16) {}
863        assert_eq!(
864            count_non_white_nodes(&heap, p1),
865            2,
866            "p1: B + C both marked after p1 processes its gray_list"
867        );
868        let freed = heap.sweep(p1, |_, _| {});
869        assert!(freed > 0, "p1 should free isolated node D");
870
871        let ids1 = get_all_node_ids(&heap, p1);
872        assert!(
873            ids1.contains(&2),
874            "NodeB should survive (cross-partition protected)"
875        );
876        assert!(
877            ids1.contains(&3),
878            "NodeC should survive (transitive cross-partition protected)"
879        );
880        assert!(
881            !ids1.contains(&4),
882            "NodeD should be collected (no incoming ref)"
883        );
884    }
885
886    /// Test 3: Three-partition cascade.
887    ///
888    ///   p0: Root(0) → A(1)
889    ///                     ↓
890    ///   p1:             B(2)
891    ///                     ↓
892    ///   p2:             C(3)
893    ///
894    /// Mark from p0. The cross-partition push cascades across three
895    /// partitions correctly.
896    #[test]
897    fn test_cross_partition_three_way_cascade() {
898        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
899        let p0 = heap.create_partition(64 * 1024, 16 * 1024);
900        let p1 = heap.create_partition(64 * 1024, 16 * 1024);
901        let p2 = heap.create_partition(64 * 1024, 16 * 1024);
902
903        let node_c = unsafe { heap.alloc_raw(p2, TestNode::new(3)) }.unwrap();
904
905        let mut node_b = TestNode::new(2);
906        node_b.add_child(node_c);
907        let node_b_ref = unsafe { heap.alloc_raw(p1, node_b) }.unwrap();
908
909        let mut node_a = TestNode::new(1);
910        node_a.add_child(node_b_ref);
911        let node_a_ref = unsafe { heap.alloc_raw(p0, node_a) }.unwrap();
912
913        let mut root = TestNode::new(0);
914        root.add_child(node_a_ref);
915        let _root_ref = unsafe { heap.alloc_root_raw(p0, root) }.unwrap();
916
917        // ── Mark from p0 ─────────────────────────────────────────────
918        while !heap.mark(p0, 16) {}
919
920        assert_eq!(count_non_white_nodes(&heap, p0), 2, "p0: Root + NodeA");
921        assert_eq!(
922            count_non_white_nodes(&heap, p1),
923            1,
924            "p1: NodeB (cross-partition from A)"
925        );
926        // C is pushed to p2's gray_list but not traced until p2
927        // processes it.
928        assert_eq!(
929            count_non_white_nodes(&heap, p2),
930            0,
931            "p2: NodeC not yet traced (needs p2's mark_grays)"
932        );
933
934        // ── Cascade marks — each partition processes its gray_list ──
935        heap.sweep(p0, |_, _| {});
936        while !heap.mark(p1, 16) {}
937        heap.sweep(p1, |_, _| {});
938        while !heap.mark(p2, 16) {}
939        let freed = heap.sweep(p2, |_, _| {});
940        assert_eq!(freed, 0, "no garbage in cascade");
941
942        assert!(
943            get_all_node_ids(&heap, p2).contains(&3),
944            "NodeC should survive entire GC cascade"
945        );
946    }
947
948    /// Test 4: Bidirectional (circular) cross-partition reference.
949    ///
950    ///   p0: Root(0) → A(1) ──→ B(2)
951    ///                        ←──┘
952    ///   p1:                       B(2) ──→ A(1)
953    ///
954    /// The cycle should not cause infinite loops or false collection.
955    #[test]
956    fn test_cross_partition_bidirectional_circular() {
957        let mut heap = GcHeap::new(&GC_TYPE_REGISTRY);
958        let p0 = heap.create_partition(64 * 1024, 16 * 1024);
959        let p1 = heap.create_partition(64 * 1024, 16 * 1024);
960
961        // Build A(p0) with empty children, allocate first.
962        let node_a = TestNode::new(1);
963        let mut node_a_ref = unsafe { heap.alloc_raw(p0, node_a) }.unwrap();
964
965        // Build B(p1) with children=[A], then allocate.
966        let mut node_b = TestNode::new(2);
967        node_b.add_child(node_a_ref);
968        let node_b_ref = unsafe { heap.alloc_raw(p1, node_b) }.unwrap();
969
970        // Set up A → B via write barrier (goes through bind()).
971        unsafe {
972            node_a_ref.with_write_barrier(&mut heap, |a| {
973                a.add_child(node_b_ref);
974            });
975        }
976
977        let mut root = TestNode::new(0);
978        root.add_child(node_a_ref);
979        let _root_ref = unsafe { heap.alloc_root_raw(p0, root) }.unwrap();
980
981        assert_eq!(count_nodes_in_partition(&heap, p0), 2);
982        assert_eq!(count_nodes_in_partition(&heap, p1), 1);
983
984        // ── Mark from p0 ─────────────────────────────────────────────
985        while !heap.mark(p0, 16) {}
986
987        assert_eq!(count_non_white_nodes(&heap, p0), 2, "Root + NodeA");
988        assert_eq!(
989            count_non_white_nodes(&heap, p1),
990            1,
991            "NodeB reachable via cross-partition cycle"
992        );
993
994        // Sweep both partitions
995        heap.sweep(p0, |_, _| {});
996        while !heap.mark(p1, 16) {}
997        let freed = heap.sweep(p1, |_, _| {});
998        assert_eq!(freed, 0);
999
1000        assert!(get_all_node_ids(&heap, p0).contains(&1), "NodeA survives");
1001        assert!(get_all_node_ids(&heap, p1).contains(&2), "NodeB survives");
1002    }
1003}