use lua_types::{
error::LuaError,
value::LuaValue,
LuaType,
LuaStatus,
};
use crate::state_stub::{LuaState, LuaStateStubExt as _};
const SPACECHARS: &[u8] = b" \x0c\n\r\t\x0b";
const RESERVED_SLOT: i32 = 5;
const LUA_VERSION_STR: &[u8] = b"Lua 5.4";
const LUA_GNAME: &[u8] = b"_G";
const LUA_MULTRET: i32 = -1;
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GcOp {
Stop = 0,
Restart = 1,
Collect = 2,
Count = 3,
#[expect(dead_code, reason = "ported stdlib helper; not yet wired into the runtime")]
CountB = 4,
Step = 5,
SetPause = 6,
SetStepMul = 7,
IsRunning = 9,
Gen = 10,
Inc = 11,
}
pub(crate) type LuaLibFn = fn(&mut LuaState) -> Result<usize, LuaError>;
fn push_mode(state: &mut LuaState, oldmode: i32) -> Result<usize, LuaError> {
if oldmode == -1 {
state.push(LuaValue::Nil);
} else {
let s: &[u8] = if oldmode == GcOp::Inc as i32 {
b"incremental"
} else {
b"generational"
};
state.push_string(s)?;
}
Ok(1)
}
fn finish_pcall(state: &mut LuaState, ok: bool, extra: i32) -> Result<usize, LuaError> {
if !ok {
state.push(LuaValue::Bool(false));
state.push_copy(-2)?;
return Ok(2);
}
Ok((state.top() as i32 - extra) as usize)
}
fn b_str2int(s: &[u8], base: u32) -> Option<(usize, i64)> {
let mut pos = 0usize;
while pos < s.len() && SPACECHARS.contains(&s[pos]) {
pos += 1;
}
let neg = if pos < s.len() && s[pos] == b'-' {
pos += 1;
true
} else {
if pos < s.len() && s[pos] == b'+' {
pos += 1;
}
false
};
if pos >= s.len() || !s[pos].is_ascii_alphanumeric() {
return None;
}
let mut n: u64 = 0u64;
loop {
let byte = s[pos];
let digit = if byte.is_ascii_digit() {
(byte - b'0') as u32
} else {
(byte.to_ascii_uppercase() - b'A') as u32 + 10
};
if digit >= base {
return None;
}
n = n.wrapping_mul(base as u64).wrapping_add(digit as u64);
pos += 1;
if pos >= s.len() || !s[pos].is_ascii_alphanumeric() {
break;
}
}
while pos < s.len() && SPACECHARS.contains(&s[pos]) {
pos += 1;
}
let value: i64 = if neg {
0u64.wrapping_sub(n) as i64
} else {
n as i64
};
Some((pos, value))
}
fn load_aux(state: &mut LuaState, status_ok: bool, envidx: i32) -> Result<usize, LuaError> {
if status_ok {
if envidx != 0 {
state.push_copy(envidx)?;
if state.set_upvalue(-2, 1)?.is_none() {
state.pop_n(1);
}
}
Ok(1)
} else {
state.push(LuaValue::Nil);
state.insert(-2)?;
Ok(2)
}
}
pub(crate) fn print_fn(state: &mut LuaState) -> Result<usize, LuaError> {
let n = state.top();
for i in 1..=n {
let display_ref = state.to_display_string(i)?;
if i > 1 {
state.write_output(b"\t")?;
}
let bytes = display_ref.clone();
state.write_output(&bytes)?;
state.pop_n(1);
}
state.write_output(b"\n")?;
Ok(0)
}
pub(crate) fn warn_fn(state: &mut LuaState) -> Result<usize, LuaError> {
let n = state.top();
state.check_arg_string(1)?;
for i in 2..=n {
state.check_arg_string(i)?;
}
for i in 1..n {
let s: Vec<u8> = state
.to_lua_string_bytes(i)
.map(|b| b.to_vec())
.unwrap_or_default();
state.warning(&s, true)?;
}
let s: Vec<u8> = state
.to_lua_string_bytes(n)
.map(|b| b.to_vec())
.unwrap_or_default();
state.warning(&s, false)?;
Ok(0)
}
pub(crate) fn tonumber_fn(state: &mut LuaState) -> Result<usize, LuaError> {
if matches!(state.type_at(2), LuaType::None | LuaType::Nil) {
if state.type_at(1) == LuaType::Number {
lua_vm::api::set_top(state, 1)?;
return Ok(1);
}
if let Some(len) = state.to_lua_string_len(1) {
if let Some(consumed) = state.string_to_number(1) {
if consumed == len + 1 {
return Ok(1);
}
}
}
state.check_arg_any(1)?;
} else {
let base = state.check_arg_integer(2)?;
state.check_arg_type(1, LuaType::String)?;
let bytes: Vec<u8> = state
.to_lua_string_bytes(1)
.map(|b| b.to_vec())
.unwrap_or_default();
if !(2..=36).contains(&base) {
return Err(LuaError::arg_error(2, "base out of range"));
}
if let Some((consumed, n)) = b_str2int(&bytes, base as u32) {
if consumed == bytes.len() {
state.push(LuaValue::Int(n));
return Ok(1);
}
}
}
state.push(LuaValue::Nil);
Ok(1)
}
pub(crate) fn error_fn(state: &mut LuaState) -> Result<usize, LuaError> {
let level = state.opt_arg_integer(2, 1)? as i32;
lua_vm::api::set_top(state, 1)?;
if state.type_at(1) == LuaType::String && level > 0 {
state.push_where(level)?;
state.push_copy(1)?;
state.concat(2)?;
}
Err(LuaError::from_value(state.pop()))
}
pub(crate) fn getmetatable_fn(state: &mut LuaState) -> Result<usize, LuaError> {
state.check_arg_any(1)?;
if !state.get_metatable(1)? {
state.push(LuaValue::Nil);
return Ok(1);
}
state.get_metafield(1, b"__metatable")?;
Ok(1)
}
pub(crate) fn setmetatable_fn(state: &mut LuaState) -> Result<usize, LuaError> {
let t = state.type_at(2);
state.check_arg_type(1, LuaType::Table)?;
if !(t == LuaType::Nil || t == LuaType::Table) {
let got = state.value_at(2);
return Err(LuaError::type_arg_error(2, "nil or table", &got));
}
if state.get_metafield(1, b"__metatable")? != LuaType::Nil {
return Err(LuaError::runtime(format_args!(
"cannot change a protected metatable"
)));
}
lua_vm::api::set_top(state, 2)?;
state.set_metatable(1)?;
Ok(1)
}
pub(crate) fn rawequal_fn(state: &mut LuaState) -> Result<usize, LuaError> {
state.check_arg_any(1)?;
state.check_arg_any(2)?;
let eq = state.raw_equal(1, 2)?;
state.push(LuaValue::Bool(eq));
Ok(1)
}
pub(crate) fn rawlen_fn(state: &mut LuaState) -> Result<usize, LuaError> {
let t = state.type_at(1);
if !(t == LuaType::Table || t == LuaType::String) {
let got = state.value_at(1);
return Err(LuaError::type_arg_error(1, "table or string", &got));
}
let len = state.raw_len(1);
state.push(LuaValue::Int(len));
Ok(1)
}
pub(crate) fn rawget_fn(state: &mut LuaState) -> Result<usize, LuaError> {
state.check_arg_type(1, LuaType::Table)?;
state.check_arg_any(2)?;
lua_vm::api::set_top(state, 2)?;
state.raw_get(1)?;
Ok(1)
}
pub(crate) fn rawset_fn(state: &mut LuaState) -> Result<usize, LuaError> {
state.check_arg_type(1, LuaType::Table)?;
state.check_arg_any(2)?;
state.check_arg_any(3)?;
lua_vm::api::set_top(state, 3)?;
state.raw_set(1)?;
Ok(1)
}
pub(crate) fn collectgarbage_fn(state: &mut LuaState) -> Result<usize, LuaError> {
static OPTS: &[&[u8]] = &[
b"stop", b"restart", b"collect",
b"count", b"step", b"setpause", b"setstepmul",
b"isrunning", b"generational", b"incremental",
];
static OPTS_NUM: &[GcOp] = &[
GcOp::Stop, GcOp::Restart, GcOp::Collect,
GcOp::Count, GcOp::Step, GcOp::SetPause, GcOp::SetStepMul,
GcOp::IsRunning, GcOp::Gen, GcOp::Inc,
];
let idx = state.check_arg_option(1, Some(b"collect"), OPTS)?;
let op = OPTS_NUM[idx];
let valid: bool = match op {
GcOp::Count => {
let k = state.gc_count()?;
let b = state.gc_count_b()?;
if k == -1 {
false
} else {
state.push(LuaValue::Float(k as f64 + b as f64 / 1024.0));
return Ok(1);
}
}
GcOp::Step => {
let step = state.opt_arg_integer(2, 0)? as i32;
let res = state.gc_step(step)?;
if res == -1 {
false
} else {
state.push(LuaValue::Bool(res != 0));
return Ok(1);
}
}
GcOp::SetPause | GcOp::SetStepMul => {
let p = state.opt_arg_integer(2, 0)? as i32;
let previous = state.gc_set_param(op as i32, p)?;
if previous == -1 {
false
} else {
state.push(LuaValue::Int(previous as i64));
return Ok(1);
}
}
GcOp::IsRunning => {
let res = state.gc_is_running()?;
state.push(LuaValue::Bool(res));
return Ok(1);
}
GcOp::Gen => {
let minormul = state.opt_arg_integer(2, 0)? as i32;
let majormul = state.opt_arg_integer(3, 0)? as i32;
let oldmode = state.gc_gen(minormul, majormul)?;
return push_mode(state, oldmode);
}
GcOp::Inc => {
let pause = state.opt_arg_integer(2, 0)? as i32;
let stepmul = state.opt_arg_integer(3, 0)? as i32;
let stepsize = state.opt_arg_integer(4, 0)? as i32;
let oldmode = state.gc_inc(pause, stepmul, stepsize)?;
return push_mode(state, oldmode);
}
_ => {
let res = state.gc_control_simple(op as i32)?;
if res == -1 {
false
} else {
state.push(LuaValue::Int(res as i64));
return Ok(1);
}
}
};
debug_assert!(!valid, "valid arms return early; reaching here means checkvalres fired");
state.push(LuaValue::Nil);
Ok(1)
}
pub(crate) fn type_fn(state: &mut LuaState) -> Result<usize, LuaError> {
let t = state.type_at(1);
if t == LuaType::None {
return Err(LuaError::arg_error(1, "value expected"));
}
let name: Vec<u8> = state.type_name(t).to_vec();
state.push_string(&name)?;
Ok(1)
}
pub(crate) fn next_fn(state: &mut LuaState) -> Result<usize, LuaError> {
state.check_arg_type(1, LuaType::Table)?;
lua_vm::api::set_top(state, 2)?;
if state.table_next(1)? {
Ok(2)
} else {
state.push(LuaValue::Nil);
Ok(1)
}
}
fn pairs_cont(_state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
Ok(3)
}
pub(crate) fn pairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
state.check_arg_any(1)?;
if state.get_metafield(1, b"__pairs")? == LuaType::Nil {
state.push_c_function(next_fn)?;
state.push_copy(1)?;
state.push(LuaValue::Nil);
} else {
state.push_copy(1)?;
state.call_k(1, 3, 0, Some(pairs_cont))?;
}
Ok(3)
}
fn ipairs_aux(state: &mut LuaState) -> Result<usize, LuaError> {
let i = state.check_arg_integer(2)?;
let i = (i as u64).wrapping_add(1u64) as i64;
state.push(LuaValue::Int(i));
let t = state.get_i(1, i)?;
if t == LuaType::Nil {
Ok(1)
} else {
Ok(2)
}
}
pub(crate) fn ipairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
state.check_arg_any(1)?;
state.push_c_function(ipairs_aux)?;
state.push_copy(1)?;
state.push(LuaValue::Int(0));
Ok(3)
}
pub(crate) fn loadfile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
let mode: Option<Vec<u8>> = state.opt_arg_lstring(2, None)?;
let env = if state.type_at(3) != LuaType::None { 3 } else { 0 };
let status_ok = state.load_file_ex(fname.as_deref(), mode.as_deref())?;
load_aux(state, status_ok, env)
}
fn generic_reader(state: &mut LuaState) -> Result<Option<Vec<u8>>, LuaError> {
state.ensure_stack(2, b"too many nested functions")?;
state.push_copy(1)?;
state.call(0, 1)?;
if state.type_at(-1) == LuaType::Nil {
state.pop_n(1);
return Ok(None);
}
if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
return Err(LuaError::runtime(format_args!(
"reader function must return a string"
)));
}
state.replace(RESERVED_SLOT)?;
let bytes = state
.to_lua_string_bytes(RESERVED_SLOT)
.map(|b| b.to_vec());
Ok(bytes)
}
pub(crate) fn load_fn(state: &mut LuaState) -> Result<usize, LuaError> {
let is_string = matches!(state.type_at(1), LuaType::String | LuaType::Number);
let mode: Vec<u8> = state.opt_arg_string(3, b"bt")?;
let env = if state.type_at(4) != LuaType::None { 4 } else { 0 };
let status_ok = if is_string {
let chunk: Vec<u8> = state.to_lua_string_bytes(1).unwrap_or_default();
let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
chunk.clone()
} else {
state.check_arg_string(2)?
};
state.load_buffer_ex(&chunk, &chunkname, &mode)?
} else {
let chunkname: Vec<u8> = state
.opt_arg_string_bytes(2)
.unwrap_or_else(|_| b"=(load)".to_vec());
state.check_arg_type(1, LuaType::Function)?;
lua_vm::api::set_top(state, RESERVED_SLOT)?;
state.load_with_reader(generic_reader, &chunkname, &mode)?
};
load_aux(state, status_ok, env)
}
fn dofile_cont(state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
Ok((state.top() as i32 - 1) as usize)
}
pub(crate) fn dofile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
lua_vm::api::set_top(state, 1)?;
if !state.load_file(fname.as_deref())? {
return Err(LuaError::from_value(state.pop()));
}
state.call_k(0, LUA_MULTRET, 0, Some(dofile_cont))?;
dofile_cont(state, 0, 0)
}
pub(crate) fn assert_fn(state: &mut LuaState) -> Result<usize, LuaError> {
if state.to_boolean(1) {
return Ok(state.top() as usize);
}
state.check_arg_any(1)?;
state.remove(1)?;
state.push_string(b"assertion failed!")?;
lua_vm::api::set_top(state, 1)?;
error_fn(state)
}
pub(crate) fn select_fn(state: &mut LuaState) -> Result<usize, LuaError> {
let n = state.top() as i64;
let first_is_hash = state.type_at(1) == LuaType::String && {
state
.to_lua_string_bytes(1)
.and_then(|b| b.first().copied())
== Some(b'#')
};
if first_is_hash {
state.push(LuaValue::Int(n - 1));
return Ok(1);
}
let mut i = state.check_arg_integer(1)?;
if i < 0 {
i = n + i;
} else if i > n {
i = n;
}
if i < 1 {
return Err(LuaError::arg_error(1, "index out of range"));
}
Ok((n - i) as usize)
}
pub(crate) fn pcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
state.check_arg_any(1)?;
state.push(LuaValue::Bool(true));
state.insert(1)?;
let nargs = state.top() as i32 - 2;
let yieldable = state.is_yieldable();
let ok = match state.protected_call_k(nargs, LUA_MULTRET, 0, 0, Some(finish_pcall_k)) {
Ok(()) => true,
Err(LuaError::Yield) => return Err(LuaError::Yield),
Err(e) if yieldable => return Err(e),
Err(e) => {
state.push(e.into_value());
false
}
};
finish_pcall(state, ok, 0)
}
fn finish_pcall_k(state: &mut LuaState, status: i32, extra: isize) -> Result<usize, LuaError> {
let ok = status == LuaStatus::Ok as i32 || status == LuaStatus::Yield as i32;
finish_pcall(state, ok, extra as i32)
}
pub(crate) fn xpcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
let n = state.top() as i32;
state.check_arg_type(2, LuaType::Function)?;
state.push(LuaValue::Bool(true));
state.push_copy(1)?;
state.rotate(3, 2)?;
let yieldable = state.is_yieldable();
let ok = match state.protected_call_k(n - 2, LUA_MULTRET, 2, 2, Some(finish_pcall_k)) {
Ok(()) => true,
Err(LuaError::Yield) => return Err(LuaError::Yield),
Err(e) if yieldable => return Err(e),
Err(e) => {
state.push(e.into_value());
false
}
};
finish_pcall(state, ok, 2)
}
pub(crate) fn tostring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
state.check_arg_any(1)?;
state.to_display_string(1)?;
Ok(1)
}
pub(crate) const BASE_FUNCS: &[(&[u8], LuaLibFn)] = &[
(b"assert", assert_fn),
(b"collectgarbage", collectgarbage_fn),
(b"dofile", dofile_fn),
(b"error", error_fn),
(b"getmetatable", getmetatable_fn),
(b"ipairs", ipairs_fn),
(b"loadfile", loadfile_fn),
(b"load", load_fn),
(b"next", next_fn),
(b"pairs", pairs_fn),
(b"pcall", pcall_fn),
(b"print", print_fn),
(b"warn", warn_fn),
(b"rawequal", rawequal_fn),
(b"rawlen", rawlen_fn),
(b"rawget", rawget_fn),
(b"rawset", rawset_fn),
(b"select", select_fn),
(b"setmetatable", setmetatable_fn),
(b"tonumber", tonumber_fn),
(b"tostring", tostring_fn),
(b"type", type_fn),
(b"xpcall", xpcall_fn),
];
pub fn open(state: &mut LuaState) -> Result<usize, LuaError> {
state.push_globals()?;
state.set_funcs(BASE_FUNCS, 0)?;
state.push_copy(-1)?;
state.set_field(-2, LUA_GNAME)?;
state.push_string(LUA_VERSION_STR)?;
state.set_field(-2, b"_VERSION")?;
Ok(1)
}