Files
moonwell-client-core/crates/benilla-lua/src/api.rs
T
Sam Sung 826194e2ac benilla-lua's strings and numbers read and print as the client's (#1773)
benilla-lua's string and number library now answers as the client's:
- `string.byte` returns one byte (`0x7fc210`).
- `gsub` takes only a string or a function (`0x7fd163`), and `%0` raises (`0x7fce20`).
- `lower` and `upper` case-map UTF-8 (`0x7fbeb0`).
- `tonumber` with a base is MSVC's 32-bit `strtoul` (`0x40db09`), on every platform.
- Chunk names cut at a NUL and at `\n` (`0x835144`).
- `string.format` is the client's native one (`0x7fd3d0`): `__ftol` conversions, `%q`'s table, one-digit `n$`, and the client's errors.
- Number text is the MSVC CRT's, ported down to `$I10_OUTPUT`: `1.#INF`, `-1.#IND`, `1e+020`.
- A NaN made from numbers is the x87 indefinite, and the math library returns the runtime's own answers at infinities.

A fuzz against the client's own `sprintf`, run in emulation, matches 1.5M cases. Every fix fails its test without it. `tostring` and `%g` got faster.

Fixes #1367
Fixes #1368
Fixes #1373
Fixes #1375
Fixes #1376
Fixes #1450
Fixes #1481
Fixes #1486
Fixes #1770

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:51:19 +07:00

275 lines
9.2 KiB
Rust

//! What a native function sees: 5.1's `lauxlib` checks and results, over [`Vm`].
use crate::heap::{FuncObj, NativeFn};
use crate::value::{StrRef, TableRef, Value, V};
use crate::vm::{LuaError, Vm};
pub type R<T> = Result<T, LuaError>;
/// 2^63, where a C conversion to a 64-bit integer runs out.
const TWO63: f64 = (1u64 << 63) as f64;
/// 5.1's `lua_number2integer` on x86-64: C's `(ptrdiff_t)` cast, `cvttsd2si`, which truncates
/// toward zero and answers `INT64_MIN` for a NaN or an out-of-range value. The x86-64 Linux C
/// build's answer on every platform (arm64 saturates). The client's MSVC x86-32 conversions are
/// later work.
pub fn c_integer(n: f64) -> i64 {
if n.is_nan() || !(-TWO63..TWO63).contains(&n) {
i64::MIN
} else {
n as i64
}
}
/// 5.1's `luaL_checkint`: `(int)` of [`c_integer`], its low 32 bits. The client's `__ftol`
/// (`0x40a2b0`) answers the same: a `fistp` to a qword, rounding set to chop, and the low dword,
/// 0 for what the qword cannot hold.
pub fn c_int(n: f64) -> i32 {
c_integer(n) as i32
}
/// The longest string the libraries build, `INT32_MAX` bytes: more than the 1.12 client's 32-bit
/// address space could hold. Deviation: the C build grows a string while `realloc` succeeds, and
/// a failure is the catchable `not enough memory`; Rust's allocator aborts the process instead,
/// and Linux grants far more than it can fill, so past this bound the libraries raise C's message.
pub const MAX_STRING: usize = i32::MAX as usize;
/// 5.1's `LUAI_MAXCSTACK`: the most values a native's frame may hold.
pub const MAX_C_STACK: usize = 8000;
impl Vm {
/// 5.1's memory error: `not enough memory`, with no position (`luaD_throw(L, LUA_ERRMEM)`).
/// It is thrown as it is: only `luaG_errormsg`, a runtime error, runs an `xpcall` handler.
#[cold]
pub fn memory_error(&mut self) -> LuaError {
self.heap.oom = false;
LuaError {
value: Value::Str(self.heap.oom_str),
}
}
/// Room in `buf` for `extra` more bytes of a string being built, within [`MAX_STRING`] and
/// what the allocator grants; else [`Vm::memory_error`].
#[inline]
pub fn reserve(&mut self, buf: &mut Vec<u8>, extra: usize) -> R<()> {
if extra <= buf.capacity() - buf.len() {
return Ok(());
}
self.grow_str(buf, extra)
}
/// [`Vm::memory_error`] once `buf` holds [`MAX_STRING`] bytes, for a string that grows by
/// short pieces.
#[inline]
pub fn check_len(&mut self, buf: &[u8]) -> R<()> {
if buf.len() >= MAX_STRING {
return Err(self.memory_error());
}
Ok(())
}
/// [`Vm::reserve`] past the capacity: doubling, as `Vec` grows, but never past
/// [`MAX_STRING`].
#[cold]
#[inline(never)]
fn grow_str(&mut self, buf: &mut Vec<u8>, extra: usize) -> R<()> {
let need = buf.len().saturating_add(extra);
if need > MAX_STRING {
return Err(self.memory_error());
}
let want = need.max(buf.capacity().saturating_mul(2)).min(MAX_STRING);
if buf.try_reserve_exact(want - buf.len()).is_err() {
return Err(self.memory_error());
}
Ok(())
}
/// 5.1's `lua_checkstack` from a native: room for `n` more values within its frame's
/// `LUAI_MAXCSTACK`, its arguments counted.
pub fn check_stack(&self, n: usize) -> bool {
n <= MAX_C_STACK && self.top - self.native_base() + n <= MAX_C_STACK
}
/// `luaL_checkany`.
pub fn check_any(&mut self, nargs: usize, n: usize) -> R<Value> {
if n > nargs {
return Err(self.arg_error(n, "value expected"));
}
Ok(self.arg(nargs, n - 1))
}
/// `luaL_checktype(L, n, LUA_TTABLE)`.
pub fn check_table(&mut self, nargs: usize, n: usize) -> R<TableRef> {
match self.arg(nargs, n - 1).as_table() {
Some(t) if n <= nargs => Ok(t),
_ => Err(self.type_arg_error(nargs, n, "table")),
}
}
/// `luaL_checktype(L, n, LUA_TFUNCTION)`.
pub fn check_function(&mut self, nargs: usize, n: usize) -> R<Value> {
let v = self.arg(nargs, n - 1);
match v.as_func() {
Some(_) if n <= nargs => Ok(v),
_ => Err(self.type_arg_error(nargs, n, "function")),
}
}
/// `luaL_checknumber`: a number or a numeric string.
#[inline]
pub fn check_number(&mut self, nargs: usize, n: usize) -> R<f64> {
// A number, the common argument, here; a string or an error out of line. An argument
// past `nargs` reads nil.
if let Some(x) = self.arg(nargs, n - 1).as_num() {
return Ok(x);
}
self.check_number_slow(nargs, n)
}
#[inline(never)]
fn check_number_slow(&mut self, nargs: usize, n: usize) -> R<f64> {
let v = self.arg(nargs, n - 1);
match self.tonumber(v) {
Some(x) if n <= nargs => Ok(x),
_ => Err(self.type_arg_error(nargs, n, "number")),
}
}
/// `luaL_optnumber`: absent or nil is the default.
pub fn opt_number(&mut self, nargs: usize, n: usize, def: f64) -> R<f64> {
if self.arg(nargs, n - 1).is_nil() {
Ok(def)
} else {
self.check_number(nargs, n)
}
}
pub fn check_int(&mut self, nargs: usize, n: usize) -> R<i32> {
Ok(c_int(self.check_number(nargs, n)?))
}
pub fn opt_int(&mut self, nargs: usize, n: usize, def: i32) -> R<i32> {
if self.arg(nargs, n - 1).is_nil() {
Ok(def)
} else {
self.check_int(nargs, n)
}
}
/// `luaL_checkinteger`: a `ptrdiff_t`.
pub fn check_integer(&mut self, nargs: usize, n: usize) -> R<i64> {
Ok(c_integer(self.check_number(nargs, n)?))
}
/// `luaL_optinteger`.
pub fn opt_integer(&mut self, nargs: usize, n: usize, def: i64) -> R<i64> {
if self.arg(nargs, n - 1).is_nil() {
Ok(def)
} else {
self.check_integer(nargs, n)
}
}
/// `luaL_checklstring`: a string, or a number converted in its stack slot as
/// `lua_tolstring` converts it.
#[inline]
pub fn check_str(&mut self, nargs: usize, n: usize) -> R<StrRef> {
// A string, the common argument, here: converting it in place would store it unchanged.
// A number or an error out of line. An argument past `nargs` reads nil.
if let Some(s) = self.arg(nargs, n - 1).as_str() {
return Ok(s);
}
self.check_str_slow(nargs, n)
}
#[inline(never)]
fn check_str_slow(&mut self, nargs: usize, n: usize) -> R<StrRef> {
let v = self.arg(nargs, n - 1);
if n <= nargs {
if let Some(s) = self.tostr(v) {
self.set_arg(n - 1, Value::Str(s));
return Ok(s);
}
}
Err(self.type_arg_error(nargs, n, "string"))
}
/// `luaL_optlstring`.
pub fn opt_str(&mut self, nargs: usize, n: usize, def: &[u8]) -> R<StrRef> {
if self.arg(nargs, n - 1).is_nil() {
Ok(self.heap.intern(def))
} else {
self.check_str(nargs, n)
}
}
/// Replace argument `i` (0-based) of the running native.
pub fn set_arg(&mut self, i: usize, v: Value) {
let b = self.native_base();
self.stack[b + i] = v;
}
/// Push one result and answer the count, for `return Ok(vm.ret1(v))`.
#[inline]
pub fn ret1(&mut self, v: Value) -> usize {
self.push(v);
1
}
pub fn ret_str(&mut self, s: &[u8]) -> usize {
let v = self.str(s);
self.ret1(v)
}
/// A native closure with upvalues, which [`Vm::upvalue`] reads.
pub fn new_closure(&mut self, name: &'static str, f: NativeFn, upvals: Vec<Value>) -> Value {
let env = self.globals;
Value::Func(self.heap.new_func(FuncObj::Native {
f,
host: None,
name,
upvals: upvals.into_boxed_slice(),
env,
}))
}
/// The running native closure's upvalue `i`.
pub fn upvalue(&self, i: usize) -> Value {
match self.heap.func(self.running()) {
FuncObj::Native { upvals, .. } => upvals[i],
_ => Value::Nil,
}
}
pub fn set_upvalue(&mut self, i: usize, v: Value) {
let f = self.running();
if let FuncObj::Native { upvals, .. } = self.heap.func_mut(f) {
upvals[i] = v;
}
}
/// `luaL_getmetafield`: a field of `v`'s metatable, raw.
pub fn metafield(&mut self, v: Value, name: &[u8]) -> Value {
match self.heap.metatable(v) {
Some(m) => {
let k = self.str(name);
self.heap.table(m).get(k)
}
None => Value::Nil,
}
}
/// `lua_isstring`: a string or a number.
pub fn is_string(&self, v: Value) -> bool {
matches!(v.unpack(), V::Str(_) | V::Num(_))
}
}
/// Room for `extra` more bytes in a string being built, within `MAX_STRING` and what the
/// allocator grants, growing as `Vec` grows: whether it was granted.
#[cold]
#[inline(never)]
pub(crate) fn grow_fallibly(buf: &mut Vec<u8>, extra: usize) -> bool {
buf.len().saturating_add(extra) <= crate::api::MAX_STRING && buf.try_reserve(extra).is_ok()
}