use std::collections::HashSet;
use crate::gc::heap::OldSpace;
use crate::objects::heap_object::HeapObject;
pub struct MarkSweepCompactor<'heap> {
old_space: &'heap mut OldSpace,
mark_set: HashSet<usize>,
}
impl<'heap> MarkSweepCompactor<'heap> {
pub fn new(old_space: &'heap mut OldSpace) -> Self {
Self {
old_space,
mark_set: HashSet::new(),
}
}
fn is_marked(&self, ptr: *mut HeapObject) -> bool {
self.mark_set.contains(&(ptr as usize))
}
fn mark_ptr(&mut self, ptr: *mut HeapObject) {
self.mark_set.insert(ptr as usize);
}
fn clear_marks(&mut self) {
self.mark_set.clear();
}
pub unsafe fn mark(&mut self, roots: &[*mut *mut HeapObject]) {
let mut grey_stack: Vec<*mut HeapObject> = Vec::new();
for &slot in roots {
let ptr = unsafe { *slot };
if ptr.is_null() {
continue;
}
if self.old_space.contains(ptr as *mut u8) && !self.is_marked(ptr) {
self.mark_ptr(ptr);
grey_stack.push(ptr);
}
}
while let Some(_obj) = grey_stack.pop() {
}
}
pub unsafe fn sweep(&self) -> Vec<(usize, usize)> {
let base = self.old_space.base_ptr();
let used = self.old_space.used();
let mut free_regions: Vec<(usize, usize)> = Vec::new();
let mut offset = 0usize;
while offset < used {
let obj_ptr = unsafe { base.add(offset) } as *mut HeapObject;
let size = unsafe { (*obj_ptr).alloc_size() } as usize;
if size == 0 {
break;
}
if !self.is_marked(obj_ptr) {
free_regions.push((offset, size));
}
offset += size;
}
free_regions
}
pub unsafe fn compact(&mut self, roots: &mut [*mut *mut HeapObject]) {
let base = self.old_space.base_ptr();
let used = self.old_space.used();
let mut plan: Vec<(*mut HeapObject, *mut HeapObject, usize)> = Vec::new();
let mut new_offset: usize = 0;
let mut offset = 0usize;
while offset < used {
let old_ptr = unsafe { base.add(offset) } as *mut HeapObject;
let size = unsafe { (*old_ptr).alloc_size() } as usize;
if size == 0 {
break;
}
if self.is_marked(old_ptr) {
let new_ptr = unsafe { base.add(new_offset) } as *mut HeapObject;
plan.push((old_ptr, new_ptr, size));
new_offset += size;
}
offset += size;
}
for slot in roots.iter_mut() {
let old_ptr = unsafe { **slot };
if old_ptr.is_null() {
continue;
}
if let Some(&(_, new_ptr, _)) = plan.iter().find(|(o, _, _)| *o == old_ptr) {
unsafe { **slot = new_ptr };
}
}
for &(old_ptr, new_ptr, size) in &plan {
if old_ptr != new_ptr {
unsafe {
std::ptr::copy(old_ptr as *const u8, new_ptr as *mut u8, size);
}
}
}
self.old_space.force_used(new_offset);
self.clear_marks();
}
pub unsafe fn collect(&mut self, roots: &mut [*mut *mut HeapObject]) {
unsafe { self.mark(roots) };
let _free_regions = unsafe { self.sweep() };
unsafe { self.compact(roots) };
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gc::heap::OldSpace;
use crate::objects::heap_object::HeapObject;
use std::alloc::Layout;
use std::mem::align_of;
fn alloc_in_old(old: &mut OldSpace) -> *mut HeapObject {
let layout = Layout::new::<HeapObject>();
let raw = old.bump_alloc(layout).expect("old space has free space");
unsafe { std::ptr::write_bytes(raw, 0, layout.size()) };
let obj = raw as *mut HeapObject;
unsafe { (*obj).init_alloc_size(layout.size() as u32) };
obj
}
fn alloc_sized_in_old(old: &mut OldSpace, extra: usize) -> *mut HeapObject {
let base_size = std::mem::size_of::<HeapObject>();
let total = base_size + extra;
let layout = Layout::from_size_align(total, align_of::<HeapObject>())
.expect("valid layout")
.pad_to_align();
let raw = old.bump_alloc(layout).expect("old space has free space");
unsafe { std::ptr::write_bytes(raw, 0, layout.size()) };
let obj = raw as *mut HeapObject;
unsafe { (*obj).init_alloc_size(layout.size() as u32) };
obj
}
#[test]
fn test_mark_marks_rooted_object() {
let mut old = OldSpace::new(65536);
let obj = alloc_in_old(&mut old);
let mut root: *mut HeapObject = obj;
let mut msc = MarkSweepCompactor::new(&mut old);
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { msc.mark(&roots) };
assert!(msc.is_marked(obj), "rooted old-space object must be marked");
}
#[test]
fn test_mark_does_not_mark_null_root() {
let mut old = OldSpace::new(65536);
let mut root: *mut HeapObject = std::ptr::null_mut();
let mut msc = MarkSweepCompactor::new(&mut old);
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { msc.mark(&roots) };
assert!(msc.mark_set.is_empty(), "null root must not add any marks");
}
#[test]
fn test_mark_ignores_non_old_space_pointer() {
let mut old = OldSpace::new(65536);
let mut stack_obj = HeapObject::new_null();
let stack_ptr: *mut HeapObject = &raw mut stack_obj;
let mut root: *mut HeapObject = stack_ptr;
let mut msc = MarkSweepCompactor::new(&mut old);
let roots = [&raw mut root as *mut *mut HeapObject];
unsafe { msc.mark(&roots) };
assert!(
msc.mark_set.is_empty(),
"pointer outside old-space must not be marked"
);
}
#[test]
fn test_sweep_returns_free_region_for_unmarked_object() {
let mut old = OldSpace::new(65536);
let obj = alloc_in_old(&mut old);
let msc = MarkSweepCompactor::new(&mut old);
let free_regions = unsafe { msc.sweep() };
assert_eq!(free_regions.len(), 1, "one dead object → one free region");
let (offset, size) = free_regions[0];
assert_eq!(offset, 0, "dead object must be at offset 0");
let expected_size = unsafe { (*obj).alloc_size() } as usize;
assert_eq!(
size, expected_size,
"free region size must match alloc_size"
);
}
#[test]
fn test_sweep_returns_no_free_regions_when_all_marked() {
let mut old = OldSpace::new(65536);
let obj = alloc_in_old(&mut old);
let mut msc = MarkSweepCompactor::new(&mut old);
msc.mark_ptr(obj);
let free_regions = unsafe { msc.sweep() };
assert!(
free_regions.is_empty(),
"marked object must not appear in the free-region list"
);
}
#[test]
fn test_sweep_mixed_live_dead() {
let mut old = OldSpace::new(65536);
let live = alloc_in_old(&mut old);
let _dead = alloc_in_old(&mut old);
let live2 = alloc_in_old(&mut old);
let mut msc = MarkSweepCompactor::new(&mut old);
msc.mark_ptr(live);
msc.mark_ptr(live2);
let free_regions = unsafe { msc.sweep() };
assert_eq!(
free_regions.len(),
1,
"exactly one dead object → one free region"
);
}
#[test]
fn test_compact_single_live_object_updates_used() {
let mut old = OldSpace::new(65536);
let obj = alloc_in_old(&mut old);
let obj_size = unsafe { (*obj).alloc_size() } as usize;
let _dead = alloc_in_old(&mut old);
let mut msc = MarkSweepCompactor::new(&mut old);
msc.mark_ptr(obj);
let mut root: *mut HeapObject = obj;
let mut roots = [&raw mut root as *mut *mut HeapObject];
unsafe { msc.compact(&mut roots) };
assert_eq!(
old.used(),
obj_size,
"used must equal exactly one live object's size after compaction"
);
}
#[test]
fn test_compact_no_live_objects_zeros_used() {
let mut old = OldSpace::new(65536);
let _dead1 = alloc_in_old(&mut old);
let _dead2 = alloc_in_old(&mut old);
let mut msc = MarkSweepCompactor::new(&mut old);
let mut roots: [*mut *mut HeapObject; 0] = [];
unsafe { msc.compact(&mut roots) };
assert_eq!(
old.used(),
0,
"used must be 0 after compacting all-dead space"
);
}
#[test]
fn test_compact_updates_root_slot() {
let mut old = OldSpace::new(65536);
let _dead = alloc_in_old(&mut old);
let live = alloc_in_old(&mut old);
let original_addr = live as usize;
let mut msc = MarkSweepCompactor::new(&mut old);
msc.mark_ptr(live);
let mut root: *mut HeapObject = live;
let mut roots = [&raw mut root as *mut *mut HeapObject];
unsafe { msc.compact(&mut roots) };
let new_addr = root as usize;
assert_ne!(
new_addr, original_addr,
"root must point to the relocated (compacted) address"
);
let base = old.base_ptr() as usize;
assert_eq!(new_addr, base, "single survivor must be at the region base");
}
#[test]
fn test_collect_old_space_object_survives() {
let mut old = OldSpace::new(65536);
let obj = alloc_in_old(&mut old);
let obj_size = unsafe { (*obj).alloc_size() } as usize;
let mut root: *mut HeapObject = obj;
let mut roots = [&raw mut root as *mut *mut HeapObject];
unsafe { MarkSweepCompactor::new(&mut old).collect(&mut roots) };
assert!(
!root.is_null(),
"root must remain non-null after a full MSC cycle"
);
assert_eq!(
old.used(),
obj_size,
"used must equal the surviving object's size"
);
}
#[test]
fn test_collect_unrooted_object_is_reclaimed() {
let mut old = OldSpace::new(65536);
let live = alloc_in_old(&mut old);
let live_size = unsafe { (*live).alloc_size() } as usize;
let _dead = alloc_in_old(&mut old);
let mut root: *mut HeapObject = live;
let mut roots = [&raw mut root as *mut *mut HeapObject];
unsafe { MarkSweepCompactor::new(&mut old).collect(&mut roots) };
assert_eq!(
old.used(),
live_size,
"dead object must have been reclaimed; only the live object remains"
);
}
#[test]
fn test_collect_fragmentation_is_eliminated() {
let mut old = OldSpace::new(65536);
let obj0 = alloc_sized_in_old(&mut old, 0);
let _obj1 = alloc_sized_in_old(&mut old, 0); let obj2 = alloc_sized_in_old(&mut old, 0);
let _obj3 = alloc_sized_in_old(&mut old, 0);
let live_size =
(unsafe { (*obj0).alloc_size() } + unsafe { (*obj2).alloc_size() }) as usize;
let mut root0: *mut HeapObject = obj0;
let mut root2: *mut HeapObject = obj2;
let mut roots = [
&raw mut root0 as *mut *mut HeapObject,
&raw mut root2 as *mut *mut HeapObject,
];
unsafe { MarkSweepCompactor::new(&mut old).collect(&mut roots) };
assert_eq!(
old.used(),
live_size,
"after compaction, used must equal the total size of the two survivors"
);
let base = old.base_ptr() as usize;
let first_size = unsafe { (*root0).alloc_size() } as usize;
assert_eq!(
root0 as usize, base,
"first survivor must be at the region base after compaction"
);
assert_eq!(
root2 as usize,
base + first_size,
"second survivor must immediately follow the first"
);
}
#[test]
fn test_collect_multiple_cycles_consistent() {
let mut old = OldSpace::new(65536);
let obj = alloc_in_old(&mut old);
let obj_size = unsafe { (*obj).alloc_size() } as usize;
let mut root: *mut HeapObject = obj;
for _ in 0..3 {
let mut roots = [&raw mut root as *mut *mut HeapObject];
unsafe { MarkSweepCompactor::new(&mut old).collect(&mut roots) };
assert!(!root.is_null(), "root must survive every collect cycle");
assert_eq!(
old.used(),
obj_size,
"used must be stable across repeated cycles with the same root"
);
}
}
}