luau-vm 0.732.0

Pure-Rust Luau virtual machine, garbage collector, and standard libraries
Documentation
use core::ptr::NonNull;

use luau_common::{BStr, BString};

use crate::Table;
use crate::VmErrorResult;
use crate::function::UpVal;
use crate::handle::RawHandle;
use crate::handle::sealed::Sealed;
use crate::state::GlobalState;
use crate::thread::Thread;
use crate::value::TValue;

mod atomic;
pub(crate) mod barrier;
mod debug;
mod mark;
pub(crate) mod object;
pub(crate) mod step;
mod sweep;

pub(crate) use object::GcHandle;
pub use object::{GcObject, RawGcObject};

pub const LUA_GC_STOP: i32 = 0;
pub const LUA_GC_RESTART: i32 = 1;
pub const LUA_GC_COLLECT: i32 = 2;
pub const LUA_GC_COUNT: i32 = 3;
pub const LUA_GC_COUNT_B: i32 = 4;
pub const LUA_GC_IS_RUNNING: i32 = 5;
pub const LUA_GC_STEP: i32 = 6;
pub const LUA_GC_SET_GOAL: i32 = 7;
pub const LUA_GC_SET_STEP_MUL: i32 = 8;
pub const LUA_GC_SET_STEP_SIZE: i32 = 9;
pub const LUA_GC_IS_PAUSED: i32 = 10;

#[derive(Default)]
#[repr(C)]
pub struct GcStats {
    pub trigger_terms: [i32; 32],
    pub trigger_term_pos: u32,
    pub trigger_integral: i32,
    pub atomic_start_total_size_bytes: usize,
    pub end_total_size_bytes: usize,
    pub heap_goal_size_bytes: usize,
    pub start_timestamp: f64,
    pub atomic_start_timestamp: f64,
    pub end_timestamp: f64,
}

#[repr(C)]
pub struct GcCycleMetrics {
    pub start_total_size_bytes: usize,
    pub heap_trigger_size_bytes: usize,
    pub pause_time: f64,
    pub start_timestamp: f64,
    pub end_timestamp: f64,
    pub mark_time: f64,
    pub mark_assist_time: f64,
    pub mark_max_explicit_time: f64,
    pub mark_explicit_steps: usize,
    pub mark_work: usize,
    pub atomic_start_timestamp: f64,
    pub atomic_start_total_size_bytes: usize,
    pub atomic_time: f64,
    pub atomic_time_upval: f64,
    pub atomic_time_weak: f64,
    pub atomic_time_gray: f64,
    pub atomic_time_clear: f64,
    pub sweep_time: f64,
    pub sweep_assist_time: f64,
    pub sweep_max_explicit_time: f64,
    pub sweep_explicit_steps: usize,
    pub sweep_work: usize,
    pub assist_work: usize,
    pub explicit_work: usize,
    pub propagate_work: usize,
    pub propagate_again_work: usize,
    pub end_total_size_bytes: usize,
}

#[repr(C)]
pub struct GcMetrics {
    pub step_explicit_time_acc: f64,
    pub step_assist_time_acc: f64,
    pub completed_cycles: u64,
    pub last_cycle: GcCycleMetrics,
    pub curr_cycle: GcCycleMetrics,
}

pub const fn bit_mask(bit: u8) -> u8 {
    1u8 << bit
}

pub const WHITE0_BIT: u8 = 0;
pub const WHITE1_BIT: u8 = 1;
pub const BLACK_BIT: u8 = 2;
pub const FIXED_BIT: u8 = 3;
pub const WHITE_BITS: u8 = bit_mask(WHITE0_BIT) | bit_mask(WHITE1_BIT);

pub const GCS_PAUSE: u8 = 0;
pub const GCS_PROPAGATE: u8 = 1;
pub const GCS_PROPAGATE_AGAIN: u8 = 2;
pub const GCS_ATOMIC: u8 = 3;
pub const GCS_SWEEP: u8 = 4;

#[allow(
    clippy::missing_safety_doc,
    reason = "GlobalState's shared raw-handle contract is documented on GlobalState"
)]
impl GlobalState {
    pub fn gc_state(&self) -> u8 {
        unsafe { (*self.as_ptr()).gc_state }
    }

