use std::any::{Any, TypeId};
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Default, Clone, Copy)]
pub struct HeapStats {
pub allocations: u64,
pub reclaimed: u64,
pub live: u64,
pub bytes_allocated: u64,
pub bytes_live: u64,
}
static GLOBAL_ALLOC_COUNT: AtomicU64 = AtomicU64::new(0);
#[must_use]
pub fn global_alloc_count() -> u64 {
GLOBAL_ALLOC_COUNT.load(Ordering::Relaxed)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct HeapIndex {
index: u32,
generation: u32,
type_id: TypeId,
}
impl HeapIndex {
#[must_use]
pub const fn index(self) -> u32 {
self.index
}
#[must_use]
pub const fn generation(self) -> u32 {
self.generation
}
}
struct HeapEntry {
value: Box<dyn Any + Send + Sync>,
generation: u32,
size_hint: usize,
}
enum HeapSlot {
Occupied(HeapEntry),
Vacant {
next_free: Option<u32>,
generation: u32,
},
}
#[derive(Default)]
pub struct RegionHeap {
slots: Vec<HeapSlot>,
free_head: Option<u32>,
len: usize,
stats: HeapStats,
}
impl std::fmt::Debug for RegionHeap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegionHeap")
.field("len", &self.len)
.field("stats", &self.stats)
.finish_non_exhaustive()
}
}
impl RegionHeap {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
slots: Vec::with_capacity(capacity),
free_head: None,
len: 0,
stats: HeapStats::default(),
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.len
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
pub const fn stats(&self) -> HeapStats {
self.stats
}
pub fn alloc<T: Send + Sync + 'static>(&mut self, value: T) -> HeapIndex {
let size_hint = std::mem::size_of::<T>();
let type_id = TypeId::of::<T>();
let entry_value: Box<dyn Any + Send + Sync> = Box::new(value);
let heap_index = if let Some(free_index) = self.free_head {
let Some(slot) = self.slots.get_mut(free_index as usize) else {
unreachable!("free list pointed outside heap slots");
};
match slot {
HeapSlot::Vacant {
next_free,
generation,
} => {
let generation_value = *generation;
self.free_head = *next_free;
*slot = HeapSlot::Occupied(HeapEntry {
value: entry_value,
generation: generation_value,
size_hint,
});
HeapIndex {
index: free_index,
generation: generation_value,
type_id,
}
}
HeapSlot::Occupied(_) => unreachable!("free list pointed to occupied slot"),
}
} else {
let index = u32::try_from(self.slots.len()).expect("region heap overflow");
self.slots.push(HeapSlot::Occupied(HeapEntry {
value: entry_value,
generation: 0,
size_hint,
}));
HeapIndex {
index,
generation: 0,
type_id,
}
};
self.len += 1;
self.stats.allocations += 1;
self.stats.live += 1;
self.stats.bytes_allocated += size_hint as u64;
self.stats.bytes_live += size_hint as u64;
GLOBAL_ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
heap_index
}
#[must_use]
pub fn get<T: 'static>(&self, index: HeapIndex) -> Option<&T> {
if TypeId::of::<T>() != index.type_id {
return None;
}
match self.slots.get(index.index as usize)? {
HeapSlot::Occupied(entry) if entry.generation == index.generation => {
entry.value.downcast_ref::<T>()
}
_ => None,
}
}
pub fn get_mut<T: 'static>(&mut self, index: HeapIndex) -> Option<&mut T> {
if TypeId::of::<T>() != index.type_id {
return None;
}
match self.slots.get_mut(index.index as usize)? {
HeapSlot::Occupied(entry) if entry.generation == index.generation => {
entry.value.downcast_mut::<T>()
}
_ => None,
}
}
#[must_use]
pub fn contains(&self, index: HeapIndex) -> bool {
match self.slots.get(index.index as usize) {
Some(HeapSlot::Occupied(entry)) => entry.generation == index.generation,
_ => false,
}
}
pub fn dealloc(&mut self, index: HeapIndex) -> bool {
let Some(slot) = self.slots.get_mut(index.index as usize) else {
return false;
};
let (size_hint, new_gen) = {
let HeapSlot::Occupied(entry) = slot else {
return false;
};
if entry.generation != index.generation {
return false;
}
(entry.size_hint, entry.generation.wrapping_add(1))
};
let old_slot = std::mem::replace(
slot,
HeapSlot::Vacant {
next_free: self.free_head,
generation: new_gen,
},
);
self.free_head = Some(index.index);
self.len -= 1;
self.stats.reclaimed += 1;
self.stats.live -= 1;
self.stats.bytes_live = self.stats.bytes_live.saturating_sub(size_hint as u64);
GLOBAL_ALLOC_COUNT.fetch_sub(1, Ordering::Relaxed);
drop(old_slot);
true
}
pub fn reclaim_all(&mut self) {
let reclaimed_count = self.len as u64;
GLOBAL_ALLOC_COUNT.fetch_sub(reclaimed_count, Ordering::Relaxed);
self.stats.reclaimed += reclaimed_count;
self.stats.live = 0;
self.stats.bytes_live = 0;
self.free_head = None;
self.len = 0; self.slots.clear();
}
}
impl Drop for RegionHeap {
fn drop(&mut self) {
let live = self.len as u64;
if live > 0 {
GLOBAL_ALLOC_COUNT.fetch_sub(live, Ordering::Relaxed);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct HeapRef<T> {
index: HeapIndex,
_marker: std::marker::PhantomData<T>,
}
impl<T: Send + Sync + 'static> HeapRef<T> {
#[must_use]
pub const fn new(index: HeapIndex) -> Self {
Self {
index,
_marker: std::marker::PhantomData,
}
}
#[must_use]
pub const fn index(&self) -> HeapIndex {
self.index
}
#[must_use]
pub fn get<'a>(&self, heap: &'a RegionHeap) -> Option<&'a T> {
heap.get::<T>(self.index)
}
pub fn get_mut<'a>(&self, heap: &'a mut RegionHeap) -> Option<&'a mut T> {
heap.get_mut::<T>(self.index)
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::pedantic,
clippy::nursery,
clippy::expect_fun_call,
clippy::map_unwrap_or,
clippy::cast_possible_wrap,
clippy::future_not_send
)]
use super::*;
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
#[derive(Clone, Default)]
struct DropLog(Arc<Mutex<Vec<&'static str>>>);
impl DropLog {
fn push(&self, label: &'static str) {
self.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.push(label);
}
fn snapshot(&self) -> Vec<&'static str> {
self.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
}
struct DropProbe {
label: &'static str,
log: DropLog,
}
impl DropProbe {
fn new(label: &'static str, log: DropLog) -> Self {
Self { label, log }
}
}
impl Drop for DropProbe {
fn drop(&mut self) {
self.log.push(self.label);
}
}
fn close_suffix_after_early_deallocs(
first: &'static str,
second: &'static str,
) -> Vec<&'static str> {
let log = DropLog::default();
let mut heap = RegionHeap::new();
let a = heap.alloc(DropProbe::new("a", log.clone()));
let b = heap.alloc(DropProbe::new("b", log.clone()));
let c = heap.alloc(DropProbe::new("c", log.clone()));
let d = heap.alloc(DropProbe::new("d", log.clone()));
for label in [first, second] {
let deallocated = match label {
"a" => heap.dealloc(a),
"b" => heap.dealloc(b),
"c" => heap.dealloc(c),
"d" => heap.dealloc(d),
_ => panic!("unexpected label"),
};
assert!(deallocated, "{label} should deallocate successfully");
}
let dealloc_drops = log.snapshot();
assert_eq!(dealloc_drops.len(), 2, "expected two eager drops");
heap.reclaim_all();
log.snapshot()[dealloc_drops.len()..].to_vec()
}
fn independent_heap_drop_traces(
reclaim_order: [usize; 2],
) -> (Vec<&'static str>, Vec<&'static str>) {
let left_log = DropLog::default();
let right_log = DropLog::default();
let mut left = RegionHeap::new();
left.alloc(DropProbe::new("left-a", left_log.clone()));
left.alloc(DropProbe::new("left-b", left_log.clone()));
let mut right = RegionHeap::new();
right.alloc(DropProbe::new("right-a", right_log.clone()));
right.alloc(DropProbe::new("right-b", right_log.clone()));
for which in reclaim_order {
match which {
0 => left.reclaim_all(),
1 => right.reclaim_all(),
_ => panic!("unexpected heap selector"),
}
}
(left_log.snapshot(), right_log.snapshot())
}
#[test]
fn alloc_and_get() {
let mut heap = RegionHeap::new();
let idx = heap.alloc(42u32);
assert_eq!(heap.get::<u32>(idx), Some(&42));
assert_eq!(heap.len(), 1);
assert_eq!(heap.stats().allocations, 1);
assert_eq!(heap.stats().live, 1);
}
#[test]
fn multiple_types() {
let mut heap = RegionHeap::new();
let idx1 = heap.alloc(42u32);
let idx2 = heap.alloc("hello".to_string());
let idx3 = heap.alloc(vec![1, 2, 3]);
assert_eq!(heap.get::<u32>(idx1), Some(&42));
assert_eq!(heap.get::<String>(idx2).map(String::as_str), Some("hello"));
assert_eq!(heap.get::<Vec<i32>>(idx3), Some(&vec![1, 2, 3]));
assert_eq!(heap.get::<String>(idx1), None);
assert_eq!(heap.get::<u32>(idx2), None);
}
#[test]
fn dealloc_and_reuse() {
let mut heap = RegionHeap::new();
let idx1 = heap.alloc(1u32);
let idx2 = heap.alloc(2u32);
assert!(heap.dealloc(idx1));
assert_eq!(heap.len(), 1);
assert_eq!(heap.stats().live, 1);
assert_eq!(heap.stats().reclaimed, 1);
assert_eq!(heap.get::<u32>(idx1), None);
let idx3 = heap.alloc(3u32);
assert_eq!(idx3.index(), idx1.index());
assert_ne!(idx3.generation(), idx1.generation());
assert_eq!(heap.get::<u32>(idx2), Some(&2));
assert_eq!(heap.get::<u32>(idx3), Some(&3));
}
#[test]
fn generation_prevents_aba() {
let mut heap = RegionHeap::new();
let idx1 = heap.alloc(1u32);
heap.dealloc(idx1);
let idx2 = heap.alloc(2u32);
assert_eq!(idx1.index(), idx2.index());
assert_ne!(idx1.generation(), idx2.generation());
assert_eq!(heap.get::<u32>(idx1), None);
assert_eq!(heap.get::<u32>(idx2), Some(&2));
}
#[test]
fn generation_monotonic_on_reuse() {
let mut heap = RegionHeap::new();
let mut idx = heap.alloc(0u32);
for i in 1u32..16 {
assert!(heap.dealloc(idx));
let next = heap.alloc(i);
assert_eq!(next.index(), idx.index());
assert_eq!(next.generation(), idx.generation().wrapping_add(1));
assert_eq!(heap.get::<u32>(idx), None);
idx = next;
}
}
#[test]
fn deterministic_reuse_pattern() {
fn run_pattern() -> Vec<(u32, u32)> {
let mut heap = RegionHeap::new();
let first = heap.alloc(1u32);
let second = heap.alloc(2u32);
let third = heap.alloc(3u32);
assert!(heap.dealloc(second));
let reuse_second = heap.alloc(4u32);
assert!(heap.dealloc(first));
assert!(heap.dealloc(third));
let reuse_third = heap.alloc(5u32); let reuse_first = heap.alloc(6u32);
vec![
(first.index(), first.generation()),
(second.index(), second.generation()),
(third.index(), third.generation()),
(reuse_second.index(), reuse_second.generation()),
(reuse_third.index(), reuse_third.generation()),
(reuse_first.index(), reuse_first.generation()),
]
}
let first = run_pattern();
let second = run_pattern();
assert_eq!(first, second, "allocation pattern should be deterministic");
}
#[test]
fn mr_early_dealloc_permutation_preserves_close_drop_suffix() {
let forward = close_suffix_after_early_deallocs("b", "d");
let reverse = close_suffix_after_early_deallocs("d", "b");
assert_eq!(forward, reverse);
assert_eq!(forward, vec!["a", "c"]);
}
#[test]
fn mr_independent_heap_reclaim_order_preserves_per_heap_drop_traces() {
let forward = independent_heap_drop_traces([0, 1]);
let reverse = independent_heap_drop_traces([1, 0]);
assert_eq!(forward.0, reverse.0);
assert_eq!(forward.1, reverse.1);
assert_eq!(forward.0, vec!["left-a", "left-b"]);
assert_eq!(forward.1, vec!["right-a", "right-b"]);
}
#[test]
fn alloc_panic_does_not_mutate_len_or_stats() {
let mut heap = RegionHeap::new();
heap.free_head = Some(1);
let before_len = heap.len();
let before_stats = heap.stats();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = heap.alloc(123u32);
}));
assert!(
result.is_err(),
"alloc should panic on corrupted free-list head"
);
assert_eq!(heap.len(), before_len);
let after_stats = heap.stats();
assert_eq!(after_stats.allocations, before_stats.allocations);
assert_eq!(after_stats.reclaimed, before_stats.reclaimed);
assert_eq!(after_stats.live, before_stats.live);
assert_eq!(after_stats.bytes_allocated, before_stats.bytes_allocated);
assert_eq!(after_stats.bytes_live, before_stats.bytes_live);
}
#[test]
fn reclaim_all() {
let mut heap = RegionHeap::new();
heap.alloc(1u32);
heap.alloc(2u32);
heap.alloc(3u32);
assert_eq!(heap.len(), 3);
assert_eq!(heap.stats().live, 3);
heap.reclaim_all();
assert_eq!(heap.len(), 0);
assert!(heap.is_empty());
assert_eq!(heap.stats().live, 0);
assert_eq!(heap.stats().reclaimed, 3);
}
#[test]
fn stats_tracking() {
let mut heap = RegionHeap::new();
heap.alloc(42u32);
heap.alloc("hello".to_string());
let stats = heap.stats();
assert_eq!(stats.allocations, 2);
assert_eq!(stats.live, 2);
assert_eq!(stats.reclaimed, 0);
heap.dealloc(HeapIndex {
index: 0,
generation: 0,
type_id: TypeId::of::<u32>(),
});
let stats = heap.stats();
assert_eq!(stats.allocations, 2);
assert_eq!(stats.live, 1);
assert_eq!(stats.reclaimed, 1);
}
#[test]
fn heap_ref_typed_access() {
let mut heap = RegionHeap::new();
let idx = heap.alloc(42u32);
let href: HeapRef<u32> = HeapRef::new(idx);
assert_eq!(href.get(&heap), Some(&42));
*href.get_mut(&mut heap).unwrap() = 100;
assert_eq!(href.get(&heap), Some(&100));
}
#[test]
fn drop_reclaims_memory() {
let mut heap = RegionHeap::new();
for i in 0u64..100 {
heap.alloc(i);
}
assert_eq!(heap.len(), 100);
assert_eq!(heap.stats().live, 100);
assert_eq!(heap.stats().allocations, 100);
assert_eq!(heap.stats().reclaimed, 0);
}
#[test]
fn heap_stats_debug_default_clone_copy() {
let stats = HeapStats::default();
assert_eq!(stats.allocations, 0);
assert_eq!(stats.reclaimed, 0);
assert_eq!(stats.live, 0);
assert_eq!(stats.bytes_allocated, 0);
assert_eq!(stats.bytes_live, 0);
let dbg = format!("{stats:?}");
assert!(dbg.contains("HeapStats"), "{dbg}");
let copied = stats;
let cloned = stats;
assert_eq!(format!("{copied:?}"), format!("{cloned:?}"));
}
#[test]
fn heap_index_debug_clone_copy_eq_hash() {
use std::collections::HashSet;
let mut heap = RegionHeap::new();
let idx1 = heap.alloc(42u32);
let idx2 = heap.alloc(99u32);
let dbg = format!("{idx1:?}");
assert!(dbg.contains("HeapIndex"), "{dbg}");
let copied = idx1;
let cloned = idx1;
assert_eq!(copied, cloned);
assert_eq!(idx1, idx1);
assert_ne!(idx1, idx2);
let mut set = HashSet::new();
set.insert(idx1);
set.insert(idx2);
set.insert(idx1);
assert_eq!(set.len(), 2);
assert_eq!(idx1.index(), 0);
assert_eq!(idx1.generation(), 0);
}
#[test]
fn heap_ref_debug_clone_copy_eq_hash() {
use std::collections::HashSet;
let mut heap = RegionHeap::new();
let idx1 = heap.alloc(42u32);
let idx2 = heap.alloc(99u32);
let r1 = HeapRef::<u32>::new(idx1);
let r2 = HeapRef::<u32>::new(idx2);
let dbg = format!("{r1:?}");
assert!(dbg.contains("HeapRef"), "{dbg}");
let copied = r1;
let cloned = r1;
assert_eq!(copied, cloned);
assert_eq!(r1, r1);
assert_ne!(r1, r2);
let mut set = HashSet::new();
set.insert(r1);
set.insert(r2);
set.insert(r1);
assert_eq!(set.len(), 2);
assert_eq!(r1.get(&heap), Some(&42));
assert_eq!(r2.get(&heap), Some(&99));
}
#[test]
fn region_heap_debug_default() {
let heap = RegionHeap::default();
let dbg = format!("{heap:?}");
assert!(dbg.contains("RegionHeap"), "{dbg}");
assert_eq!(heap.len(), 0);
assert!(heap.is_empty());
}
use proptest::prelude::*;
prop_compose! {
fn arb_allocation_sequence()
(size in 1usize..50)
(allocations in prop::collection::vec(0u64..1000, size)) -> Vec<u64> {
allocations
}
}
prop_compose! {
fn arb_mixed_types()
(nums in prop::collection::vec(0u32..100, 0..20),
strs in prop::collection::vec("[a-z]{1,10}", 0..20),
vecs in prop::collection::vec(prop::collection::vec(0i32..10, 0..5), 0..20))
-> (Vec<u32>, Vec<String>, Vec<Vec<i32>>) {
(nums, strs, vecs)
}
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug)]
enum HeapOp {
AllocU32(u32),
AllocString(usize), Dealloc(usize), }
prop_compose! {
fn arb_heap_operations()
(alloc_ops in 5usize..30,
dealloc_rate in 0.1f64..0.7)
(ops in prop::collection::vec(
prop_oneof![
(0u32..100).prop_map(HeapOp::AllocU32),
(0usize..20).prop_map(HeapOp::AllocString),
prop::strategy::Just(()).prop_flat_map(move |_|
if fastrand::f64() < dealloc_rate {
(0usize..alloc_ops).prop_map(HeapOp::Dealloc).boxed()
} else {
prop::strategy::Just(HeapOp::AllocU32(fastrand::u32(..100))).boxed()
}
)
],
alloc_ops
)) -> Vec<HeapOp> {
ops
}
}
proptest! {
#[test]
fn mr_live_count_conservation(sequence in arb_allocation_sequence()) {
let mut heap = RegionHeap::new();
let mut allocation_indices = Vec::new();
let mut deallocated_count = 0;
for value in &sequence {
let idx = heap.alloc(*value);
allocation_indices.push(idx);
}
let initial_allocs = sequence.len();
prop_assert_eq!(heap.len(), initial_allocs);
prop_assert_eq!(heap.stats().live as usize, initial_allocs);
let dealloc_indices = if allocation_indices.len() >= 2 {
vec![allocation_indices[0], allocation_indices[allocation_indices.len() / 2]]
} else {
vec![]
};
for idx in dealloc_indices {
if heap.dealloc(idx) {
deallocated_count += 1;
}
}
let expected_live = initial_allocs - deallocated_count;
prop_assert_eq!(heap.len(), expected_live);
prop_assert_eq!(heap.stats().live as usize, expected_live);
prop_assert_eq!(heap.stats().reclaimed as usize, deallocated_count);
heap.reclaim_all();
prop_assert_eq!(heap.len(), 0);
prop_assert_eq!(heap.stats().live, 0);
prop_assert_eq!(heap.stats().reclaimed as usize, initial_allocs);
}
}
proptest! {
#[test]
fn mr_generation_isolation(values in prop::collection::vec(0u32..1000, 3..10)) {
let mut heap = RegionHeap::new();
let mut stale_indices = Vec::new();
for value in &values {
let idx = heap.alloc(*value);
stale_indices.push(idx);
}
for idx in &stale_indices {
prop_assert!(heap.dealloc(*idx));
}
let mut fresh_indices = Vec::new();
for value in &values {
let idx = heap.alloc(value + 1000); fresh_indices.push(idx);
}
for (i, stale_idx) in stale_indices.iter().enumerate() {
prop_assert_eq!(heap.get::<u32>(*stale_idx), None,
"Stale index {} should not access fresh value", i);
prop_assert!(!heap.contains(*stale_idx),
"Stale index {} should not be contained", i);
}
let stale_generation_by_slot: BTreeMap<u32, u32> = stale_indices
.iter()
.map(|idx| (idx.index(), idx.generation()))
.collect();
for (i, fresh_idx) in fresh_indices.iter().enumerate() {
prop_assert_eq!(heap.get::<u32>(*fresh_idx), Some(&(values[i] + 1000)));
prop_assert!(heap.contains(*fresh_idx));
let stale_generation = stale_generation_by_slot
.get(&fresh_idx.index())
.copied()
.expect("fresh allocation should reuse a stale slot");
prop_assert_eq!(
fresh_idx.generation(),
stale_generation.wrapping_add(1)
);
}
}
}
proptest! {
#[test]
fn mr_type_isolation(mixed_data in arb_mixed_types()) {
let (nums, strs, vecs) = mixed_data;
let mut heap = RegionHeap::new();
let mut u32_indices = Vec::new();
let mut str_indices = Vec::new();
let mut vec_indices = Vec::new();
for num in &nums {
u32_indices.push(heap.alloc(*num));
}
for s in &strs {
str_indices.push(heap.alloc(s.clone()));
}
for v in &vecs {
vec_indices.push(heap.alloc(v.clone()));
}
let u32_deallocs = if u32_indices.len() >= 2 {
vec![u32_indices[0], u32_indices[u32_indices.len() / 2]]
} else {
vec![]
};
for idx in u32_deallocs {
heap.dealloc(idx);
}
for (i, idx) in str_indices.iter().enumerate() {
prop_assert_eq!(heap.get::<String>(*idx).map(String::as_str), Some(strs[i].as_str()),
"String {} accessibility affected by u32 operations", i);
}
for (i, idx) in vec_indices.iter().enumerate() {
prop_assert_eq!(heap.get::<Vec<i32>>(*idx), Some(&vecs[i]),
"Vec {} accessibility affected by u32 operations", i);
}
for idx in &str_indices {
prop_assert_eq!(heap.get::<u32>(*idx), None);
prop_assert_eq!(heap.get::<Vec<i32>>(*idx), None);
}
}
}
proptest! {
#[test]
fn mr_free_reuse_determinism(values in prop::collection::vec(0u32..100, 5..15)) {
let mut heap = RegionHeap::new();
let mut indices = Vec::new();
for val in &values {
indices.push(heap.alloc(*val));
}
for (i, &original_val) in values.iter().enumerate() {
let original_idx = indices[i];
prop_assert!(heap.dealloc(original_idx));
let new_val = original_val + 1000;
let reused_idx = heap.alloc(new_val);
prop_assert_eq!(reused_idx.index(), original_idx.index(),
"Failed to reuse slot {} for position {}", original_idx.index(), i);
prop_assert_eq!(reused_idx.generation(), original_idx.generation().wrapping_add(1),
"Generation not incremented correctly for slot {}", original_idx.index());
prop_assert_eq!(heap.get::<u32>(original_idx), None);
prop_assert_eq!(heap.get::<u32>(reused_idx), Some(&new_val));
indices[i] = reused_idx;
}
}
}
proptest! {
#[test]
fn mr_allocation_count_linearity(
first_batch in prop::collection::vec(0u32..100, 5..20),
second_batch in prop::collection::vec(100u32..200, 3..15)
) {
let mut heap = RegionHeap::new();
let initial_stats = heap.stats();
for val in &first_batch {
heap.alloc(*val);
}
let after_first = heap.stats();
prop_assert_eq!(after_first.allocations,
initial_stats.allocations + first_batch.len() as u64);
for val in &second_batch {
heap.alloc(*val);
}
let after_second = heap.stats();
prop_assert_eq!(after_second.allocations,
initial_stats.allocations + (first_batch.len() + second_batch.len()) as u64);
prop_assert_eq!(after_second.live, after_second.allocations);
}
}
proptest! {
#[test]
fn mr_deallocation_order_independence(values in prop::collection::vec(0u32..50, 4..8)) {
fn run_with_dealloc_order(values: &[u32], first: usize, second: usize) -> (HeapStats, Vec<bool>) {
let mut heap = RegionHeap::new();
let indices: Vec<_> = values.iter().map(|&v| heap.alloc(v)).collect();
let dealloc_results = vec![
heap.dealloc(indices[first]),
heap.dealloc(indices[second]),
];
(heap.stats(), dealloc_results)
}
if values.len() >= 4 {
let forward = run_with_dealloc_order(&values, 1, 3);
let reverse = run_with_dealloc_order(&values, 3, 1);
prop_assert_eq!(forward.0.live, reverse.0.live);
prop_assert_eq!(forward.0.reclaimed, reverse.0.reclaimed);
prop_assert_eq!(forward.0.allocations, reverse.0.allocations);
prop_assert_eq!(forward.1, vec![true, true]);
prop_assert_eq!(reverse.1, vec![true, true]);
}
}
}
proptest! {
#[test]
fn mr_composite_alloc_dealloc_commutativity(
base_values in prop::collection::vec(0u32..50, 3..10),
extra_values in prop::collection::vec(100u32..150, 2..5)
) {
let mut heap1 = RegionHeap::new();
let mut heap2 = RegionHeap::new();
let mut indices1 = Vec::new();
for val in &base_values { indices1.push(heap1.alloc(*val)); }
for val in &extra_values { indices1.push(heap1.alloc(*val)); }
if indices1.len() >= 3 {
heap1.dealloc(indices1[1]);
heap1.dealloc(indices1[indices1.len() - 2]);
}
let mut indices2 = Vec::new();
for val in &extra_values { indices2.push(heap2.alloc(*val)); }
for val in &base_values { indices2.push(heap2.alloc(*val)); }
if indices2.len() >= 3 {
heap2.dealloc(indices2[indices2.len() - 2]);
heap2.dealloc(indices2[1]);
}
prop_assert_eq!(heap1.len(), heap2.len());
prop_assert_eq!(heap1.stats().live, heap2.stats().live);
prop_assert_eq!(heap1.stats().allocations, heap2.stats().allocations);
prop_assert_eq!(heap1.stats().reclaimed, heap2.stats().reclaimed);
let all_values: Vec<u32> = base_values.iter().chain(extra_values.iter()).copied().collect();
prop_assert_eq!(all_values.len(), heap1.stats().allocations as usize);
}
}
#[test]
fn validate_mr_suite_catches_planted_bugs() {
{
let mut heap = RegionHeap::new();
heap.alloc(42u32);
assert_eq!(heap.stats().allocations, 1);
assert_eq!(heap.len(), 1);
}
{
let mut heap = RegionHeap::new();
let idx1 = heap.alloc(1u32);
heap.dealloc(idx1);
let idx2 = heap.alloc(2u32);
assert_eq!(idx1.index(), idx2.index());
assert_ne!(idx1.generation(), idx2.generation());
}
{
let mut heap = RegionHeap::new();
let str_idx = heap.alloc("hello".to_string());
assert_eq!(heap.get::<u32>(str_idx), None);
assert_ne!(heap.get::<String>(str_idx), None);
}
{
let mut heap = RegionHeap::new();
heap.alloc(1u32);
heap.alloc(2u32);
let idx3 = heap.alloc(3u32);
assert_eq!(heap.len(), 3);
heap.dealloc(idx3);
assert_eq!(heap.len(), 2);
}
}
}