luau 0.732.0

Safe lifetime-bound Rust embedding API for the Luau runtime
use core::cell::RefCell;

use luau_common::{BStr, BString};
use luau_vm::LuaDebug;
use luau_vm::thread::StackGuard;

use super::Function;
use crate::error::Error;

/// Debug information about a Luau function.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct FunctionInfo {
    /// The function name, when known.
    pub name: Option<BString>,
    /// The function kind.
    pub what: BString,
    /// The source name.
    pub source: Option<BString>,
    /// A shortened form of the source name.
    pub short_src: Option<BString>,
    /// The line where the function was defined.
    pub line_defined: Option<usize>,
    /// The number of upvalues.
    pub num_upvalues: u8,
    /// The number of fixed parameters.
    pub num_params: u8,
    /// Whether the function accepts variadic arguments.
    pub is_vararg: bool,
}

/// Coverage counters for one function in a prototype tree.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CoverageInfo {
    /// The function name, when known.
    pub function: Option<BString>,
    /// The line where the function was defined.
    pub line_defined: i32,
    /// The function's depth in the prototype tree.
    pub depth: i32,
    /// Hit counts indexed by source line.
    ///
    /// A negative count means the line is not executable.
    pub hits: Vec<i32>,
}

impl Function<'_> {
    /// Returns debug information about this function.
    pub fn info(&self) -> Result<FunctionInfo, Error> {
        unsafe {
            let thread = self.thread();
            let vm_thread = thread.as_vm();
            let _stack = StackGuard::new(vm_thread);
            let mut debug = LuaDebug::default();

            self.push_to(vm_thread)?;
            if vm_thread
                .get_info(-1, "snau", &mut debug)
                .map_err(|exit| Error::from_thread_exit(vm_thread, exit))?
                == 0
            {
                return Err(Error::runtime("failed to read function debug info"));
            }
            let source = debug.source.as_bytes();
            let short_src = debug.short_src();

            Ok(FunctionInfo {
                name: debug.name.map(|name| BString::from(name.as_bytes())),
                what: BString::from(debug.what.as_bytes()),
                source: (!source.is_empty()).then(|| BString::from(source)),
                short_src: (!short_src.is_empty()).then(|| BString::from(short_src)),
                line_defined: usize::try_from(debug.linedefined).ok(),
                num_upvalues: debug.nupvals,
                num_params: debug.nparams,
                is_vararg: debug.is_vararg,
            })
        }
    }

    /// Visits the coverage counters for this function and its nested functions.
    ///
    /// Coverage information is available when the source was compiled with a
    /// nonzero [`Compiler::set_coverage_level`](crate::Compiler::set_coverage_level).
    pub fn coverage<F>(&self, callback: F) -> Result<(), Error>
    where
        F: FnMut(CoverageInfo),
    {
        unsafe {
            let thread = self.thread();
            let vm_thread = thread.as_vm();
            let _stack = StackGuard::new(vm_thread);

            self.push_to(vm_thread)?;
            if vm_thread.is_native_function(-1) != 0 {
                return Err(Error::runtime(
                    "coverage is only available for Lua functions",
                ));
            }

            let callback = RefCell::new(callback);
            vm_thread
                .get_coverage(
                    -1,
                    (&callback as *const RefCell<F>).cast_mut().cast(),
                    coverage_callback::<F>,
                )
                .map_err(|exit| Error::from_thread_exit(vm_thread, exit))
        }
    }
}

fn coverage_callback<F>(
    context: *mut (),
    function: Option<&BStr>,
    line_defined: i32,
    depth: i32,
    hits: &[i32],
) where
    F: FnMut(CoverageInfo),
{
    unsafe {
        let callback = &*context.cast::<RefCell<F>>();
        let mut callback = callback.borrow_mut();
        callback(CoverageInfo {
            function: function.map(BString::from),
            line_defined,
            depth,
            hits: hits.to_vec(),
        });
    }
}