use luau_vm::thread::{
LUA_COERR, LUA_COFIN, LUA_CONOR, LUA_CORUN, LUA_COSUS, LUA_GLOBALS_INDEX, LUA_MULTRET,
StackGuard,
};
use luau_vm::{Thread as VmThread, VmControl, VmError, VmExit};
use super::Thread;
use crate::error::Error;
use crate::function::Function;
use crate::lua::{ChunkLoad, Lua, LuaRef};
use crate::table::Table;
use crate::value::{FromLuaMulti, IntoLua, IntoLuaMulti};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ThreadStatus {
Resumable,
Running,
Normal,
Finished,
Error,
}
impl Lua {
pub fn create_thread<'lua>(
&'lua self,
function: Function<'lua>,
) -> Result<Thread<'lua>, Error> {
self.lua_ref().create_thread(function)
}
}
impl<'lua> LuaRef<'lua> {
pub fn create_thread(&self, function: Function<'lua>) -> Result<Thread<'lua>, Error> {
if !unsafe { self.as_vm().same_vm(function.thread().as_vm()) } {
return Err(Error::foreign_lua_handle());
}
function.into_thread()
}
}
impl<'lua> Thread<'lua> {
pub(crate) fn create_empty_thread(&self) -> Result<Thread<'lua>, Error> {
unsafe {
let vm_thread = self.as_vm();
let _stack = StackGuard::new(vm_thread);
let thread = vm_thread
.new_thread()
.map_err(|exit| Error::from_thread_exit(vm_thread, exit))?;
let pointer = vm_thread.to_pointer(-1);
let reference = vm_thread
.ref_value(-1)
.map_err(|exit| Error::from_thread_exit(vm_thread, exit))?;
Ok(Thread::from_ref(
self.reference_thread(),
self.runtime(),
thread,
reference,
pointer,
))
}
}
pub fn status(&self) -> ThreadStatus {
match unsafe { self.reference_thread().co_status(self.as_vm()) } {
LUA_CORUN => ThreadStatus::Running,
LUA_COSUS => ThreadStatus::Resumable,
LUA_CONOR => ThreadStatus::Normal,
LUA_COFIN => ThreadStatus::Finished,
LUA_COERR => ThreadStatus::Error,
status => unreachable!("unexpected coroutine status {status}"),
}
}
pub fn is_resumable(&self) -> bool {
self.status() == ThreadStatus::Resumable
}
pub fn is_running(&self) -> bool {
self.status() == ThreadStatus::Running
}
pub fn is_normal(&self) -> bool {
self.status() == ThreadStatus::Normal
}
pub fn is_finished(&self) -> bool {
self.status() == ThreadStatus::Finished
}
pub fn is_error(&self) -> bool {
self.status() == ThreadStatus::Error
}
pub fn reset(&self, function: Function<'lua>) -> Result<(), Error> {
if matches!(self.status(), ThreadStatus::Running | ThreadStatus::Normal) {
return Err(Error::runtime("cannot reset an active coroutine"));
}
if !self.same_vm(function.thread().as_vm()) {
return Err(Error::foreign_lua_handle());
}
unsafe {
self.as_vm()
.reset()
.map_err(|error| Error::from_thread_exit(self.as_vm(), error))?;
}
function.push_to(self)
}
pub(crate) fn load_body(
&self,
chunk_name: impl AsRef<[u8]>,
bytecode: impl AsRef<[u8]>,
environment: Option<&Table<'_>>,
load: ChunkLoad,
) -> Result<(), Error> {
unsafe {
let thread = self.as_vm();
if let Err(error) =
self.load_chunk(chunk_name.as_ref(), bytecode.as_ref(), environment, load)
{
thread.restore_top(0);
return Err(error);
}
}
Ok(())
}
pub fn resume<'thread, R>(&'thread self, args: impl IntoLuaMulti<'thread>) -> Result<R, Error>
where
R: FromLuaMulti<'thread>,
{
if !self.is_resumable() {
return Err(Error::CoroutineUnresumable);
}
unsafe {
let vm_thread = self.as_vm();
let stack = StackGuard::new(vm_thread);
let arg_count =
i32::try_from(args.push_into_stack_multi(self)?).map_err(|_| Error::StackError)?;
let resumed = self
.runtime()
.with_current_thread(|from| vm_thread.resume(Some(from), arg_count));
match resumed {
Ok(()) | Err(VmExit::Control(VmControl::Yield)) => {}
Err(VmExit::Error(error)) => {
let error = error_with_traceback(vm_thread, error);
stack.dismiss();
vm_thread.restore_top(0);
return Err(error);
}
Err(VmExit::Control(VmControl::Break)) => {
stack.dismiss();
vm_thread.restore_top(0);
return Err(Error::Interrupted);
}
}
stack.dismiss();
let result_count = vm_thread.get_top();
let result = R::from_stack_multi(self, 0, result_count);
vm_thread.restore_top(0);
result
}
}
pub fn resume_error<'thread, R>(&'thread self, error: impl IntoLua<'thread>) -> Result<R, Error>
where
R: FromLuaMulti<'thread>,
{
if !self.is_resumable() {
return Err(Error::CoroutineUnresumable);
}
unsafe {
let vm_thread = self.as_vm();
let stack = StackGuard::new(vm_thread);
error.push_into_stack(self)?;
let resumed = self
.runtime()
.with_current_thread(|from| vm_thread.resume_error(Some(from)));
match resumed {
Ok(()) | Err(VmExit::Control(VmControl::Yield)) => {}
Err(VmExit::Error(error)) => {
let error = error_with_traceback(vm_thread, error);
stack.dismiss();
vm_thread.restore_top(0);
return Err(error);
}
Err(VmExit::Control(VmControl::Break)) => {
stack.dismiss();
vm_thread.restore_top(0);
return Err(Error::Interrupted);
}
}
stack.dismiss();
let result_count = vm_thread.get_top();
let result = R::from_stack_multi(self, 0, result_count);
vm_thread.restore_top(0);
result
}
}
pub(crate) fn load_bytecode(
&self,
chunk_name: impl AsRef<[u8]>,
bytecode: impl AsRef<[u8]>,
environment: Option<&Table<'_>>,
load: ChunkLoad,
) -> Result<Function<'lua>, Error> {
unsafe {
let thread = self.as_vm();
let _stack = StackGuard::new(thread);
self.load_chunk(chunk_name.as_ref(), bytecode.as_ref(), environment, load)?;
let pointer = thread.to_pointer(-1);
let reference = match thread.ref_value(-1) {
Ok(reference) => reference,
Err(exit) => return Err(Error::from_thread_exit(thread, exit)),
};
Ok(Function::from_ref(
self.reference_thread(),
self.runtime(),
reference,
pointer,
))
}
}
pub(crate) fn run_bytecode_with_args<R>(
&self,
chunk_name: impl AsRef<[u8]>,
bytecode: impl AsRef<[u8]>,
args: impl IntoLuaMulti<'lua>,
environment: Option<&Table<'_>>,
load: ChunkLoad,
) -> Result<R, Error>
where
R: FromLuaMulti<'lua>,
{
unsafe {
let vm_thread = self.as_vm();
let stack = StackGuard::new(vm_thread);
self.load_chunk(chunk_name.as_ref(), bytecode.as_ref(), environment, load)?;
let arg_count =
i32::try_from(args.push_into_stack_multi(self)?).map_err(|_| Error::StackError)?;
vm_thread
.protected_call(arg_count, LUA_MULTRET, 0)
.map_err(|exit| Error::from_thread_exit(vm_thread, exit))?;
let result_count = vm_thread.get_top() - stack.top();
R::from_stack_multi(self, stack.top(), result_count)
}
}
fn load_chunk(
&self,
chunk_name: &[u8],
bytecode: &[u8],
environment: Option<&Table<'_>>,
load: ChunkLoad,
) -> Result<(), Error> {
unsafe {
let thread = self.as_vm();
let environment_index = match environment {
Some(environment) => {
environment.push_to(self)?;
thread.get_top()
}
None => {
thread
.push_value(LUA_GLOBALS_INDEX)
.map_err(|error| Error::from_thread_exit(thread, error))?;
thread.get_top()
}
};
let uses_current_environment =
thread.raw_equal(environment_index, LUA_GLOBALS_INDEX) != 0;
load.before_load(
self.runtime(),
thread,
environment_index,
uses_current_environment,
);
thread
.load(chunk_name, bytecode, environment_index)
.map_err(|exit| Error::from_thread_exit(thread, exit))?;
load.loaded(self.runtime(), uses_current_environment);
thread.remove(environment_index);
}
Ok(())
}
}
fn error_with_traceback(thread: &VmThread, error: VmError) -> Error {
let mut error = Error::from_thread_exit(thread, error);
let Error::RuntimeError(message) = &mut error else {
return error;
};
let Ok(traceback) = (unsafe { thread.debug_trace() }) else {
return error;
};
let traceback = String::from_utf8_lossy(traceback.as_slice());
let traceback = traceback.trim();
if !traceback.is_empty() {
message.push_str("\nstack traceback:\n");
message.push_str(traceback);
}
error
}