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 (or its bootstrap list inside a bootstrap window).
30 /// Otherwise it allocates detached — never freed for the life of the
31 /// process (the issue #249 leak class). The detached arm exists only for
32 /// the handful of pre-heap allocations in `new_state`; every other path
33 /// must have a guard, and `OMNILUA_GC_STRICT_GUARD=1` turns this arm into
34 /// a panic so guard-coverage gaps self-report under the test suites.
35 pub fn new(value: T) -> Self {
36 let gc = lua_gc::with_current_heap(|heap| match heap {
37 Some(heap) => heap.allocate(value),
38 None => {
39 if lua_gc::strict_guard_mode() {
40 panic!(
41 "OMNILUA_GC_STRICT_GUARD: GcRef::new::<{}> with no active HeapGuard — \
42 this allocation would be detached and never freed (issue #249 class); \
43 push a HeapGuard or bootstrap window on the entry path",
44 std::any::type_name::<T>()
45 );
46 }
47 Gc::new_uncollected(value)
48 }
49 });
50 GcRef(gc)
51 }
52
53 /// The pre-strict fallback semantics, spelled explicitly: allocate on the
54 /// active heap if one is present, otherwise detached (never freed for the
55 /// life of the process).
56 ///
57 /// DEBT(fallback): the only sanctioned caller is host-side `LuaError`
58 /// message construction (`lua-types/src/error.rs`) — errors are built
59 /// with no VM in scope and `LuaError::Runtime` carries a `LuaValue`, so
60 /// the message must be a GC string even off-heap. Remove by carrying
61 /// owned bytes in `LuaError` until VM entry. Exempt from
62 /// `OMNILUA_GC_STRICT_GUARD` because the call is explicit and auditable;
63 /// volume still surfaces through [`lua_gc::detached_allocations`], which
64 /// the leak canaries assert stays flat.
65 pub(crate) fn new_or_detached(value: T) -> Self {
66 let gc = lua_gc::with_current_heap(|heap| match heap {
67 Some(heap) => heap.allocate(value),
68 None => Gc::new_uncollected(value),
69 });
70 GcRef(gc)
71 }
72
73 /// Cycle-aware trace dispatch.
74 ///
75 /// During D-0/D-1 (before a real Color::Gray flag is reachable from
76 /// inside Trace impls), `Marker::try_visit` records the underlying
77 /// allocation's identity and short-circuits a second recursion. Once
78 /// D-2 ships the in-header color flag, this helper collapses to
79 /// `m.mark(self.0)`.
80 pub fn trace_obj(&self, m: &mut Marker) {
81 if m.try_visit(self.identity()) {
82 (**self).trace(m);
83 }
84 }
85}
86
87impl<T: Trace + 'static> GcRef<T> {
88 /// Two `GcRef`s are identity-equal iff they point at the same box.
89 pub fn ptr_eq(a: &Self, b: &Self) -> bool {
90 Gc::ptr_eq(a.0, b.0)
91 }
92
93 /// Identity as a usize — used as a HashMap key for "same object" tests.
94 pub fn identity(&self) -> usize {
95 self.0.identity()
96 }
97
98 /// Number of strong references. `GcRef` is not reference-counted, so a live
99 /// handle reports one owning GC reachability handle.
100 pub fn strong_count(&self) -> usize {
101 1
102 }
103
104 /// Number of weak references. Weak handles are not counted.
105 pub fn weak_count(&self) -> usize {
106 0
107 }
108
109 /// Get a weak handle. If this allocation belongs to the currently-active
110 /// heap, the weak handle will stop upgrading once sweep removes that exact
111 /// heap allocation.
112 ///
113 /// Downgrading a *heap-owned* box with no active `HeapGuard` is a
114 /// latent use-after-free: the resulting handle has no heap identity to
115 /// check, so it keeps upgrading after sweep frees the target.
116 /// `OMNILUA_GC_STRICT_GUARD=1` turns that case into a panic; detached
117 /// (process-lifetime) targets keep the legacy always-upgrades behavior.
118 pub fn downgrade(&self) -> GcWeak<T> {
119 let identity = self.identity();
120 let tracked = lua_gc::with_current_heap(|heap| {
121 heap.map(|heap| {
122 let token = heap.register_allocation_token(identity);
123 (HeapRef::from_heap(heap), token)
124 })
125 });
126 if tracked.is_none() && lua_gc::strict_guard_mode() && self.0.is_heap_owned() {
127 panic!(
128 "OMNILUA_GC_STRICT_GUARD: GcRef::downgrade::<{}> on a heap-owned box with no \
129 active HeapGuard — the weak handle would upgrade forever, including after \
130 sweep frees the target (use-after-free); push a HeapGuard on the entry path",
131 std::any::type_name::<T>()
132 );
133 }
134 let (heap, allocation_token) = match tracked {
135 Some((heap, token)) => (Some(heap), token),
136 None => (None, 0),
137 };
138 GcWeak {
139 target: self.0,
140 identity,
141 allocation_token,
142 heap,
143 }
144 }
145
146 /// Charge (`delta > 0`) or refund (`delta < 0`) bytes of this object's
147 /// owned heap buffers against the active heap's pacer, so collections
148 /// fire at honest memory pressure. No-op on `delta == 0`, when no heap is
149 /// active, or when the underlying box is uncollected (see
150 /// [`lua_gc::Gc::account_buffer`]).
151 pub fn account_buffer(&self, delta: isize) {
152 if delta == 0 {
153 return;
154 }
155 lua_gc::with_current_heap(|h| match h {
156 Some(h) => self.0.account_buffer(h, delta),
157 None => {
158 if lua_gc::strict_guard_mode() && self.0.is_heap_owned() {
159 panic!(
160 "OMNILUA_GC_STRICT_GUARD: account_buffer({delta}) on a heap-owned \
161 {} with no active HeapGuard — the charge would be silently dropped \
162 and the pacer would drift from real memory; push a HeapGuard on \
163 the entry path",
164 std::any::type_name::<T>()
165 );
166 }
167 }
168 })
169 }
170}
171
172/// A weak handle to a `GcRef<T>`.
173#[derive(Debug)]
174pub struct GcWeak<T: Trace + 'static> {
175 target: Gc<T>,
176 identity: usize,
177 allocation_token: usize,
178 heap: Option<HeapRef>,
179}
180
181impl<T: Trace + 'static> GcWeak<T> {
182 /// Try to promote to a strong reference.
183 pub fn upgrade(&self) -> Option<GcRef<T>> {
184 if let Some(heap) = self.heap {
185 if !heap.contains_allocation(self.identity, self.allocation_token) {
186 return None;
187 }
188 }
189 Some(GcRef(self.target))
190 }
191
192 /// Strong reference count of the target from this weak handle's point of
193 /// view: one while it can still upgrade, zero after sweep.
194 pub fn strong_count(&self) -> usize {
195 usize::from(self.upgrade().is_some())
196 }
197
198 pub fn identity(&self) -> usize {
199 self.identity
200 }
201}
202
203impl<T: Trace + 'static> Clone for GcWeak<T> {
204 fn clone(&self) -> Self {
205 GcWeak {
206 target: self.target,
207 identity: self.identity,
208 allocation_token: self.allocation_token,
209 heap: self.heap,
210 }
211 }
212}
213
214impl<T: Trace + 'static> Clone for GcRef<T> {
215 fn clone(&self) -> Self {
216 GcRef(self.0)
217 }
218}
219
220impl<T: Trace + 'static> Copy for GcRef<T> {}
221
222impl<T: Trace + 'static> std::ops::Deref for GcRef<T> {
223 type Target = T;
224 fn deref(&self) -> &T {
225 &*self.0
226 }
227}
228
229impl<T: Trace + 'static> AsRef<T> for GcRef<T> {
230 fn as_ref(&self) -> &T {
231 &*self.0
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 struct NoRoots;
240
241 impl Trace for NoRoots {
242 fn trace(&self, _m: &mut Marker) {}
243 }
244
245 #[derive(Debug)]
246 struct Cell0;
247
248 impl Trace for Cell0 {
249 fn trace(&self, _m: &mut Marker) {}
250 }
251
252 #[test]
253 fn heap_tracked_weak_refs_stop_upgrading_after_sweep() {
254 let heap = lua_gc::Heap::new();
255 heap.unpause();
256 let _guard = lua_gc::HeapGuard::push(&heap);
257
258 let strong = GcRef::new(Cell0);
259 let weak = strong.downgrade();
260 assert!(weak.upgrade().is_some());
261 assert_eq!(weak.strong_count(), 1);
262
263 heap.full_collect(&NoRoots);
264 assert!(weak.upgrade().is_none());
265 assert_eq!(weak.strong_count(), 0);
266 }
267
268 /// Exercises the sanctioned guard-less path on purpose, which is exactly
269 /// what `OMNILUA_GC_STRICT_GUARD=1` exists to forbid — skipped under
270 /// strict mode rather than deleted so the legacy detached semantics stay
271 /// covered in normal runs.
272 #[test]
273 fn uncollected_weak_refs_keep_process_lifetime_behavior() {
274 if lua_gc::strict_guard_mode() {
275 return;
276 }
277 let strong = GcRef::new(Cell0);
278 let weak = strong.downgrade();
279
280 assert!(weak.upgrade().is_some());
281 assert_eq!(weak.strong_count(), 1);
282 }
283}
284
285impl<T: PartialEq + Trace + 'static> PartialEq for GcRef<T> {
286 fn eq(&self, other: &Self) -> bool {
287 Gc::ptr_eq(self.0, other.0) || **self == **other
288 }
289}
290
291// ──────────────────────────────────────────────────────────────────────────────
292// PORT STATUS
293// source: n/a (GcRef public wrapper around lua-gc::Gc<T>)
294// target_crate: lua-types
295// confidence: high
296// todos: 0
297// port_notes: 0
298// unsafe_blocks: 0
299// notes: Thin wrapper type so consumers across crates don't depend on lua-gc's
300// raw Gc<T>. Clone/Deref/PartialEq forwarded; no unsafe surface.
301// ──────────────────────────────────────────────────────────────────────────────