    pub fn set_gc_state(&self, gc_state: u8) {
        unsafe {
            (*self.as_ptr()).gc_state = gc_state;
        }
    }

    pub fn gray(&self) -> Option<GcObject> {
        unsafe {
            NonNull::new(self.as_ptr().as_ref().unwrap_unchecked().gray)
                .map(|raw| GcObject::from_raw(raw))
        }
    }

    pub fn set_gray(&self, gray: Option<GcObject>) {
        unsafe {
            self.as_ptr().as_mut().unwrap_unchecked().gray =
                gray.map_or(core::ptr::null_mut(), |object| object.as_ptr());
        }
    }

    pub fn gray_again(&self) -> Option<GcObject> {
        unsafe {
            NonNull::new(self.as_ptr().as_ref().unwrap_unchecked().gray_again)
                .map(|raw| GcObject::from_raw(raw))
        }
    }

    pub fn set_gray_again(&self, gray_again: Option<GcObject>) {
        unsafe {
            self.as_ptr().as_mut().unwrap_unchecked().gray_again =
                gray_again.map_or(core::ptr::null_mut(), |object| object.as_ptr());
        }
    }

    pub fn weak(&self) -> Option<GcObject> {
        unsafe {
            NonNull::new(self.as_ptr().as_ref().unwrap_unchecked().weak)
                .map(|raw| GcObject::from_raw(raw))
        }
    }

    pub fn set_weak(&self, weak: Option<GcObject>) {
        unsafe {
            self.as_ptr().as_mut().unwrap_unchecked().weak =
                weak.map_or(core::ptr::null_mut(), |object| object.as_ptr());
        }
    }
}

pub type GcHeapNode = fn(*mut (), *mut (), u8, u8, usize, Option<&BStr>);
pub type GcHeapEdge = fn(*mut (), *mut (), *mut (), &BStr);

pub trait GcCategoryNamer {
    /// `luaC_dump categoryName`
    fn category_name(&mut self, thread: &Thread, memcat: u8, out: &mut BString);
}

pub trait GcHeapVisitor {
    /// `luaC_enumheap node`
    fn node(&mut self, ptr: *mut (), tt: u8, memcat: u8, size: usize, name: Option<&BStr>);

    /// `luaC_enumheap edge`
    fn edge(&mut self, from: *mut (), to: *mut (), name: &BStr);
}

/// Unstable garbage-collector state-machine capability.
///
/// # Safety
///
/// Every object, page, cursor, and global state must be live and belong to the
/// same VM. Callers must invoke operations in a valid collector phase and
/// preserve color, list, root, size, and traversal invariants.
#[allow(
    clippy::missing_safety_doc,
    reason = "all methods share the capability-level safety contract"
)]
pub trait GcRuntime: Sealed {
    /// `luaC_freeall`
    unsafe fn free_all(&self);

    /// `luaC_needsGC`
    unsafe fn needs_gc(&self) -> bool;

    /// `luaC_checkGC`
    unsafe fn check_gc(&self) -> VmErrorResult;

    /// `luaC_step`
    unsafe fn step(&self, assist: bool) -> VmErrorResult<usize>;

    /// `luaC_fullgc`
    unsafe fn full_gc(&self);

    /// `luaC_validate`
    unsafe fn validate(&self);

    /// `luaC_dump`
    unsafe fn dump(&self, file: *mut (), category_name: Option<&mut dyn GcCategoryNamer>);

    /// `luaC_enumheap`
    unsafe fn enum_heap(&self, context: *mut (), node: GcHeapNode, edge: GcHeapEdge);

    /// `luaC_allocationrate`
    unsafe fn allocation_rate(&self) -> i64;
}

/// Unstable GC write-barrier capability.
///
/// # Safety
///
/// Parent and child values must be live, correctly tagged records in this
/// thread's VM, and the supplied list slot must belong to the parent. The
/// caller must perform the barrier as part of the corresponding pointer write.
#[allow(
    clippy::missing_safety_doc,
    reason = "all methods share the capability-level safety contract"
)]
pub trait GcBarrier: Sealed {
    /// `luaC_barrier`
    unsafe fn barrier_value(&self, object: GcObject, value: TValue);

