lua_vm/trace_impls.rs
1//! Phase-D `Trace` implementations for GC-rooted types defined in this
2//! crate. Types in `lua-types` (LuaValue, LuaString, UpVal) have their
3//! Trace impls in `lua-types/src/trace_impls.rs` because of Rust's orphan
4//! rule.
5//!
6//! Each impl below is a `todo!("phase-d: trace X")` stub. The
7//! panic-driven mega-loop surfaces each one when a runtime path triggers
8//! `Heap::full_collect`. Each agent works on ONE type — no family
9//! expansion (Trace impls have subtle invariants).
10//!
11//! Implementation guidance for agents:
12//! 1. Read the type definition; enumerate every field
13//! 2. For every `Gc<T>`, `GcRef<T>`, or container (Vec/Option/HashMap)
14//! thereof, call `m.mark(field)` or `field.trace(m)` appropriately
15//! 3. Skip non-GC fields (primitives, `String`, `Vec<u8>`)
16//! 4. Skip "intentionally not traced" fields (weak refs)
17//! 5. Reference `reference/lua-5.4.7/src/lgc.c`'s `reallymarkobject`
18
19use lua_gc::{Marker, Trace};
20use crate::state::{FinalizerObject, LuaState, GlobalState};
21use crate::string::{LuaStringImpl, LuaUserDataImpl};
22use lua_types::{LuaClosure, LuaValue};
23
24/// Phase-B internal richer LuaString. The byte buffer is a Rust `Rc<[u8]>`
25/// (not GC-managed); no fields to mark.
26impl Trace for LuaStringImpl {
27 fn trace(&self, _m: &mut Marker) {}
28}
29
30/// Phase-B internal userdata. Both `metatable` and `uv` are currently
31/// `Option<()>` / `Vec<()>` stubs — no GC edges to walk yet. Becomes
32/// real when userdata machinery lands post-D-1.
33impl Trace for LuaUserDataImpl {
34 fn trace(&self, _m: &mut Marker) {}
35}
36
37impl Trace for FinalizerObject {
38 fn trace(&self, m: &mut Marker) {
39 match self {
40 FinalizerObject::Table(t) => t.trace(m),
41 FinalizerObject::UserData(u) => u.trace(m),
42 }
43 }
44}
45
46impl Trace for LuaState {
47 fn trace(&self, m: &mut Marker) {
48 // and the open-upvalue list. Trace frame-bounded live ranges instead of
49 // every slot up to `ci.top`: that reserved tail can contain stale values
50 // from previous calls. Lua locals that sit above the transient `top` are
51 // added explicitly from debug local metadata.
52 let trace_debug_locals = self.cached_thread_id == self.global.borrow().current_thread_id;
53 let mut ci_idx = Some(self.ci);
54 while let Some(idx) = ci_idx {
55 let ci = &self.call_info[idx.as_usize()];
56 let start = ci.func.0 as usize;
57 let end_idx = if idx == self.ci {
58 self.top.0 as usize
59 } else if let Some(next) = ci.next {
60 self.call_info[next.as_usize()].func.0 as usize
61 } else {
62 self.top.0 as usize
63 };
64 let end = end_idx.min(self.stack.len());
65 if start < end {
66 for slot in &self.stack[start..end] {
67 slot.val.trace(m);
68 }
69 }
70 if trace_debug_locals && ci.is_lua() {
71 if let Some(slot) = self.stack.get(ci.func.0 as usize) {
72 if let LuaValue::Function(LuaClosure::Lua(cl)) = &slot.val {
73 let pc = ci.saved_pc().saturating_sub(1) as i32;
74 let base = ci.func.0 as usize + 1;
75 let mut n = 1i32;
76 while crate::func::get_local_name(&cl.proto, n, pc).is_some() {
77 let idx = base + (n as usize - 1);
78 if let Some(local_slot) = self.stack.get(idx) {
79 local_slot.val.trace(m);
80 }
81 n += 1;
82 }
83 }
84 }
85 }
86 ci_idx = ci.previous;
87 }
88
89 for uv in self.openupval.iter() {
90 uv.trace(m);
91 }
92
93 // PORT NOTE: `global` (Rc<RefCell<GlobalState>>) is reached from the
94 // heap's root via GlobalState::trace; tracing it from each thread
95 // would re-enter the root and is explicitly excluded.
96 // PORT NOTE: `call_info` entries carry pc offsets and stack indices
97 // but no direct GcRef fields. The active closure is reached through
98 // the stack slot at `ci.func`, already covered by the stack walk.
99 // PORT NOTE: `tbclist` holds StackIdx values only; the to-be-closed
100 // objects themselves live on the stack and are traced there.
101 }
102}
103
104impl Trace for GlobalState {
105 fn trace(&self, m: &mut Marker) {
106 // per-type metatables, and pending finalizers. We expand the set to
107 // include preallocated short strings (memerrmsg, tmname[]) and the
108 // open-upvalue thread list, both of which the panic-driven Phase-D
109 // mega-loop expects to see at the root.
110
111 self.l_registry.trace(m);
112
113 // Values held by Rust-side embedding handles are rooted outside the
114 // Lua registry table so handle Drop can unroot without touching the
115 // Lua stack/API. They are still ordinary GC roots during marking.
116 for value in self.external_roots.iter_values() {
117 value.trace(m);
118 }
119
120 // Cross-thread open-upvalue mirrors are live roots while a coroutine
121 // resume holds the home thread's stack behind an outer mutable borrow.
122 for value in self.cross_thread_upvals.values() {
123 value.trace(m);
124 }
125
126 // PORT NOTE (phase-b-reconcile): The lua-types LuaTable placeholder is
127 // storage-less, so `globals` and `loaded` cannot live inside the registry
128 // table (see `init_registry`). They are kept as direct GlobalState fields
129 // and must be traced explicitly as roots; once the placeholder reconciles
130 // with vm::LuaTable, these become reachable via `l_registry` and the two
131 // lines below disappear.
132 self.globals.trace(m);
133 self.loaded.trace(m);
134
135 if let Some(t) = &self.mainthread {
136 t.trace(m);
137 }
138
139 self.main_thread_value.trace(m);
140
141 if self.current_thread_id != self.main_thread_id {
142 if let Some(entry) = self.threads.get(&self.current_thread_id) {
143 entry.value.trace(m);
144 }
145 }
146
147 // Registered coroutines are not roots by registration alone. The
148 // post-mark hook traces stacks only for thread handles that were
149 // reached from a real root, matching Lua's collectable coroutine
150 // semantics.
151
152 for slot in self.mt.iter() {
153 if let Some(t) = slot {
154 t.trace(m);
155 }
156 }
157
158 for s in self.tmname.iter() {
159 s.trace(m);
160 }
161
162 self.memerrmsg.trace(m);
163
164 for th in self.twups.iter() {
165 th.trace(m);
166 }
167
168 // `interned_lt` is a weak short-string cache. The collector prunes
169 // unmarked entries from the post-mark hook instead of tracing them as
170 // roots here.
171 for row in self.strcache.iter() {
172 for s in row.iter() {
173 s.trace(m);
174 }
175 }
176
177 // Pending finalizers are NOT traced here — that's what lets the mark
178 // phase distinguish "still reachable from the user program" from
179 // "only kept alive by the finalizer registry". `collect_via_heap`'s
180 // post-mark hook checks each entry against the visited set; an
181 // unvisited entry is moved to `to_be_finalized` and explicitly
182 // marked there so it survives the sweep.
183 //
184 // `to_be_finalized` IS traced as a strong root: objects in this list
185 // are awaiting their `__gc` call but are otherwise dead, and the
186 // object (plus its descendants) must survive long enough for the
187 // finalizer to run.
188 for object in self.to_be_finalized.iter() {
189 object.trace(m);
190 }
191
192 // Trace suspended parent stacks. When a coroutine is running, any
193 // parent threads are suspended and their stacks are not reachable from
194 // `threads` (which only holds coroutines, not the main thread). Before
195 // `aux_resume` resumes a coroutine it pushes a snapshot of the parent's
196 // live stack onto `suspended_parent_stacks` so those GC-managed values
197 // remain marked during collections triggered from inside the coroutine.
198 for stack_snapshot in self.suspended_parent_stacks.iter() {
199 for v in stack_snapshot.iter() {
200 v.trace(m);
201 }
202 }
203 for upval_snapshot in self.suspended_parent_open_upvals.iter() {
204 for uv in upval_snapshot.iter() {
205 uv.trace(m);
206 }
207 }
208
209 // PORT NOTE: `strt` (the internal LuaStringImpl intern table) is a
210 // weak table in C; entries are cleared during the atomic weak-table
211 // pass (`clearbykeys`), not marked as roots. The current port has no
212 // incremental weak-sweep, but `strt` is keyed by byte-content rather
213 // than by `Gc` identity, so a dangling entry there is silently
214 // recreated by the next `intern_str` — no UAF, unlike `interned_lt`.
215 // PORT NOTE: `fixedgc` holds objects pre-marked fixed/black at
216 // allocation (`luaC_fix`); the mark phase never re-visits them, and
217 // `dyn Collectable` does not implement `Trace` here.
218 // PORT NOTE: `allgc`, `finobj`, `gray`, `grayagain`, `tobefnz`,
219 // `weak`, `ephemeron`, `allweak` are GC bookkeeping lists owned by
220 // `heap` — they are the universe of allocated objects, not roots.
221 }
222}
223
224// ──────────────────────────────────────────────────────────────────────────────
225// PORT STATUS
226// source: n/a (GC Trace impls bridging lua-vm and lua-gc)
227// target_crate: lua-vm
228// confidence: high
229// todos: 0
230// port_notes: 0
231// unsafe_blocks: 0
232// notes: Implements lua_gc::Trace for LuaState + GlobalState. C does this via
233// hand-written mark routines in lgc.c; we use a trait dispatch.
234// ──────────────────────────────────────────────────────────────────────────────