luau-vm 0.732.0

Pure-Rust Luau virtual machine, garbage collector, and standard libraries
Documentation
use crate::VmResult;
use crate::call::ThreadStack;
use crate::debug::DebugRuntime;
use crate::debug::LuaDebug;
use crate::handle::RawHandle;
use crate::handle::sealed::Sealed;
use crate::metamethod::{MetamethodRuntime, TmEvent};
use crate::native::NativeCallContext;
use crate::state::ThreadState;
use crate::state::{THREAD_STATUS_BREAK, THREAD_STATUS_OK, THREAD_STATUS_YIELD};
use crate::thread::{LUA_MULTRET, Thread};
use crate::value::TValueCursor;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PreCallResult {
    Lua,
    Native,
}

/// Unstable VM call-frame entry and exit capability.
///
/// # Safety
///
/// Function cursors and call frames must be live positions in this thread,
/// with the required argument/result stack shape. Execution may relocate the
/// stack, collect objects, invoke callbacks, yield, or perform VM control flow.
#[allow(
    clippy::missing_safety_doc,
    reason = "all methods share the capability-level safety contract"
)]
pub trait VmCallFrame: Sealed {
    /// `luaV_tryfuncTM`
    unsafe fn try_func_tm(&self, function: TValueCursor) -> VmResult;

    /// `luau_precall`
    unsafe fn pre_call(&self, function: TValueCursor, n_results: i32) -> VmResult<PreCallResult>;

    /// `luau_poscall`
    unsafe fn pos_call(&self, first: TValueCursor);

    /// `luau_callhook`
    unsafe fn call_hook<F>(&self, hook: F, userdata: *mut ()) -> VmResult
    where
        F: FnOnce(&Thread, &mut LuaDebug) -> VmResult;
}

impl VmCallFrame for Thread {
    /// `luaV_tryfuncTM`
    unsafe fn try_func_tm(&self, function: TValueCursor) -> VmResult {
        unsafe {
            let function_value = function.value_unchecked();
            let Some(tm) = self.get_tm_by_obj(function_value, TmEvent::Call) else {
                return self.type_error(function_value, "call").map_err(Into::into);
            };

            if !tm.is_function() {
                return self.type_error(function_value, "call").map_err(Into::into);
            }

            let top = self.stack_top();
            let mut slot = top;

            while slot > function {
                slot.value_unchecked()
                    .set_obj(slot.sub(1).value_unchecked());
                slot = slot.sub(1);
            }

            self.set_stack_top(top.add(1));
            function.value_unchecked().set_obj(tm);
        }
        Ok(())
    }

    /// `luau_precall`
    unsafe fn pre_call(&self, function: TValueCursor, n_results: i32) -> VmResult<PreCallResult> {
        unsafe {
            if !function.value_unchecked().is_function() {
                self.try_func_tm(function)?;
            }

            let closure = function.value_unchecked().closure_value();
            let call_info = self.incr_ci()?.call_info_unchecked();

            call_info.init_call(
                function,
                self.stack_top()
                    .add(closure.as_ptr().as_ref().unwrap_unchecked().stack_size as usize),
                n_results,
                closure.proto(),
            );
            self.set_stack_base(call_info.base());

            self.check_stack_for_new_ci(
                closure.as_ptr().as_ref().unwrap_unchecked().stack_size as i32,
            )?;
            debug_assert!(call_info.top() <= self.stack_last());

            if closure.is_lua() {
                let proto = closure.proto().unwrap_unchecked();
                let mut arg = self.stack_top();
                let arg_end = self
                    .stack_base()
                    .add(proto.as_ptr().as_ref().unwrap_unchecked().num_params as usize);
                while arg < arg_end {
                    arg.value_unchecked().set_nil();
                    arg = arg.add(1);
                }

                self.set_stack_top(
                    if proto.as_ptr().as_ref().unwrap_unchecked().is_vararg != 0 {
                        arg
                    } else {
                        call_info.top()
                    },
                );

                call_info.set_saved_pc(proto.as_ptr().as_ref().unwrap_unchecked().code);

                if proto.as_ptr().as_ref().unwrap_unchecked().exec_target != 0
                    && !proto
                        .as_ptr()
                        .as_ref()
                        .unwrap_unchecked()
                        .exec_data
                        .is_null()
                {
                    call_info.as_ptr().as_mut().unwrap_unchecked().flags =
                        crate::state::LUA_CALLINFO_NATIVE;
                }

                Ok(PreCallResult::Lua)
            } else {
                let native_function = closure.native_data().function.unwrap_unchecked();
                let count = native_function(NativeCallContext::new(self))?;

                let call_info = self.current_call_info();
                let parent_cursor = self.current_call_info_cursor().sub(1);

                let mut result = call_info.function();
                let mut value = self.stack_top().sub(count);
                let value_end = self.stack_top();
                let mut remaining = n_results;

                while remaining != 0 && value < value_end {
                    result.value_unchecked().set_obj(value.value_unchecked());
                    result = result.add(1);
                    value = value.add(1);
                    remaining -= 1;
                }

                while remaining > 0 {
                    result.value_unchecked().set_nil();
                    result = result.add(1);
                    remaining -= 1;
                }

                self.restore_call_frame(parent_cursor, result);

                Ok(PreCallResult::Native)
            }
        }
    }

