1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//! GC compatibility layer — thin wrapper over `ObjectSlab`.
//!
//! The mark-sweep garbage collector has been removed. This module preserves
//! the `GcRef` and `GcHeap` API for backward compatibility, but all allocation
//! is now backed by deterministic reference counting via `ObjectSlab`.
//!
//! # What changed
//!
//! - `GcHeap::alloc()` → delegates to `ObjectSlab::alloc()` (RC-backed)
//! - `GcHeap::collect()` → no-op (RC handles deallocation)
//! - `GcHeap::mark()` / `sweep()` → removed (no mark-sweep semantics)
//! - `GcHeap::get()` → delegates to `ObjectSlab::get()`
//! - `GcHeap::live_count()` → delegates to `ObjectSlab::live_count()`
//!
//! # Determinism
//!
//! The `ObjectSlab` provides deterministic LIFO slot reuse. Same allocation
//! sequence → same slot indices. No stop-the-world pauses.
use std::any::Any;
use std::fmt;
use crate::object_slab::{ObjectSlab, SlabRef};
/// A handle into the GC heap. Lightweight, copyable index.
///
/// Now backed by `SlabRef` (RC-based slab) instead of mark-sweep GC.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GcRef {
pub index: usize,
}
impl GcRef {
fn from_slab(sr: SlabRef) -> Self {
GcRef { index: sr.index }
}
fn to_slab(self) -> SlabRef {
SlabRef { index: self.index }
}
}
/// Backward-compatible GC heap interface backed by `ObjectSlab`.
///
/// All mark-sweep semantics have been removed. Objects are reference-counted
/// and freed deterministically when no references remain.
pub struct GcHeap {
slab: ObjectSlab,
/// Maintained for API compat; incremented on `collect()` calls.
collection_count: u64,
}
impl fmt::Debug for GcHeap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GcHeap")
.field("live_count", &self.slab.live_count())
.field("capacity", &self.slab.capacity())
.field("collection_count", &self.collection_count)
.finish()
}
}
impl GcHeap {
/// Create a new heap. The `_collection_threshold` parameter is accepted
/// for API compatibility but ignored (no automatic collection in RC mode).
pub fn new(_collection_threshold: usize) -> Self {
GcHeap {
slab: ObjectSlab::new(),
collection_count: 0,
}
}
/// Allocate a value on the heap, returning a handle.
pub fn alloc<T: Any + 'static>(&mut self, value: T) -> GcRef {
GcRef::from_slab(self.slab.alloc(value))
}
/// Allocate with "auto-collection" — for API compat.
/// The `_roots` parameter is ignored (no GC to trigger).
pub fn alloc_auto<T: Any + 'static>(&mut self, value: T, _roots: &[GcRef]) -> GcRef {
self.alloc(value)
}
/// Read a reference to the value behind `gc_ref`, downcasting to `T`.
pub fn get<T: Any + 'static>(&self, gc_ref: GcRef) -> Option<&T> {
self.slab.get::<T>(gc_ref.to_slab())
}
/// No-op: mark-sweep has been removed. Objects are reference-counted.
pub fn collect(&mut self, _roots: &[GcRef]) {
self.collection_count += 1;
self.slab.collect_noop();
}
/// Number of live objects on the heap.
pub fn live_count(&self) -> usize {
self.slab.live_count()
}
/// Total capacity (number of slots, including freed ones).
pub fn capacity(&self) -> usize {
self.slab.capacity()
}
/// Access the free list (for backward compat with tests).
pub fn free_list(&self) -> &[usize] {
&self.slab.free_list
}
/// Explicitly free a slot.
pub fn free(&mut self, gc_ref: GcRef) {
self.slab.free(gc_ref.to_slab());
}
}
impl Default for GcHeap {
fn default() -> Self {
Self::new(1024)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alloc_and_read_back() {
let mut heap = GcHeap::new(1024);
let r = heap.alloc(42i64);
assert_eq!(heap.get::<i64>(r), Some(&42));
assert_eq!(heap.live_count(), 1);
}
#[test]
fn collect_is_noop_objects_survive() {
let mut heap = GcHeap::new(1024);
let r1 = heap.alloc(10i64);
let r2 = heap.alloc(20i64);
// Collect with partial roots — but since it's RC-backed, ALL survive
heap.collect(&[r1]);
assert_eq!(heap.live_count(), 2, "RC keeps all objects alive");
assert_eq!(heap.get::<i64>(r1), Some(&10));
assert_eq!(heap.get::<i64>(r2), Some(&20));
}
#[test]
fn explicit_free_and_slot_reuse() {
let mut heap = GcHeap::new(1024);
let r1 = heap.alloc(1i64);
let r2 = heap.alloc(2i64);
let _r3 = heap.alloc(3i64);
// Explicitly free r2
heap.free(r2);
assert_eq!(heap.free_list().len(), 1);
// New alloc reuses freed slot (LIFO)
let r4 = heap.alloc(4i64);
assert_eq!(r4.index, r2.index, "LIFO slot reuse");
assert_eq!(heap.get::<i64>(r4), Some(&4));
assert_eq!(heap.get::<i64>(r1), Some(&1));
}
#[test]
fn type_mismatch_returns_none() {
let mut heap = GcHeap::new(1024);
let r = heap.alloc(42i64);
assert_eq!(heap.get::<String>(r), None);
assert_eq!(heap.get::<i64>(r), Some(&42));
}
#[test]
fn alloc_auto_compat() {
let mut heap = GcHeap::new(2);
let r1 = heap.alloc(1i64);
let _ = heap.alloc(2i64);
// alloc_auto ignores roots in RC mode
let r3 = heap.alloc_auto(3i64, &[r1]);
assert_eq!(heap.get::<i64>(r1), Some(&1));
assert_eq!(heap.get::<i64>(r3), Some(&3));
// All objects still alive (RC, not GC)
assert_eq!(heap.live_count(), 3);
}
#[test]
fn deterministic_slot_order() {
let mut h1 = GcHeap::new(1024);
let mut h2 = GcHeap::new(1024);
let a1 = h1.alloc(10i64);
let a2 = h1.alloc(20i64);
h1.free(a1);
let a3 = h1.alloc(30i64);
let b1 = h2.alloc(10i64);
let b2 = h2.alloc(20i64);
h2.free(b1);
let b3 = h2.alloc(30i64);
assert_eq!(a1.index, b1.index);
assert_eq!(a2.index, b2.index);
assert_eq!(a3.index, b3.index, "LIFO reuse deterministic");
}
}