    /// `luaC_objbarrier`
    unsafe fn object_barrier(&self, object: GcObject, child: GcObject);

    /// `luaC_threadbarrier`
    unsafe fn thread_barrier(&self);

    /// `luaC_upvalclosed`
    unsafe fn upvalue_closed(&self, upvalue: UpVal);

    /// `luaC_barrierf`
    unsafe fn barrier_forward(&self, object: GcObject, value: GcObject);

    /// `luaC_barriertable`
    unsafe fn barrier_table(&self, table: Table, value: GcObject);

    /// `luaC_barrierback`
    unsafe fn barrier_back(&self, object: GcObject, gc_list: *mut *mut RawGcObject);
}

#[allow(
    clippy::missing_safety_doc,
    reason = "GlobalState's shared raw-handle contract is documented on GlobalState"
)]
impl GlobalState {
    /// `luaC_white`
    pub fn white(&self) -> u8 {
        (unsafe { self.as_ptr().as_ref().unwrap_unchecked().current_white }) & WHITE_BITS
    }

    /// `keepinvariant`
    pub unsafe fn keep_invariant(&self) -> bool {
        matches!(
            self.gc_state(),
            GCS_PROPAGATE | GCS_PROPAGATE_AGAIN | GCS_ATOMIC
        )
    }

    /// `isdead`
    pub unsafe fn is_dead(&self, object: GcObject) -> bool {
        let other_white =
            unsafe { self.as_ptr().as_ref().unwrap_unchecked().current_white ^ WHITE_BITS };
        let marked = unsafe { object.as_ptr().as_ref().unwrap_unchecked().marked };
        (marked & (WHITE_BITS | bit_mask(FIXED_BIT))) == (other_white & WHITE_BITS)
    }

    /// `makewhite`
    pub unsafe fn make_white(&self, object: GcObject) {
        unsafe {
            let mask_marks = !(bit_mask(BLACK_BIT) | WHITE_BITS);
            let current_white = self.as_ptr().as_ref().unwrap_unchecked().current_white;
            let raw = object.as_ptr().as_mut().unwrap_unchecked();
            let new_marked = (raw.marked & mask_marks) | (current_white & WHITE_BITS);
            raw.marked = new_marked;
        }
    }
}

#[allow(
    clippy::missing_safety_doc,
    reason = "GcObject's shared raw-handle contract is documented on GcObject"
)]
impl GcObject {
    /// `iswhite`
    pub unsafe fn is_white(&self) -> bool {
        (unsafe { self.as_ptr().as_ref().unwrap_unchecked().marked } & WHITE_BITS) != 0
    }

    /// `isblack`
    pub unsafe fn is_black(&self) -> bool {
        (unsafe { self.as_ptr().as_ref().unwrap_unchecked().marked } & bit_mask(BLACK_BIT)) != 0
    }

    /// `isgray`
    pub unsafe fn is_gray(&self) -> bool {
        (unsafe { self.as_ptr().as_ref().unwrap_unchecked().marked }
            & (WHITE_BITS | bit_mask(BLACK_BIT)))
            == 0
    }

    /// `changewhite`
    pub unsafe fn change_white(&mut self) {
        unsafe { self.as_ptr().as_mut().unwrap_unchecked().marked ^= WHITE_BITS };
    }

    /// `gray2black`
    pub unsafe fn gray_to_black(&mut self) {
        unsafe { self.as_ptr().as_mut().unwrap_unchecked().marked |= bit_mask(BLACK_BIT) };
    }

    /// `white2gray`
    pub unsafe fn white_to_gray(&mut self) {
        unsafe { self.as_ptr().as_mut().unwrap_unchecked().marked &= !WHITE_BITS };
    }

    /// `black2gray`
    pub unsafe fn black_to_gray(&mut self) {
        unsafe { self.as_ptr().as_mut().unwrap_unchecked().marked &= !bit_mask(BLACK_BIT) };
    }
}