    /// `luau_poscall`
    unsafe fn pos_call(&self, first: TValueCursor) {
        unsafe {
            let call_info = self.current_call_info();
            let parent_cursor = self.current_call_info_cursor().sub(1);
            let parent = parent_cursor.call_info_unchecked();

            let mut result = call_info.function();
            let mut value = first;
            let value_end = self.stack_top();
            let mut remaining = call_info.as_ptr().as_ref().unwrap_unchecked().n_results;

            while remaining != 0 && value < value_end {
                result.value_unchecked().set_obj(value.value_unchecked());
                result = result.add(1);
                value = value.add(1);
                remaining -= 1;
            }

            while remaining > 0 {
                result.value_unchecked().set_nil();
                result = result.add(1);
                remaining -= 1;
            }

            self.restore_call_frame(
                parent_cursor,
                if call_info.as_ptr().as_ref().unwrap_unchecked().n_results == LUA_MULTRET {
                    result
                } else {
                    parent.top()
                },
            );
        }
    }

    /// `luau_callhook`
    unsafe fn call_hook<F>(&self, hook: F, userdata: *mut ()) -> VmResult
    where
        F: FnOnce(&Thread, &mut LuaDebug) -> VmResult,
    {
        let (base_offset, top_offset, ci_top_offset, status, old_saved_pc, mut ar) = unsafe {
            let base_offset = self.save_stack(self.stack_base());
            let top_offset = self.save_stack(self.stack_top());
            let ci_top_offset = self.save_stack(self.current_call_info().top());
            let status = self.as_ptr().as_ref().unwrap_unchecked().status;

            if status == THREAD_STATUS_YIELD || status == THREAD_STATUS_BREAK {
                self.as_ptr().as_mut().unwrap_unchecked().status = THREAD_STATUS_OK;
                self.set_stack_base(self.current_call_info().base());
            }

            let closure = self.current_call_info().function_closure();
            let old_saved_pc = self.current_call_info().saved_pc();

            if closure.is_lua() {
                let proto = closure.proto().unwrap_unchecked();
                let code_end = proto
                    .as_ptr()
                    .as_ref()
                    .unwrap_unchecked()
                    .code
                    .add(proto.as_ptr().as_ref().unwrap_unchecked().size_code as usize);

                if !old_saved_pc.is_null() && old_saved_pc != code_end {
                    self.current_call_info().set_saved_pc(old_saved_pc.add(1));
                }
            }

            self.check_stack_internal(crate::thread::LUA_MIN_STACK as i32)?;
            self.current_call_info()
                .set_top(self.stack_top().add(crate::thread::LUA_MIN_STACK));
            debug_assert!(self.current_call_info().top() <= self.stack_last());

            let ar = LuaDebug {
                currentline: if closure.is_native() {
                    -1
                } else {
                    let proto = closure.proto().unwrap_unchecked();
                    proto.get_line(proto.pc_rel(old_saved_pc))
                },
                userdata,
                ..LuaDebug::default()
            };

            (
                base_offset,
                top_offset,
                ci_top_offset,
                status,
                old_saved_pc,
                ar,
            )
        };

        let result = hook(self, &mut ar);

        unsafe {
            self.current_call_info().set_saved_pc(old_saved_pc);
            self.current_call_info()
                .set_top(self.restore_stack(ci_top_offset));
            self.set_stack_top(self.restore_stack(top_offset));

            if status == THREAD_STATUS_YIELD
                && self.as_ptr().as_ref().unwrap_unchecked().status != THREAD_STATUS_YIELD
            {
                self.as_ptr().as_mut().unwrap_unchecked().status = THREAD_STATUS_YIELD;
                self.set_stack_base(self.restore_stack(base_offset));
            } else if status == THREAD_STATUS_BREAK {
                debug_assert_ne!(
                    self.as_ptr().as_ref().unwrap_unchecked().status,
                    THREAD_STATUS_BREAK
                );

                self.as_ptr().as_mut().unwrap_unchecked().status = THREAD_STATUS_BREAK;
                self.set_stack_base(self.restore_stack(base_offset));
            }
        }
        result
    }
}