luau-vm 0.732.0

Pure-Rust Luau virtual machine, garbage collector, and standard libraries
Documentation
use luau_common::ByteSlice;

use crate::call::{CallRuntime, ThreadStack};
use crate::debug::DebugRuntime;
use crate::function::FunctionRuntime;
use crate::handle::RawHandle;
use crate::handle::sealed::Sealed;
use crate::state::{
    GlobalState, ProtectedErrorAction, THREAD_STATUS_BREAK, THREAD_STATUS_ERR_ERR,
    THREAD_STATUS_ERR_MEM, THREAD_STATUS_ERR_RUN, THREAD_STATUS_ERR_SYNTAX, ThreadState,
};
use crate::string::StringRuntime;
use crate::thread::Thread;
use crate::value::{TValue, TValueCursor};
use crate::{VmControl, VmError, VmExit, VmResult};

/// Unstable VM error-state capability.
///
/// # Safety
///
/// The thread must be live, and every cursor must address its current stack.
/// Callers must preserve the stack shape required by the error protocol.
#[allow(
    clippy::missing_safety_doc,
    reason = "all methods share the capability-level safety contract"
)]
pub trait ErrorRuntime: Sealed {
    unsafe fn set_error_object(&self, error_code: i32, old_top: TValueCursor);
}

impl ErrorRuntime for Thread {
    /// `luaD_seterrorobj`
    unsafe fn set_error_object(&self, error_code: i32, old_top: TValueCursor) {
        unsafe {
            match error_code {
                x if x == THREAD_STATUS_ERR_MEM as i32 => {
                    let message = self
                        .intern_string(crate::state::LUA_MEMERRMSG.as_bstr())
                        .expect("memory error message is fixed during state initialization");
                    old_top.value_unchecked().set_string_value(message)
                }
                x if x == THREAD_STATUS_ERR_ERR as i32 => {
                    let message = self
                        .intern_string(crate::state::LUA_ERRERRMSG.as_bstr())
                        .expect("error handler message is fixed during state initialization");
                    old_top.value_unchecked().set_string_value(message)
                }
                x if x == THREAD_STATUS_ERR_SYNTAX as i32 || x == THREAD_STATUS_ERR_RUN as i32 => {
                    let top_value = self.stack_top().sub(1).value_unchecked();
                    old_top.value_unchecked().set_obj(top_value);
                }
                _ => unreachable!("invalid Luau error code {error_code}"),
            }

            self.set_stack_top(old_top.add(1));
        }
    }
}

#[repr(C)]
pub(crate) struct ErrorFunctionContext {
    pub(crate) error_function: TValue,
}

/// `callerrfunc`
pub(crate) unsafe fn call_error_function(
    thread: &Thread,
    context: &mut ErrorFunctionContext,
) -> VmResult {
    unsafe {
        let top = thread.stack_top();
        let top_offset = thread.save_stack(top);

        top.value_unchecked().set_obj(top.sub(1).value_unchecked());
        top.sub(1).value_unchecked().set_obj(context.error_function);

        if thread.check_stack(1) == 0 {
            return crate::run_error!(thread, "stack limit").map_err(Into::into);
        }

        let top = thread.restore_stack(top_offset);
        thread.set_stack_top(top.add(1));
        thread.call_no_yield(top.sub(1), 1)?;
    }
    Ok(())
}

pub type Pfunc<T> = unsafe fn(&Thread, &mut T) -> VmResult;

/// Unstable protected-execution capability.
///
/// # Safety
///
/// The thread, callback context, saved stack offsets, and error-function
/// position must remain valid for the protected operation. The callback must
/// obey the VM unwind protocol and may not let Rust unwinding cross the VM.
#[allow(
    clippy::missing_safety_doc,
    reason = "all methods share the capability-level safety contract"
)]
pub trait ProtectedCall: Sealed {
    unsafe fn raw_run_protected<T>(&self, function: Pfunc<T>, userdata: &mut T) -> VmResult;
    unsafe fn protected_call_internal<T>(
        &self,
        function: Pfunc<T>,
        userdata: &mut T,
        old_top: isize,
        error_function: isize,
    ) -> VmResult;
}

#[repr(C)]
pub struct LuaProtectedErrorFrame {
    _private: [u8; 0],
}

