Skip to main content

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// ──────────────────────────────────────────────────────────────────────────────