lua_types/gc.rs
1//! `GcRef<T>` — the GC-managed reference handle.
2//!
3//! Phase A/B/C: thin newtype around `Rc<T>`.
4//! Phase D-1e (current): newtype around `lua_gc::Gc<T>` — Copy under the hood,
5//! tracks allocation in the active `Heap` (via `lua_gc::with_current_heap(...)`).
6//!
7//! Surface kept stable across the swap: `new`, `ptr_eq`, `identity`,
8//! `strong_count`, `weak_count`, `downgrade`. Existing code touching
9//! `gc.0` continues to work — `.0` is now `Gc<T>` instead of `Rc<T>`.
10//!
11//! # Weak refs
12//!
13//! Heap-tracked `GcWeak<T>` handles remember the heap active when they were
14//! created plus the target's heap allocation token. They upgrade only while
15//! that identity/token pair remains live. Handles to legacy uncollected boxes
16//! still upgrade forever, matching their process-lifetime allocation model.
17
18use lua_gc::{Gc, HeapRef, Marker, Trace};
19
20/// A GC-managed pointer to a Lua collectable object. Newtype over
21/// `lua_gc::Gc<T>` so callers preserve `gc.0`-shape access while the
22/// backend swaps under them.
23#[derive(Debug)]
24pub struct GcRef<T: Trace + 'static>(pub Gc<T>);
25
26impl<T: Trace + 'static> GcRef<T> {
27 /// Allocate a new GC-tracked value. If a `HeapGuard` is active (set by
28 /// `state.run()` / `pcall_k`), the new allocation joins that heap's
29 /// allgc chain. Otherwise it allocates "uncollected" — leaks until
30 /// process exit, same as the old `Rc::new` behavior.
31 pub fn new(value: T) -> Self {
32 let gc = lua_gc::with_current_heap(|heap| match heap {
33 Some(heap) => heap.allocate(value),
34 None => Gc::new_uncollected(value),
35 });
36 GcRef(gc)
37 }
38
39 /// Cycle-aware trace dispatch.
40 ///
41 /// During D-0/D-1 (before a real Color::Gray flag is reachable from
42 /// inside Trace impls), `Marker::try_visit` records the underlying
43 /// allocation's identity and short-circuits a second recursion. Once
44 /// D-2 ships the in-header color flag, this helper collapses to
45 /// `m.mark(self.0)`.
46 pub fn trace_obj(&self, m: &mut Marker) {
47 if m.try_visit(self.identity()) {
48 (**self).trace(m);
49 }
50 }
51}
52
53impl<T: Trace + 'static> GcRef<T> {
54 /// Two `GcRef`s are identity-equal iff they point at the same box.
55 pub fn ptr_eq(a: &Self, b: &Self) -> bool {
56 Gc::ptr_eq(a.0, b.0)
57 }
58
59 /// Identity as a usize — used as a HashMap key for "same object" tests.
60 pub fn identity(&self) -> usize {
61 self.0.identity()
62 }
63
64 /// Number of strong references. `GcRef` is not reference-counted, so a live
65 /// handle reports one owning GC reachability handle.
66 pub fn strong_count(&self) -> usize {
67 1
68 }
69
70 /// Number of weak references. Weak handles are not counted.
71 pub fn weak_count(&self) -> usize {
72 0
73 }
74
75 /// Get a weak handle. If this allocation belongs to the currently-active
76 /// heap, the weak handle will stop upgrading once sweep removes that exact
77 /// heap allocation.
78 pub fn downgrade(&self) -> GcWeak<T> {
79 let identity = self.identity();
80 let tracked = lua_gc::with_current_heap(|heap| {
81 heap.and_then(|heap| {
82 heap.allocation_token(identity)
83 .map(|token| (HeapRef::from_heap(heap), token))
84 })
85 });
86 let (heap, allocation_token) = match tracked {
87 Some((heap, token)) => (Some(heap), token),
88 None => (None, 0),
89 };
90 GcWeak {
91 target: self.0,
92 identity,
93 allocation_token,
94 heap,
95 }
96 }
97
98 /// Charge (`delta > 0`) or refund (`delta < 0`) bytes of this object's
99 /// owned heap buffers against the active heap's pacer, so collections
100 /// fire at honest memory pressure. No-op on `delta == 0`, when no heap is
101 /// active, or when the underlying box is uncollected (see
102 /// [`lua_gc::Gc::account_buffer`]).
103 pub fn account_buffer(&self, delta: isize) {
104 if delta == 0 {
105 return;
106 }
107 lua_gc::with_current_heap(|h| {
108 if let Some(h) = h {
109 self.0.account_buffer(h, delta)
110 }
111 })
112 }
113}
114
115/// A weak handle to a `GcRef<T>`.
116#[derive(Debug)]
117pub struct GcWeak<T: Trace + 'static> {
118 target: Gc<T>,
119 identity: usize,
120 allocation_token: usize,
121 heap: Option<HeapRef>,
122}
123
124impl<T: Trace + 'static> GcWeak<T> {
125 /// Try to promote to a strong reference.
126 pub fn upgrade(&self) -> Option<GcRef<T>> {
127 if let Some(heap) = self.heap {
128 if !heap.contains_allocation(self.identity, self.allocation_token) {
129 return None;
130 }
131 }
132 Some(GcRef(self.target))
133 }
134
135 /// Strong reference count of the target from this weak handle's point of
136 /// view: one while it can still upgrade, zero after sweep.
137 pub fn strong_count(&self) -> usize {
138 usize::from(self.upgrade().is_some())
139 }
140
141 pub fn identity(&self) -> usize {
142 self.identity
143 }
144}
145
146impl<T: Trace + 'static> Clone for GcWeak<T> {
147 fn clone(&self) -> Self {
148 GcWeak {
149 target: self.target,
150 identity: self.identity,
151 allocation_token: self.allocation_token,
152 heap: self.heap,
153 }
154 }
155}
156
157impl<T: Trace + 'static> Clone for GcRef<T> {
158 fn clone(&self) -> Self {
159 GcRef(self.0)
160 }
161}
162
163impl<T: Trace + 'static> Copy for GcRef<T> {}
164
165impl<T: Trace + 'static> std::ops::Deref for GcRef<T> {
166 type Target = T;
167 fn deref(&self) -> &T {
168 &*self.0
169 }
170}
171
172impl<T: Trace + 'static> AsRef<T> for GcRef<T> {
173 fn as_ref(&self) -> &T {
174 &*self.0
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 struct NoRoots;
183
184 impl Trace for NoRoots {
185 fn trace(&self, _m: &mut Marker) {}
186 }
187
188 #[derive(Debug)]
189 struct Cell0;
190
191 impl Trace for Cell0 {
192 fn trace(&self, _m: &mut Marker) {}
193 }
194
195 #[test]
196 fn heap_tracked_weak_refs_stop_upgrading_after_sweep() {
197 let heap = lua_gc::Heap::new();
198 heap.unpause();
199 let _guard = lua_gc::HeapGuard::push(&heap);
200
201 let strong = GcRef::new(Cell0);
202 let weak = strong.downgrade();
203 assert!(weak.upgrade().is_some());
204 assert_eq!(weak.strong_count(), 1);
205
206 heap.full_collect(&NoRoots);
207 assert!(weak.upgrade().is_none());
208 assert_eq!(weak.strong_count(), 0);
209 }
210
211 #[test]
212 fn uncollected_weak_refs_keep_process_lifetime_behavior() {
213 let strong = GcRef::new(Cell0);
214 let weak = strong.downgrade();
215
216 assert!(weak.upgrade().is_some());
217 assert_eq!(weak.strong_count(), 1);
218 }
219}
220
221impl<T: PartialEq + Trace + 'static> PartialEq for GcRef<T> {
222 fn eq(&self, other: &Self) -> bool {
223 Gc::ptr_eq(self.0, other.0) || **self == **other
224 }
225}
226
227// ──────────────────────────────────────────────────────────────────────────────
228// PORT STATUS
229// source: n/a (GcRef public wrapper around lua-gc::Gc<T>)
230// target_crate: lua-types
231// confidence: high
232// todos: 0
233// port_notes: 0
234// unsafe_blocks: 0
235// notes: Thin wrapper type so consumers across crates don't depend on lua-gc's
236// raw Gc<T>. Clone/Deref/PartialEq forwarded; no unsafe surface.
237// ──────────────────────────────────────────────────────────────────────────────