Skip to main content

luau_vm/gc/
mod.rs

1use core::ptr::NonNull;
2
3use luau_common::{BStr, BString};
4
5use crate::Table;
6use crate::VmErrorResult;
7use crate::function::UpVal;
8use crate::handle::RawHandle;
9use crate::handle::sealed::Sealed;
10use crate::state::GlobalState;
11use crate::thread::Thread;
12use crate::value::TValue;
13
14mod atomic;
15pub(crate) mod barrier;
16mod debug;
17mod mark;
18pub(crate) mod object;
19pub(crate) mod step;
20mod sweep;
21
22pub(crate) use object::GcHandle;
23pub use object::{GcObject, RawGcObject};
24
25pub const LUA_GC_STOP: i32 = 0;
26pub const LUA_GC_RESTART: i32 = 1;
27pub const LUA_GC_COLLECT: i32 = 2;
28pub const LUA_GC_COUNT: i32 = 3;
29pub const LUA_GC_COUNT_B: i32 = 4;
30pub const LUA_GC_IS_RUNNING: i32 = 5;
31pub const LUA_GC_STEP: i32 = 6;
32pub const LUA_GC_SET_GOAL: i32 = 7;
33pub const LUA_GC_SET_STEP_MUL: i32 = 8;
34pub const LUA_GC_SET_STEP_SIZE: i32 = 9;
35pub const LUA_GC_IS_PAUSED: i32 = 10;
36
37#[derive(Default)]
38#[repr(C)]
39pub struct GcStats {
40    pub trigger_terms: [i32; 32],
41    pub trigger_term_pos: u32,
42    pub trigger_integral: i32,
43    pub atomic_start_total_size_bytes: usize,
44    pub end_total_size_bytes: usize,
45    pub heap_goal_size_bytes: usize,
46    pub start_timestamp: f64,
47    pub atomic_start_timestamp: f64,
48    pub end_timestamp: f64,
49}
50
51#[repr(C)]
52pub struct GcCycleMetrics {
53    pub start_total_size_bytes: usize,
54    pub heap_trigger_size_bytes: usize,
55    pub pause_time: f64,
56    pub start_timestamp: f64,
57    pub end_timestamp: f64,
58    pub mark_time: f64,
59    pub mark_assist_time: f64,
60    pub mark_max_explicit_time: f64,
61    pub mark_explicit_steps: usize,
62    pub mark_work: usize,
63    pub atomic_start_timestamp: f64,
64    pub atomic_start_total_size_bytes: usize,
65    pub atomic_time: f64,
66    pub atomic_time_upval: f64,
67    pub atomic_time_weak: f64,
68    pub atomic_time_gray: f64,
69    pub atomic_time_clear: f64,
70    pub sweep_time: f64,
71    pub sweep_assist_time: f64,
72    pub sweep_max_explicit_time: f64,
73    pub sweep_explicit_steps: usize,
74    pub sweep_work: usize,
75    pub assist_work: usize,
76    pub explicit_work: usize,
77    pub propagate_work: usize,
78    pub propagate_again_work: usize,
79    pub end_total_size_bytes: usize,
80}
81
82#[repr(C)]
83pub struct GcMetrics {
84    pub step_explicit_time_acc: f64,
85    pub step_assist_time_acc: f64,
86    pub completed_cycles: u64,
87    pub last_cycle: GcCycleMetrics,
88    pub curr_cycle: GcCycleMetrics,
89}
90
91pub const fn bit_mask(bit: u8) -> u8 {
92    1u8 << bit
93}
94
95pub const WHITE0_BIT: u8 = 0;
96pub const WHITE1_BIT: u8 = 1;
97pub const BLACK_BIT: u8 = 2;
98pub const FIXED_BIT: u8 = 3;
99pub const WHITE_BITS: u8 = bit_mask(WHITE0_BIT) | bit_mask(WHITE1_BIT);
100
101pub const GCS_PAUSE: u8 = 0;
102pub const GCS_PROPAGATE: u8 = 1;
103pub const GCS_PROPAGATE_AGAIN: u8 = 2;
104pub const GCS_ATOMIC: u8 = 3;
105pub const GCS_SWEEP: u8 = 4;
106
107#[allow(
108    clippy::missing_safety_doc,
109    reason = "GlobalState's shared raw-handle contract is documented on GlobalState"
110)]
111impl GlobalState {
112    pub fn gc_state(&self) -> u8 {
113        unsafe { (*self.as_ptr()).gc_state }
114    }
115
116    pub fn set_gc_state(&self, gc_state: u8) {
117        unsafe {
118            (*self.as_ptr()).gc_state = gc_state;
119        }
120    }
121
122    pub fn gray(&self) -> Option<GcObject> {
123        unsafe {
124            NonNull::new(self.as_ptr().as_ref().unwrap_unchecked().gray)
125                .map(|raw| GcObject::from_raw(raw))
126        }
127    }
128
129    pub fn set_gray(&self, gray: Option<GcObject>) {
130        unsafe {
131            self.as_ptr().as_mut().unwrap_unchecked().gray =
132                gray.map_or(core::ptr::null_mut(), |object| object.as_ptr());
133        }
134    }
135
136    pub fn gray_again(&self) -> Option<GcObject> {
137        unsafe {
138            NonNull::new(self.as_ptr().as_ref().unwrap_unchecked().gray_again)
139                .map(|raw| GcObject::from_raw(raw))
140        }
141    }
142
143    pub fn set_gray_again(&self, gray_again: Option<GcObject>) {
144        unsafe {
145            self.as_ptr().as_mut().unwrap_unchecked().gray_again =
146                gray_again.map_or(core::ptr::null_mut(), |object| object.as_ptr());
147        }
148    }
149
150    pub fn weak(&self) -> Option<GcObject> {
151        unsafe {
152            NonNull::new(self.as_ptr().as_ref().unwrap_unchecked().weak)
153                .map(|raw| GcObject::from_raw(raw))
154        }
155    }
156
157    pub fn set_weak(&self, weak: Option<GcObject>) {
158        unsafe {
159            self.as_ptr().as_mut().unwrap_unchecked().weak =
160                weak.map_or(core::ptr::null_mut(), |object| object.as_ptr());
161        }
162    }
163}
164
165pub type GcHeapNode = fn(*mut (), *mut (), u8, u8, usize, Option<&BStr>);
166pub type GcHeapEdge = fn(*mut (), *mut (), *mut (), &BStr);
167
168pub trait GcCategoryNamer {
169    /// `luaC_dump categoryName`
170    fn category_name(&mut self, thread: &Thread, memcat: u8, out: &mut BString);
171}
172
173pub trait GcHeapVisitor {
174    /// `luaC_enumheap node`
175    fn node(&mut self, ptr: *mut (), tt: u8, memcat: u8, size: usize, name: Option<&BStr>);
176
177    /// `luaC_enumheap edge`
178    fn edge(&mut self, from: *mut (), to: *mut (), name: &BStr);
179}
180
181/// Unstable garbage-collector state-machine capability.
182///
183/// # Safety
184///
185/// Every object, page, cursor, and global state must be live and belong to the
186/// same VM. Callers must invoke operations in a valid collector phase and
187/// preserve color, list, root, size, and traversal invariants.
188#[allow(
189    clippy::missing_safety_doc,
190    reason = "all methods share the capability-level safety contract"
191)]
192pub trait GcRuntime: Sealed {
193    /// `luaC_freeall`
194    unsafe fn free_all(&self);
195
196    /// `luaC_needsGC`
197    unsafe fn needs_gc(&self) -> bool;
198
199    /// `luaC_checkGC`
200    unsafe fn check_gc(&self) -> VmErrorResult;
201
202    /// `luaC_step`
203    unsafe fn step(&self, assist: bool) -> VmErrorResult<usize>;
204
205    /// `luaC_fullgc`
206    unsafe fn full_gc(&self);
207
208    /// `luaC_validate`
209    unsafe fn validate(&self);
210
211    /// `luaC_dump`
212    unsafe fn dump(&self, file: *mut (), category_name: Option<&mut dyn GcCategoryNamer>);
213
214    /// `luaC_enumheap`
215    unsafe fn enum_heap(&self, context: *mut (), node: GcHeapNode, edge: GcHeapEdge);
216
217    /// `luaC_allocationrate`
218    unsafe fn allocation_rate(&self) -> i64;
219}
220
221/// Unstable GC write-barrier capability.
222///
223/// # Safety
224///
225/// Parent and child values must be live, correctly tagged records in this
226/// thread's VM, and the supplied list slot must belong to the parent. The
227/// caller must perform the barrier as part of the corresponding pointer write.
228#[allow(
229    clippy::missing_safety_doc,
230    reason = "all methods share the capability-level safety contract"
231)]
232pub trait GcBarrier: Sealed {
233    /// `luaC_barrier`
234    unsafe fn barrier_value(&self, object: GcObject, value: TValue);
235
236    /// `luaC_objbarrier`
237    unsafe fn object_barrier(&self, object: GcObject, child: GcObject);
238
239    /// `luaC_threadbarrier`
240    unsafe fn thread_barrier(&self);
241
242    /// `luaC_upvalclosed`
243    unsafe fn upvalue_closed(&self, upvalue: UpVal);
244
245    /// `luaC_barrierf`
246    unsafe fn barrier_forward(&self, object: GcObject, value: GcObject);
247
248    /// `luaC_barriertable`
249    unsafe fn barrier_table(&self, table: Table, value: GcObject);
250
251    /// `luaC_barrierback`
252    unsafe fn barrier_back(&self, object: GcObject, gc_list: *mut *mut RawGcObject);
253}
254
255#[allow(
256    clippy::missing_safety_doc,
257    reason = "GlobalState's shared raw-handle contract is documented on GlobalState"
258)]
259impl GlobalState {
260    /// `luaC_white`
261    pub fn white(&self) -> u8 {
262        (unsafe { self.as_ptr().as_ref().unwrap_unchecked().current_white }) & WHITE_BITS
263    }
264
265    /// `keepinvariant`
266    pub unsafe fn keep_invariant(&self) -> bool {
267        matches!(
268            self.gc_state(),
269            GCS_PROPAGATE | GCS_PROPAGATE_AGAIN | GCS_ATOMIC
270        )
271    }
272
273    /// `isdead`
274    pub unsafe fn is_dead(&self, object: GcObject) -> bool {
275        let other_white =
276            unsafe { self.as_ptr().as_ref().unwrap_unchecked().current_white ^ WHITE_BITS };
277        let marked = unsafe { object.as_ptr().as_ref().unwrap_unchecked().marked };
278        (marked & (WHITE_BITS | bit_mask(FIXED_BIT))) == (other_white & WHITE_BITS)
279    }
280
281    /// `makewhite`
282    pub unsafe fn make_white(&self, object: GcObject) {
283        unsafe {
284            let mask_marks = !(bit_mask(BLACK_BIT) | WHITE_BITS);
285            let current_white = self.as_ptr().as_ref().unwrap_unchecked().current_white;
286            let raw = object.as_ptr().as_mut().unwrap_unchecked();
287            let new_marked = (raw.marked & mask_marks) | (current_white & WHITE_BITS);
288            raw.marked = new_marked;
289        }
290    }
291}
292
293#[allow(
294    clippy::missing_safety_doc,
295    reason = "GcObject's shared raw-handle contract is documented on GcObject"
296)]
297impl GcObject {
298    /// `iswhite`
299    pub unsafe fn is_white(&self) -> bool {
300        (unsafe { self.as_ptr().as_ref().unwrap_unchecked().marked } & WHITE_BITS) != 0
301    }
302
303    /// `isblack`
304    pub unsafe fn is_black(&self) -> bool {
305        (unsafe { self.as_ptr().as_ref().unwrap_unchecked().marked } & bit_mask(BLACK_BIT)) != 0
306    }
307
308    /// `isgray`
309    pub unsafe fn is_gray(&self) -> bool {
310        (unsafe { self.as_ptr().as_ref().unwrap_unchecked().marked }
311            & (WHITE_BITS | bit_mask(BLACK_BIT)))
312            == 0
313    }
314
315    /// `changewhite`
316    pub unsafe fn change_white(&mut self) {
317        unsafe { self.as_ptr().as_mut().unwrap_unchecked().marked ^= WHITE_BITS };
318    }
319
320    /// `gray2black`
321    pub unsafe fn gray_to_black(&mut self) {
322        unsafe { self.as_ptr().as_mut().unwrap_unchecked().marked |= bit_mask(BLACK_BIT) };
323    }
324
325    /// `white2gray`
326    pub unsafe fn white_to_gray(&mut self) {
327        unsafe { self.as_ptr().as_mut().unwrap_unchecked().marked &= !WHITE_BITS };
328    }
329
330    /// `black2gray`
331    pub unsafe fn black_to_gray(&mut self) {
332        unsafe { self.as_ptr().as_mut().unwrap_unchecked().marked &= !bit_mask(BLACK_BIT) };
333    }
334}