#[repr(C)]
struct ActiveProtectedErrorFrame {
    prev: *mut LuaProtectedErrorFrame,
}

struct ProtectedErrorGuard {
    global: GlobalState,
    prev: *mut LuaProtectedErrorFrame,
}

impl Drop for ProtectedErrorGuard {
    fn drop(&mut self) {
        self.global.set_protected_error(self.prev);
    }
}

impl GlobalState {
    pub fn protected_error(&self) -> *mut LuaProtectedErrorFrame {
        unsafe { self.as_ptr().as_ref().unwrap_unchecked().protected_error }
    }

    pub fn set_protected_error(&self, protected_error: *mut LuaProtectedErrorFrame) {
        unsafe {
            self.as_ptr().as_mut().unwrap_unchecked().protected_error = protected_error;
        }
    }
}

impl ProtectedCall for Thread {
    /// `luaD_rawrunprotected`
    unsafe fn raw_run_protected<T>(&self, function: Pfunc<T>, userdata: &mut T) -> VmResult {
        let global = unsafe { self.global() };
        let mut frame = ActiveProtectedErrorFrame {
            prev: global.protected_error(),
        };
        global.set_protected_error((&raw mut frame).cast());
        let _protected_error_guard = ProtectedErrorGuard {
            global,
            prev: frame.prev,
        };

        unsafe { function(self, userdata) }
    }

    /// `luaD_pcall`
    unsafe fn protected_call_internal<T>(
        &self,
        function: Pfunc<T>,
        userdata: &mut T,
        old_top: isize,
        error_function: isize,
    ) -> VmResult {
        unsafe {
            let old_native_call_depth = self.as_ptr().as_ref().unwrap_unchecked().native_call_depth;
            let old_base_native_call_depth = self
                .as_ptr()
                .as_ref()
                .unwrap_unchecked()
                .base_native_call_depth;
            let old_ci = self.save_ci(self.current_call_info_cursor());
            let old_active = self.as_ptr().as_ref().unwrap_unchecked().is_active;
            let mut result = self.raw_run_protected(function, userdata);

            if let Err(exit) = result {
                let VmExit::Error(mut error) = exit else {
                    return result;
                };
                let mut error_object = error;

                if error_function != 0 {
                    if error != VmError::Runtime {
                        self.set_error_object(error.status(), self.stack_top());
                    }

                    let mut error_context = ErrorFunctionContext {
                        error_function: self.restore_stack(error_function).value_unchecked(),
                    };
                    let error_function_result =
                        self.raw_run_protected(call_error_function, &mut error_context);

                    error_object = match error_function_result {
                        Ok(()) => VmError::Runtime,
                        Err(VmExit::Error(VmError::Memory)) if error == VmError::Memory => {
                            VmError::Memory
                        }
                        Err(VmExit::Error(_)) => {
                            error = VmError::ErrorHandler;
                            VmError::ErrorHandler
                        }
                        Err(exit) => return Err(exit),
                    };

                    result = Err(VmExit::Error(error));
                }

                if !old_active {
                    self.as_ptr().as_mut().unwrap_unchecked().is_active = false;
                }

                let yieldable = self.as_ptr().as_ref().unwrap_unchecked().native_call_depth
                    <= self
                        .as_ptr()
                        .as_ref()
                        .unwrap_unchecked()
                        .base_native_call_depth;
                self.as_ptr().as_mut().unwrap_unchecked().native_call_depth = old_native_call_depth;
                self.as_ptr()
                    .as_mut()
                    .unwrap_unchecked()
                    .base_native_call_depth = old_base_native_call_depth;

                if yieldable
                    && let Some(callback) = self.global().protected_error_callback()
                    && callback(self) == ProtectedErrorAction::Break
                {
                    self.as_ptr().as_mut().unwrap_unchecked().status = THREAD_STATUS_BREAK;
                    return Err(VmExit::Control(VmControl::Break));
                }

                let restored_old_top = self.restore_stack(old_top);
                let restored_ci = self.restore_ci(old_ci);
                if self.open_upvalue().is_some() {
                    self.close(restored_old_top.value_unchecked());
                }

                self.set_error_object(error_object.status(), restored_old_top);
                self.set_current_call_info(restored_ci);
                self.set_stack_base(restored_ci.call_info_unchecked().base());
                self.restore_stack_limit()?;
            }

            result
        }
    }
}