Skip to main content

lua_stdlib/
auxlib.rs

1//! Auxiliary library: helper functions for building Lua libraries.
2//!
3//! C source: `reference/lua-5.4.7/src/lauxlib.c` (1127 lines, ~50 functions)
4//! Target crate: `lua-stdlib`
5//!
6//! This module provides the high-level `luaL_*` API layer that sits on top of
7//! the raw `lua_*` C API. In Rust we translate each `luaL_*` function as a
8//! free function receiving `&mut LuaState` rather than a method, matching the
9//! structure of the other stdlib modules.
10//!
11//! PORT NOTE: The C buffer system (`luaL_Buffer`) uses a small inline initial
12//! buffer backed by a Lua-stack userdata box on overflow. In Rust we replace
13//! this with a plain `Vec<u8>` (`LuaBuffer`), dropping all the C-internal
14//! `UBox` / `resizebox` / `boxgc` / `boxmt` / `newbox` / `buffonstack`
15//! machinery. The public interface remains compatible.
16//!
17//! PORT NOTE: File-loading functions (`load_filex`) reference `std::fs` which
18//! is banned outside `lua-cli`. Those functions carry `TODO(port)` markers.
19
20// TODO(port): LuaState, LuaValue, LuaError, GcRef, LuaString, LuaUserData,
21// LuaDebug, and LuaType are defined across lua-vm / lua-types. Imports will be
22// wired in Phase B. Using local stubs for Phase A so rustc can parse the file.
23
24use lua_types::{
25    error::LuaError,
26    value::LuaValue,
27    gc::GcRef,
28    string::LuaString,
29    userdata::LuaUserData,
30    LuaType,
31    LuaStatus,
32    arith::ArithOp,
33};
34use crate::state_stub::{LuaState, LuaStateStubExt as _, lua_CFunction, upvalue_index, CompareOp, LuaDebug};
35
36// ── Constants ─────────────────────────────────────────────────────────────────
37
38/// Number of stack frames to show in the first part of a traceback.
39/// C: `#define LEVELS1 10`
40const LEVELS1: i32 = 10;
41
42/// Number of stack frames to show in the second part of a traceback.
43/// C: `#define LEVELS2 11`
44const LEVELS2: i32 = 11;
45
46/// Index (1-based) in the reference table that heads the free-list of recycled
47/// references. Placed after the last predefined registry key.
48/// C: `#define freelist (LUA_RIDX_LAST + 1)` where `LUA_RIDX_LAST = 2`.
49const FREELIST_REF: i64 = 3; // LUA_RIDX_GLOBALS (2) + 1
50
51/// Pseudo-reference returned by `lua_ref` when the pushed value was `nil`.
52/// C: `#define LUA_REFNIL (-1)`
53pub const LUA_REFNIL: i32 = -1;
54
55/// Pseudo-reference meaning "no reference" (never created by `lua_ref`).
56/// C: `#define LUA_NOREF (-2)`
57pub const LUA_NOREF: i32 = -2;
58
59/// Extended error code: file-related I/O error from `load_filex`.
60/// C: `#define LUA_ERRFILE (LUA_ERRERR + 1)` = 6.
61pub const LUA_ERRFILE: i32 = 6;
62
63/// Registry key for the table of loaded modules.
64/// C: `#define LUA_LOADED_TABLE "_LOADED"`
65pub const LUA_LOADED_TABLE: &[u8] = b"_LOADED";
66
67/// Registry key for the table of preloaded loaders.
68/// C: `#define LUA_PRELOAD_TABLE "_PRELOAD"`
69pub const LUA_PRELOAD_TABLE: &[u8] = b"_PRELOAD";
70
71/// Name of the global environment table.
72/// C: `#define LUA_GNAME "_G"`
73pub const LUA_GNAME: &[u8] = b"_G";
74
75/// Metatable name / file-handle key for the IO library.
76/// C: `#define LUA_FILEHANDLE "FILE*"`
77pub const LUA_FILE_HANDLE: &[u8] = b"FILE*";
78
79/// Pseudo-index for the Lua registry.
80/// C: `#define LUA_REGISTRYINDEX (-LUAI_MAXSTACK - 1000)`
81const LUA_REGISTRYINDEX: i32 = -1_001_000;
82
83/// Minimum number of extra stack slots `lua_checkstack` guarantees per call.
84/// C: `LUA_MINSTACK = 20`
85const LUA_MINSTACK: i32 = 20;
86
87// ── Public types ──────────────────────────────────────────────────────────────
88
89/// A function-registration entry for `set_funcs`.
90///
91/// C: `typedef struct luaL_Reg { const char *name; lua_CFunction func; } luaL_Reg;`
92///
93/// In Rust, `name` is `&'static [u8]` (never `&str`). A `None` func is a
94/// placeholder that pushes `false` rather than a closure.
95pub struct LuaReg {
96    pub name: &'static [u8],
97    pub func: Option<fn(&mut LuaState) -> Result<usize, LuaError>>,
98}
99
100/// Growable byte-buffer used by the auxiliary library for building strings.
101///
102/// C: `luaL_Buffer` from `lauxlib.h`
103///
104/// The C version uses a small inline initial buffer with overflow managed via
105/// a Lua-stack userdata box. The Rust port collapses this to a plain `Vec<u8>`.
106/// All buffer mutating functions take `&mut LuaState` as a separate parameter.
107pub struct LuaBuffer {
108    pub data: Vec<u8>,
109}
110
111/// File-stream handle used by the IO library.
112///
113/// C: `luaL_Stream` from `lauxlib.h`
114///
115/// `closef` in C is a `lua_CFunction`. In Rust we store an optional closer.
116// TODO(port): file I/O belongs in lua-stdlib/src/io_lib.rs; this definition
117// may move there. Keeping here to mirror the C header.
118pub struct LuaStream {
119    /// The underlying file handle. `None` for incompletely opened or closed streams.
120    // TODO(port): use a real File type (e.g. `std::fs::File`) in Phase B,
121    // noting std::fs is allowed in lua-stdlib for I/O library support.
122    pub f: Option<Box<dyn std::io::Read>>,
123    /// Optional close function (None for already-closed streams).
124    pub closef: Option<fn(&mut LuaState) -> Result<usize, LuaError>>,
125}
126
127// ── Traceback ─────────────────────────────────────────────────────────────────
128
129/// Search for `objidx` in the table at the top of the stack.
130/// `objidx` must be an absolute API stack index.
131/// Returns `true` (and leaves name string on top) when found.
132///
133/// C: `static int findfield(lua_State *L, int objidx, int level)`
134fn find_field(
135    state: &mut LuaState,
136    objidx: i32,
137    level: i32,
138) -> Result<bool, LuaError> {
139    // C: if (level == 0 || !lua_istable(L, -1)) return 0;
140    if level == 0 || state.type_at(-1) != LuaType::Table {
141        return Ok(false);
142    }
143    // C: lua_pushnil(L);  /* start 'next' loop */
144    state.push(LuaValue::Nil);
145    // C: while (lua_next(L, -2))
146    while state.table_next(-2)? {
147        // C: if (lua_type(L, -2) == LUA_TSTRING)
148        if state.type_at(-2) == LuaType::String {
149            // C: if (lua_rawequal(L, objidx, -1))
150            if state.raw_equal(objidx, -1)? {
151                state.pop_n(1); // remove value (keep name)
152                return Ok(true);
153            } else if find_field(state, objidx, level - 1)? {
154                // stack: lib_name, lib_table, field_name (top)
155                state.push_string(b".")?; // place '.' between the two names
156                state.replace(-3); // in the slot occupied by table
157                state.concat(3)?; // lib_name.field_name
158                return Ok(true);
159            }
160        }
161        state.pop_n(1); // remove value
162    }
163    Ok(false)
164}
165
166/// Search all loaded modules for a global name for the function at `top+1`.
167/// Returns `true` and leaves name string on top (at `top+1`) if found.
168///
169/// C: `static int pushglobalfuncname(lua_State *L, lua_Debug *ar)`
170fn push_global_func_name(
171    state: &mut LuaState,
172    ar: &mut LuaDebug,
173) -> Result<bool, LuaError> {
174    // C: int top = lua_gettop(L);
175    let top = state.top_count();
176    // C: lua_getinfo(L, "f", ar);  /* push function */
177    state.get_info(b"f", ar)?;
178    // C: lua_getfield(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
179    state.get_field(LUA_REGISTRYINDEX, LUA_LOADED_TABLE)?;
180    // C: luaL_checkstack(L, 6, "not enough stack");
181    check_stack(state, 6, Some(b"not enough stack"))?;
182    if find_field(state, top + 1, 2)? {
183        // C: const char *name = lua_tostring(L, -1);
184        // C: if (strncmp(name, LUA_GNAME ".", 3) == 0)
185        if state.peek_bytes(-1).map_or(false, |n| n.starts_with(b"_G.")) {
186            // C: lua_pushstring(L, name + 3); /* push name without prefix */
187            let suffix = state.peek_bytes(-1)
188                .map(|n| n[3..].to_vec())
189                .unwrap_or_default();
190            state.push_bytes(&suffix)?;
191            // C: lua_remove(L, -2); /* remove original name */
192            state.remove(-2)?;
193        }
194        // C: lua_copy(L, -1, top + 1);
195        state.copy_value(-1, top + 1)?;
196        // C: lua_settop(L, top + 1);
197        lua_vm::api::set_top(state, top + 1)?;
198        Ok(true)
199    } else {
200        // C: lua_settop(L, top);
201        lua_vm::api::set_top(state, top)?;
202        Ok(false)
203    }
204}
205
206fn push_global_func_name_from_target(
207    state: &mut LuaState,
208    target: &mut LuaState,
209    ar: &mut LuaDebug,
210) -> Result<bool, LuaError> {
211    let top = state.top_count();
212    target.get_info(b"f", ar)?;
213    let func = target.get_at(target.top_idx() - 1);
214    target.pop_n(1);
215    state.push(func);
216    state.get_field(LUA_REGISTRYINDEX, LUA_LOADED_TABLE)?;
217    check_stack(state, 6, Some(b"not enough stack"))?;
218    if find_field(state, top + 1, 2)? {
219        if state.peek_bytes(-1).map_or(false, |n| n.starts_with(b"_G.")) {
220            let suffix = state.peek_bytes(-1)
221                .map(|n| n[3..].to_vec())
222                .unwrap_or_default();
223            state.push_bytes(&suffix)?;
224            state.remove(-2)?;
225        }
226        state.copy_value(-1, top + 1)?;
227        lua_vm::api::set_top(state, top + 1)?;
228        Ok(true)
229    } else {
230        lua_vm::api::set_top(state, top)?;
231        Ok(false)
232    }
233}
234
235/// Push a human-readable name for the function described by `ar`.
236///
237/// C: `static void pushfuncname(lua_State *L, lua_Debug *ar)`
238fn push_func_name(
239    state: &mut LuaState,
240    ar: &mut LuaDebug,
241    global_lookup_target: Option<&mut LuaState>,
242) -> Result<(), LuaError> {
243    let found_global = match global_lookup_target {
244        Some(target) => push_global_func_name_from_target(state, target, ar)?,
245        None => push_global_func_name(state, ar)?,
246    };
247    if found_global {
248        // C: lua_pushfstring(L, "function '%s'", lua_tostring(L, -1));
249        let name = state.peek_bytes(-1).unwrap_or_else(|| b"?".to_vec());
250        state.push_fstring(format_args!("function '{}'", BStr(&name)))?;
251        // C: lua_remove(L, -2);
252        state.remove(-2)?;
253    } else if !ar.namewhat.is_empty() {
254        // C: lua_pushfstring(L, "%s '%s'", ar->namewhat, ar->name);
255        let namewhat = ar.namewhat.clone();
256        let name = ar.name.clone().unwrap_or_else(|| b"?".to_vec());
257        state.push_fstring(format_args!("{} '{}'", BStr(&namewhat), BStr(&name)))?;
258    } else if ar.what == b'm' {
259        // C: lua_pushliteral(L, "main chunk");
260        state.push_string(b"main chunk")?;
261    } else if ar.what != b'C' {
262        // C: lua_pushfstring(L, "function <%s:%d>", ar->short_src, ar->linedefined);
263        let src = ar.short_src.clone();
264        let line = ar.linedefined;
265        state.push_fstring(format_args!("function <{}:{}>", BStr(&src), line))?;
266    } else {
267        // C: lua_pushliteral(L, "?");
268        state.push_string(b"?")?;
269    }
270    Ok(())
271}
272
273/// Binary-search for the last valid stack level in `state`.
274///
275/// C: `static int lastlevel(lua_State *L)`
276fn last_level(state: &mut LuaState) -> i32 {
277    let mut ar = LuaDebug::default();
278    let mut li: i32 = 1;
279    let mut le: i32 = 1;
280    // C: while (lua_getstack(L, le, &ar)) { li = le; le *= 2; }
281    while state.get_stack(le, &mut ar) {
282        li = le;
283        le *= 2;
284    }
285    // binary search
286    while li < le {
287        let m = (li + le) / 2;
288        if state.get_stack(m, &mut ar) {
289            li = m + 1;
290        } else {
291            le = m;
292        }
293    }
294    le - 1
295}
296
297/// Build a stack traceback string from thread `other` starting at `level`.
298/// If `msg` is non-None it is prepended on its own line.
299/// Leaves the result string on top of `state`.
300///
301/// When `other` is `None`, the traceback is built for `state` itself (the
302/// common single-thread case). Rust's borrow checker forbids passing the same
303/// `&mut LuaState` twice, so we use an `Option` to express the aliasing intent
304/// rather than a separate parameter.
305///
306/// C: `LUALIB_API void luaL_traceback(lua_State *L, lua_State *L1, const char *msg, int level)`
307pub fn traceback(
308    state: &mut LuaState,
309    mut other: Option<&mut LuaState>,
310    msg: Option<&[u8]>,
311    level: i32,
312) -> Result<(), LuaError> {
313    let mut b = LuaBuffer::new();
314    let mut ar = LuaDebug::default();
315    let last = match &mut other {
316        Some(o) => last_level(o),
317        None => last_level(state),
318    };
319    // C: int limit2show = (last - level > LEVELS1 + LEVELS2) ? LEVELS1 : -1;
320    let mut limit2show: i32 = if last - level > LEVELS1 + LEVELS2 { LEVELS1 } else { -1 };
321    buf_init(state, &mut b);
322    if let Some(m) = msg {
323        add_lstring(&mut b, m);
324        add_char(&mut b, b'\n');
325    }
326    add_lstring(&mut b, b"stack traceback:");
327    let mut level = level;
328    loop {
329        let got = match &mut other {
330            Some(o) => o.get_stack(level, &mut ar),
331            None => state.get_stack(level, &mut ar),
332        };
333        if !got {
334            break;
335        }
336        level += 1;
337        if limit2show == 0 {
338            // C: int n = last - level - LEVELS2 + 1;
339            let n = last - level - LEVELS2 + 1;
340            // C: lua_pushfstring(L, "\n\t...\t(skipping %d levels)", n);
341            state.push_fstring(format_args!("\n\t...\t(skipping {} levels)", n))?;
342            add_value(state, &mut b)?;
343            level += n;
344            limit2show = LEVELS2;
345        } else {
346            limit2show -= 1;
347            // C: lua_getinfo(L1, "Slnt", &ar).
348            match &mut other {
349                Some(o) => o.get_info(b"Slnt", &mut ar)?,
350                None => state.get_info(b"Slnt", &mut ar)?,
351            }
352            if ar.currentline <= 0 {
353                // C: lua_pushfstring(L, "\n\t%s: in ", ar.short_src);
354                let src = ar.short_src.clone();
355                state.push_fstring(format_args!("\n\t{}: in ", BStr(&src)))?;
356            } else {
357                // C: lua_pushfstring(L, "\n\t%s:%d: in ", ar.short_src, ar.currentline);
358                let src = ar.short_src.clone();
359                let line = ar.currentline;
360                state.push_fstring(format_args!("\n\t{}:{}: in ", BStr(&src), line))?;
361            }
362            add_value(state, &mut b)?;
363            match &mut other {
364                Some(o) => push_func_name(state, &mut ar, Some(&mut **o))?,
365                None => push_func_name(state, &mut ar, None)?,
366            }
367            add_value(state, &mut b)?;
368            if ar.istailcall {
369                add_lstring(&mut b, b"\n\t(...tail calls...)");
370            }
371        }
372    }
373    push_result(state, &mut b)?;
374    Ok(())
375}
376
377// ── Error-report functions ─────────────────────────────────────────────────────
378
379/// Push an error for argument `arg` with extra message `extramsg`.
380/// Attempts to enrich the message with the calling function's name.
381/// Always returns `Err`.
382///
383/// C: `LUALIB_API int luaL_argerror(lua_State *L, int arg, const char *extramsg)`
384pub fn arg_error(
385    state: &mut LuaState,
386    mut arg: i32,
387    extramsg: &[u8],
388) -> Result<usize, LuaError> {
389    let mut ar = LuaDebug::default();
390    if !state.get_stack(0, &mut ar) {
391        // C: return luaL_error(L, "bad argument #%d (%s)", arg, extramsg);
392        return Err(LuaError::runtime(format_args!(
393            "bad argument #{} ({})",
394            arg,
395            BStr(extramsg)
396        )));
397    }
398    state.get_info(b"n", &mut ar)?;
399    if ar.namewhat == b"method" {
400        arg -= 1; // do not count 'self'
401        if arg == 0 {
402            let name = ar.name.clone().unwrap_or_else(|| b"?".to_vec());
403            return Err(LuaError::runtime(format_args!(
404                "calling '{}' on bad self ({})",
405                BStr(&name),
406                BStr(extramsg)
407            )));
408        }
409    }
410    let fname = if ar.name.is_none() {
411        if push_global_func_name(state, &mut ar)? {
412            state.peek_bytes(-1).unwrap_or_else(|| b"?".to_vec())
413        } else {
414            b"?".to_vec()
415        }
416    } else {
417        ar.name.clone().unwrap_or_else(|| b"?".to_vec())
418    };
419    Err(LuaError::runtime(format_args!(
420        "bad argument #{} to '{}' ({})",
421        arg,
422        BStr(&fname),
423        BStr(extramsg)
424    )))
425}
426
427/// Push a type-mismatch error for argument `arg`, stating `tname` was expected.
428/// Always returns `Err`.
429///
430/// C: `LUALIB_API int luaL_typeerror(lua_State *L, int arg, const char *tname)`
431pub fn type_error_arg(
432    state: &mut LuaState,
433    arg: i32,
434    tname: &[u8],
435) -> Result<usize, LuaError> {
436    // C: if (luaL_getmetafield(L, arg, "__name") == LUA_TSTRING)
437    //      typearg = lua_tostring(L, -1);
438    //    else if (lua_type(L, arg) == LUA_TLIGHTUSERDATA)
439    //      typearg = "light userdata";
440    //    else
441    //      typearg = luaL_typename(L, arg);
442    let typearg: Vec<u8> = if get_metafield(state, arg, b"__name")? == LuaType::String {
443        let bytes = state.peek_bytes(-1).unwrap_or_else(|| b"?".to_vec());
444        state.pop_n(1);
445        bytes
446    } else if state.type_at(arg) == LuaType::LightUserData {
447        b"light userdata".to_vec()
448    } else {
449        state.type_name_at(arg).to_vec()
450    };
451    // C: msg = lua_pushfstring(L, "%s expected, got %s", tname, typearg);
452    // C: return luaL_argerror(L, arg, msg);
453    let msg_owned = format!(
454        "{} expected, got {}",
455        BStr(tname),
456        BStr(&typearg)
457    );
458    arg_error(state, arg, msg_owned.as_bytes())
459}
460
461/// Push a type-tag error for `arg`, using the Lua type name for `tag`.
462///
463/// C: `static void tag_error(lua_State *L, int arg, int tag)`
464fn tag_error(state: &mut LuaState, arg: i32, tag: LuaType) -> Result<(), LuaError> {
465    let name = state.type_name(tag);
466    type_error_arg(state, arg, name)?;
467    Ok(())
468}
469
470/// Push a string describing the location of the call at `level` onto the stack.
471/// If no location is available, pushes an empty string.
472///
473/// C: `LUALIB_API void luaL_where(lua_State *L, int level)`
474pub fn push_where(state: &mut LuaState, level: i32) -> Result<(), LuaError> {
475    let mut ar = LuaDebug::default();
476    if state.get_stack(level, &mut ar) {
477        state.get_info(b"Sl", &mut ar)?;
478        if ar.currentline > 0 {
479            let src = ar.short_src.clone();
480            let line = ar.currentline;
481            state.push_fstring(format_args!("{}:{}: ", BStr(&src), line))?;
482            return Ok(());
483        }
484    }
485    // C: lua_pushfstring(L, "");  /* no information available */
486    state.push_string(b"")?;
487    Ok(())
488}
489
490/// Format a runtime error with source location and raise it.
491/// Always returns `Err`.
492///
493/// C: `LUALIB_API int luaL_error(lua_State *L, const char *fmt, ...)`
494///
495/// PORT NOTE: C uses varargs + `lua_pushvfstring`. Rust callers pass a
496/// pre-formatted `&[u8]` message; use `format_args!` at the call site.
497pub fn lua_error(state: &mut LuaState, msg: &[u8]) -> Result<usize, LuaError> {
498    push_where(state, 1)?;
499    let where_str = state.pop_bytes();
500    let full = [where_str.as_slice(), msg].concat();
501    Err(LuaError::runtime(format_args!("{}", BStr(&full))))
502}
503
504/// Push the result of a POSIX-style file operation onto the stack.
505/// On success pushes `true`; on failure pushes `nil, errmsg, errno`.
506/// Returns the number of pushed values.
507///
508/// C: `LUALIB_API int luaL_fileresult(lua_State *L, int stat, const char *fname)`
509pub fn file_result(
510    state: &mut LuaState,
511    stat: bool,
512    fname: Option<&[u8]>,
513) -> Result<usize, LuaError> {
514    if stat {
515        // C: lua_pushboolean(L, 1);
516        state.push(LuaValue::Bool(true));
517        Ok(1)
518    } else {
519        // C: luaL_pushfail(L); = lua_pushnil
520        state.push(LuaValue::Nil);
521        // TODO(port): use std::io::Error::last_os_error() for errno-style message.
522        let errmsg = b"(errno unavailable in Rust port)".to_vec();
523        if let Some(name) = fname {
524            let full = [name, b": ".as_slice(), &errmsg].concat();
525            state.push_bytes(&full)?;
526        } else {
527            state.push_bytes(&errmsg)?;
528        }
529        // C: lua_pushinteger(L, en);
530        // TODO(port): push actual errno integer once os-error helpers are available.
531        state.push(LuaValue::Int(0));
532        Ok(3)
533    }
534}
535
536/// Push the result of a process-exit status onto the stack.
537/// Returns 3 values: success-bool-or-nil, exit-kind string, status code.
538///
539/// C: `LUALIB_API int luaL_execresult(lua_State *L, int stat)`
540// TODO(port): POSIX WIFEXITED / WIFSIGNALED inspection requires cfg(unix).
541pub fn exec_result(state: &mut LuaState, stat: i32) -> Result<usize, LuaError> {
542    if stat != 0 {
543        return file_result(state, false, None);
544    }
545    // C: const char *what = "exit";
546    let what = b"exit".as_slice();
547    // C: if (*what == 'e' && stat == 0) lua_pushboolean(L, 1);
548    state.push(LuaValue::Bool(true));
549    state.push_bytes(what)?;
550    state.push(LuaValue::Int(stat as i64));
551    Ok(3)
552}
553
554// ── Userdata / metatable helpers ──────────────────────────────────────────────
555
556/// Create a new metatable for type `tname` and register it in the registry.
557/// Returns `true` (and leaves new metatable on stack) if the table was created;
558/// returns `false` (and leaves existing table on stack) if already existed.
559///
560/// C: `LUALIB_API int luaL_newmetatable(lua_State *L, const char *tname)`
561pub fn new_metatable(state: &mut LuaState, tname: &[u8]) -> Result<bool, LuaError> {
562    // C: if (luaL_getmetatable(L, tname) != LUA_TNIL)  return 0;
563    if get_metatable(state, tname)? != LuaType::Nil {
564        return Ok(false); // leave previous value on top
565    }
566    state.pop_n(1);
567    // C: lua_createtable(L, 0, 2);
568    state.create_table(0, 2)?;
569    // C: lua_pushstring(L, tname); lua_setfield(L, -2, "__name");
570    state.push_bytes(tname)?;
571    state.set_field(-2, b"__name")?;
572    // C: lua_pushvalue(L, -1); lua_setfield(L, LUA_REGISTRYINDEX, tname);
573    state.push_value(-1)?;
574    state.set_field(LUA_REGISTRYINDEX, tname)?;
575    Ok(true)
576}
577
578/// Set the metatable of the value at stack top to the one registered as `tname`.
579///
580/// C: `LUALIB_API void luaL_setmetatable(lua_State *L, const char *tname)`
581pub fn set_metatable(state: &mut LuaState, tname: &[u8]) -> Result<(), LuaError> {
582    // C: luaL_getmetatable(L, tname); lua_setmetatable(L, -2);
583    get_metatable(state, tname)?;
584    state.set_metatable(-2)?;
585    Ok(())
586}
587
588/// Check whether the value at `ud` is a full userdata with metatable `tname`.
589/// Returns `Some(userdata)` if yes, `None` otherwise.
590///
591/// C: `LUALIB_API void *luaL_testudata(lua_State *L, int ud, const char *tname)`
592pub fn test_udata(
593    state: &mut LuaState,
594    ud: i32,
595    tname: &[u8],
596) -> Result<Option<GcRef<LuaUserData>>, LuaError> {
597    // C: void *p = lua_touserdata(L, ud);
598    let p = state.to_userdata(ud);
599    if let Some(p) = p {
600        // C: if (lua_getmetatable(L, ud))
601        if state.get_metatable(ud)? {
602            // C: luaL_getmetatable(L, tname);
603            get_metatable(state, tname)?;
604            // C: if (!lua_rawequal(L, -1, -2))  p = NULL;
605            let eq = state.raw_equal(-1, -2)?;
606            state.pop_n(2); // remove both metatables
607            if eq {
608                return Ok(Some(p));
609            }
610        }
611    }
612    Ok(None)
613}
614
615/// Like `test_udata` but raises a type error if the check fails.
616///
617/// C: `LUALIB_API void *luaL_checkudata(lua_State *L, int ud, const char *tname)`
618pub fn check_udata(
619    state: &mut LuaState,
620    ud: i32,
621    tname: &[u8],
622) -> Result<GcRef<LuaUserData>, LuaError> {
623    // C: void *p = luaL_testudata(L, ud, tname);
624    // C: luaL_argexpected(L, p != NULL, ud, tname);
625    match test_udata(state, ud, tname)? {
626        Some(p) => Ok(p),
627        None => {
628            type_error_arg(state, ud, tname)?;
629            unreachable!()
630        }
631    }
632}
633
634// ── Argument-check functions ──────────────────────────────────────────────────
635
636/// Check that `arg` is one of the strings in `lst` and return its index.
637/// If `def` is `Some` it is used as default when `arg` is absent/nil.
638///
639/// C: `LUALIB_API int luaL_checkoption(lua_State *L, int arg, const char *def, const char *const lst[])`
640pub fn check_option(
641    state: &mut LuaState,
642    arg: i32,
643    def: Option<&[u8]>,
644    lst: &[&[u8]],
645) -> Result<usize, LuaError> {
646    let name: Vec<u8> = match def {
647        Some(d) if state.is_none_or_nil(arg) => d.to_vec(),
648        _ => check_lstring(state, arg)?.as_bytes().to_vec(),
649    };
650    for (i, entry) in lst.iter().enumerate() {
651        if *entry == name.as_slice() {
652            return Ok(i);
653        }
654    }
655    Err(LuaError::runtime(format_args!(
656        "invalid option '{}'",
657        BStr(&name)
658    )))
659}
660
661/// Ensure the stack has at least `space` extra slots; raise on failure.
662///
663/// C: `LUALIB_API void luaL_checkstack(lua_State *L, int space, const char *msg)`
664pub fn check_stack(
665    state: &mut LuaState,
666    space: i32,
667    msg: Option<&[u8]>,
668) -> Result<(), LuaError> {
669    // C: if (l_unlikely(!lua_checkstack(L, space)))
670    if !state.check_stack_space(space) {
671        match msg {
672            Some(m) => {
673                return Err(LuaError::runtime(format_args!(
674                    "stack overflow ({})",
675                    BStr(m)
676                )));
677            }
678            None => {
679                return Err(LuaError::runtime(format_args!("stack overflow")));
680            }
681        }
682    }
683    Ok(())
684}
685
686/// Assert that the value at `arg` has Lua type `t`; raise type error otherwise.
687///
688/// C: `LUALIB_API void luaL_checktype(lua_State *L, int arg, int t)`
689pub fn check_type(state: &mut LuaState, arg: i32, t: LuaType) -> Result<(), LuaError> {
690    // C: if (l_unlikely(lua_type(L, arg) != t)) tag_error(L, arg, t);
691    if state.type_at(arg) != t {
692        tag_error(state, arg, t)?;
693    }
694    Ok(())
695}
696
697/// Assert that a value (not `none`) is present at `arg`.
698///
699/// C: `LUALIB_API void luaL_checkany(lua_State *L, int arg)`
700pub fn check_any(state: &mut LuaState, arg: i32) -> Result<(), LuaError> {
701    // C: if (l_unlikely(lua_type(L, arg) == LUA_TNONE))
702    if state.type_at(arg) == LuaType::None {
703        return Err(LuaError::arg_error(arg, "value expected"));
704    }
705    Ok(())
706}
707
708/// Return the string at `arg` as bytes; raise a type error if not a string.
709///
710/// C: `LUALIB_API const char *luaL_checklstring(lua_State *L, int arg, size_t *len)`
711pub fn check_lstring(state: &mut LuaState, arg: i32) -> Result<GcRef<LuaString>, LuaError> {
712    // C: const char *s = lua_tolstring(L, arg, len);
713    match state.to_lua_string(arg) {
714        Some(s) => Ok(s),
715        None => {
716            tag_error(state, arg, LuaType::String)?;
717            unreachable!()
718        }
719    }
720}
721
722/// Return the string at `arg`; if absent/nil return `def`.
723///
724/// C: `LUALIB_API const char *luaL_optlstring(lua_State *L, int arg, const char *def, size_t *len)`
725pub fn opt_lstring(
726    state: &mut LuaState,
727    arg: i32,
728    def: Option<&[u8]>,
729) -> Result<Option<Vec<u8>>, LuaError> {
730    // C: if (lua_isnoneornil(L, arg)) { ... return def; }
731    if state.is_none_or_nil(arg) {
732        return Ok(def.map(|d| d.to_vec()));
733    }
734    let s = check_lstring(state, arg)?;
735    Ok(Some(s.as_bytes().to_vec()))
736}
737
738/// Return the number at `arg` as `f64`; raise a type error if not a number.
739///
740/// C: `LUALIB_API lua_Number luaL_checknumber(lua_State *L, int arg)`
741pub fn check_number(state: &mut LuaState, arg: i32) -> Result<f64, LuaError> {
742    // C: int isnum; lua_Number d = lua_tonumberx(L, arg, &isnum);
743    match state.to_number_x(arg) {
744        Some(d) => Ok(d),
745        None => {
746            tag_error(state, arg, LuaType::Number)?;
747            unreachable!()
748        }
749    }
750}
751
752/// Return the number at `arg`; if absent/nil return `def`.
753///
754/// C: `LUALIB_API lua_Number luaL_optnumber(lua_State *L, int arg, lua_Number def)`
755pub fn opt_number(state: &mut LuaState, arg: i32, def: f64) -> Result<f64, LuaError> {
756    // C: return luaL_opt(L, luaL_checknumber, arg, def);
757    if state.is_none_or_nil(arg) {
758        Ok(def)
759    } else {
760        check_number(state, arg)
761    }
762}
763
764/// Raise an error for a non-integer number argument.
765///
766/// C: `static void interror(lua_State *L, int arg)`
767///
768/// Always returns `Err`. The `Ok` arm uses `unreachable!()` to satisfy the
769/// return type; `!` (never) is nightly-only so we use `Result<usize, LuaError>`.
770fn int_error(state: &mut LuaState, arg: i32) -> Result<usize, LuaError> {
771    if state.is_number(arg) {
772        Err(LuaError::arg_error(
773            arg,
774            "number has no integer representation",
775        ))
776    } else {
777        tag_error(state, arg, LuaType::Number)?;
778        unreachable!("tag_error always returns Err")
779    }
780}
781
782/// Return the integer at `arg` as `i64`; raise if not an integer-convertible number.
783///
784/// C: `LUALIB_API lua_Integer luaL_checkinteger(lua_State *L, int arg)`
785pub fn check_integer(state: &mut LuaState, arg: i32) -> Result<i64, LuaError> {
786    // C: int isnum; lua_Integer d = lua_tointegerx(L, arg, &isnum);
787    match state.to_integer_x(arg) {
788        Some(d) => Ok(d),
789        None => {
790            int_error(state, arg)?;
791            unreachable!("int_error always returns Err")
792        }
793    }
794}
795
796/// Return the integer at `arg`; if absent/nil return `def`.
797///
798/// C: `LUALIB_API lua_Integer luaL_optinteger(lua_State *L, int arg, lua_Integer def)`
799pub fn opt_integer(state: &mut LuaState, arg: i32, def: i64) -> Result<i64, LuaError> {
800    // C: return luaL_opt(L, luaL_checkinteger, arg, def);
801    if state.is_none_or_nil(arg) {
802        Ok(def)
803    } else {
804        check_integer(state, arg)
805    }
806}
807
808// ── Buffer manipulation ────────────────────────────────────────────────────────
809
810impl LuaBuffer {
811    /// Create a new empty buffer.
812    ///
813    /// C: the initial `luaL_Buffer` has a small inline array of `LUAL_BUFFERSIZE` bytes.
814    /// Rust uses `Vec::new()` which starts at zero capacity; capacity is managed by Vec.
815    pub fn new() -> Self {
816        LuaBuffer { data: Vec::new() }
817    }
818
819    /// Returns the number of bytes currently in the buffer.
820    pub fn len(&self) -> usize {
821        self.data.len()
822    }
823}
824
825impl Default for LuaBuffer {
826    fn default() -> Self {
827        LuaBuffer::new()
828    }
829}
830
831/// Initialize `buf` and associate it with `state`.
832/// Pushes a placeholder light-userdata onto `state` to anchor the buffer in C.
833/// In Rust the Vec is self-contained; we still push a placeholder for stack-slot
834/// compatibility with code that later calls `add_value` / `push_result`.
835///
836/// C: `LUALIB_API void luaL_buffinit(lua_State *L, luaL_Buffer *B)`
837pub fn buf_init(state: &mut LuaState, buf: &mut LuaBuffer) {
838    // PORT NOTE: C pushes a light-userdata placeholder onto the stack to hold
839    // the buffer's position. We still push nil as a stack slot placeholder so
840    // that add_value / push_result see the same stack layout.
841    *buf = LuaBuffer::new();
842    // C: lua_pushlightuserdata(L, (void*)B);  /* push placeholder */
843    // We push nil; Phase B can revisit if this matters for GC interaction.
844    let _ = state.push(LuaValue::Nil);
845}
846
847/// Initialize `buf`, reserve `sz` bytes, and return the writable region.
848///
849/// C: `LUALIB_API char *luaL_buffinitsize(lua_State *L, luaL_Buffer *B, size_t sz)`
850pub fn buf_init_size(
851    state: &mut LuaState,
852    buf: &mut LuaBuffer,
853    sz: usize,
854) -> Result<(), LuaError> {
855    buf_init(state, buf);
856    buf.data.reserve(sz);
857    Ok(())
858}
859
860/// Compute a new buffer capacity that accommodates `sz` more bytes,
861/// growing by ×1.5 or more.
862///
863/// C: `static size_t newbuffsize(luaL_Buffer *B, size_t sz)`
864fn new_buff_size(buf: &LuaBuffer, sz: usize) -> Result<usize, LuaError> {
865    // C: if (l_unlikely(MAX_SIZET - sz < B->n)) return luaL_error(...)
866    if usize::MAX - sz < buf.len() {
867        return Err(LuaError::runtime(format_args!("buffer too large")));
868    }
869    let newsize = (buf.data.capacity() / 2) * 3; // ×1.5
870    if newsize < buf.len() + sz {
871        Ok(buf.len() + sz)
872    } else {
873        Ok(newsize)
874    }
875}
876
877/// Ensure at least `sz` free bytes are available in `buf`.
878///
879/// C: `static char *prepbuffsize(luaL_Buffer *B, size_t sz, int boxidx)`
880/// C: `LUALIB_API char *luaL_prepbuffsize(luaL_Buffer *B, size_t sz)`
881pub fn prep_buff_size(buf: &mut LuaBuffer, sz: usize) -> Result<(), LuaError> {
882    if buf.data.capacity() - buf.data.len() < sz {
883        let newcap = new_buff_size(buf, sz)?;
884        buf.data.reserve(newcap - buf.data.len());
885    }
886    Ok(())
887}
888
889/// Append `s` to `buf`.
890///
891/// C: `LUALIB_API void luaL_addlstring(luaL_Buffer *B, const char *s, size_t l)`
892pub fn add_lstring(buf: &mut LuaBuffer, s: &[u8]) {
893    if !s.is_empty() {
894        buf.data.extend_from_slice(s);
895    }
896}
897
898/// Append a single byte to `buf`.
899///
900/// C: `#define luaL_addchar(B,c) ...`
901pub fn add_char(buf: &mut LuaBuffer, c: u8) {
902    buf.data.push(c);
903}
904
905/// Append `sz` to the length counter (used after writing directly into the buffer).
906///
907/// C: `#define luaL_addsize(B,s) ((B)->n += (s))`
908pub fn add_size(buf: &mut LuaBuffer, sz: usize) {
909    // PORT NOTE: In C this is a direct `n += sz` on the inline length field.
910    // With Vec, length is implicit; this is a no-op unless caller wrote past len.
911    // TODO(port): if direct-write into spare capacity is needed, switch to `unsafe`
912    // set_len or redesign; for Phase A this is a no-op.
913    let _ = sz;
914}
915
916/// Pop the string at top of `state`'s stack and append it to `buf`.
917///
918/// C: `LUALIB_API void luaL_addvalue(luaL_Buffer *B)`
919pub fn add_value(state: &mut LuaState, buf: &mut LuaBuffer) -> Result<(), LuaError> {
920    // C: const char *s = lua_tolstring(L, -1, &len);
921    if let Some(bytes) = state.peek_bytes(-1) {
922        let owned = bytes.to_vec();
923        add_lstring(buf, &owned);
924    }
925    // C: lua_pop(L, 1);
926    state.pop_n(1);
927    Ok(())
928}
929
930/// Push the buffer contents as a Lua string onto `state`'s stack.
931///
932/// C: `LUALIB_API void luaL_pushresult(luaL_Buffer *B)`
933pub fn push_result(state: &mut LuaState, buf: &mut LuaBuffer) -> Result<(), LuaError> {
934    // C: lua_pushlstring(L, B->b, B->n);
935    state.push_bytes(&buf.data)?;
936    // C: if (buffonstack(B)) lua_closeslot(L, -2);
937    // C: lua_remove(L, -2);  /* remove box or placeholder */
938    state.remove(-2)?;
939    Ok(())
940}
941
942/// Add `sz` bytes to the buffer count then call `push_result`.
943///
944/// C: `LUALIB_API void luaL_pushresultsize(luaL_Buffer *B, size_t sz)`
945pub fn push_result_size(
946    state: &mut LuaState,
947    buf: &mut LuaBuffer,
948    sz: usize,
949) -> Result<(), LuaError> {
950    add_size(buf, sz);
951    push_result(state, buf)
952}
953
954/// Perform global byte-string substitution: replace all occurrences of `pat`
955/// with `repl` in `s`, appending results into `buf`.
956///
957/// C: `LUALIB_API void luaL_addgsub(luaL_Buffer *b, const char *s, const char *p, const char *r)`
958pub fn add_gsub(buf: &mut LuaBuffer, s: &[u8], pat: &[u8], repl: &[u8]) {
959    if pat.is_empty() {
960        add_lstring(buf, s);
961        return;
962    }
963    let mut remaining = s;
964    while let Some(pos) = find_bytes(remaining, pat) {
965        // C: luaL_addlstring(b, s, wild - s);
966        add_lstring(buf, &remaining[..pos]);
967        // C: luaL_addstring(b, r);
968        add_lstring(buf, repl);
969        remaining = &remaining[pos + pat.len()..];
970    }
971    // C: luaL_addstring(b, s);  /* push last suffix */
972    add_lstring(buf, remaining);
973}
974
975/// Build a string from `s` by replacing `pat` with `repl`, push it on the stack,
976/// and return the bytes of the pushed string.
977///
978/// C: `LUALIB_API const char *luaL_gsub(lua_State *L, const char *s, const char *p, const char *r)`
979pub fn gsub<'a>(
980    state: &'a mut LuaState,
981    s: &[u8],
982    pat: &[u8],
983    repl: &[u8],
984) -> Result<Vec<u8>, LuaError> {
985    let mut b = LuaBuffer::new();
986    buf_init(state, &mut b);
987    add_gsub(&mut b, s, pat, repl);
988    push_result(state, &mut b)?;
989    // C: return lua_tostring(L, -1);
990    Ok(state.peek_bytes(-1).unwrap_or_default())
991}
992
993/// Find `needle` in `haystack`, returning the byte offset or `None`.
994///
995/// Internal helper replacing C's `strstr`.
996fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
997    if needle.is_empty() {
998        return Some(0);
999    }
1000    haystack.windows(needle.len()).position(|w| w == needle)
1001}
1002
1003// ── Reference system ──────────────────────────────────────────────────────────
1004
1005/// Store the value at the top of the stack in table `t` and return a unique
1006/// integer reference. If the value is `nil`, returns `LUA_REFNIL` without
1007/// modifying the table.
1008///
1009/// C: `LUALIB_API int luaL_ref(lua_State *L, int t)`
1010pub fn lua_ref(state: &mut LuaState, t: i32) -> Result<i32, LuaError> {
1011    // C: if (lua_isnil(L, -1)) { lua_pop(L, 1); return LUA_REFNIL; }
1012    if state.type_at(-1) == LuaType::Nil {
1013        state.pop_n(1);
1014        return Ok(LUA_REFNIL);
1015    }
1016    let t = state.abs_index(t);
1017    // C: if (lua_rawgeti(L, t, freelist) == LUA_TNIL)
1018    let ref_val: i32;
1019    if state.raw_get_i(t, FREELIST_REF)? == LuaType::Nil {
1020        ref_val = 0; // list is empty
1021        // C: lua_pushinteger(L, 0); lua_rawseti(L, t, freelist);
1022        state.push(LuaValue::Int(0));
1023        state.raw_set_i(t, FREELIST_REF)?;
1024    } else {
1025        // C: lua_assert(lua_isinteger(L, -1)); ref = (int)lua_tointeger(L, -1);
1026        debug_assert!(state.type_at(-1) == LuaType::Number);
1027        ref_val = state.to_integer_x(-1).unwrap_or(0) as i32;
1028    }
1029    state.pop_n(1); // remove element from stack
1030    let next_ref: i32;
1031    if ref_val != 0 {
1032        // C: lua_rawgeti(L, t, ref); lua_rawseti(L, t, freelist);
1033        state.raw_get_i(t, ref_val as i64)?;
1034        state.raw_set_i(t, FREELIST_REF)?;
1035        next_ref = ref_val;
1036    } else {
1037        // C: ref = (int)lua_rawlen(L, t) + 1;
1038        next_ref = (state.raw_len(t) as i32) + 1;
1039    }
1040    // C: lua_rawseti(L, t, ref);
1041    state.raw_set_i(t, next_ref as i64)?;
1042    Ok(next_ref)
1043}
1044
1045/// Release reference `ref` from table `t`, adding it to the free list.
1046///
1047/// C: `LUALIB_API void luaL_unref(lua_State *L, int t, int ref)`
1048pub fn lua_unref(state: &mut LuaState, t: i32, r: i32) -> Result<(), LuaError> {
1049    if r >= 0 {
1050        let t = state.abs_index(t);
1051        // C: lua_rawgeti(L, t, freelist);
1052        state.raw_get_i(t, FREELIST_REF)?;
1053        debug_assert!(state.type_at(-1) == LuaType::Number);
1054        // C: lua_rawseti(L, t, ref);  /* t[ref] = t[freelist] */
1055        state.raw_set_i(t, r as i64)?;
1056        // C: lua_pushinteger(L, ref); lua_rawseti(L, t, freelist);
1057        state.push(LuaValue::Int(r as i64));
1058        state.raw_set_i(t, FREELIST_REF)?;
1059    }
1060    Ok(())
1061}
1062
1063// ── Load functions ─────────────────────────────────────────────────────────────
1064
1065/// Internal chunk reader that returns a single buffer slice then signals EOF.
1066///
1067/// C: `static const char *getS(lua_State *L, void *ud, size_t *size)`
1068fn make_string_reader(data: Vec<u8>) -> impl FnMut() -> Option<Vec<u8>> {
1069    let mut remaining = Some(data);
1070    move || remaining.take()
1071}
1072
1073/// Strip an optional UTF-8 BOM (EF BB BF) and any `#`-prefixed first line.
1074///
1075/// PORT NOTE: C reads byte-by-byte with `getc`/`feof` and lazily reopens the
1076/// file in binary mode if it looks like a binary chunk. Here we slurp the file
1077/// into memory (`std::fs::read`), strip the BOM, and let `lua_vm::api::load`
1078/// dispatch text vs. binary by the first byte. The "binary chunk" branch in
1079/// `luaL_loadfilex` exists in C because text mode does newline translation;
1080/// `std::fs::read` already returns raw bytes on every platform we support.
1081fn skip_bom_and_shebang(buf: &[u8]) -> Vec<u8> {
1082    let s = if buf.starts_with(b"\xEF\xBB\xBF") { &buf[3..] } else { buf };
1083    if s.first() == Some(&b'#') {
1084        let nl = s.iter().position(|&b| b == b'\n').map(|p| p + 1).unwrap_or(s.len());
1085        let rest = &s[nl..];
1086        if rest.first() == Some(&0x1B) {
1087            rest.to_vec()
1088        } else {
1089            let mut out = Vec::with_capacity(rest.len() + 1);
1090            out.push(b'\n');
1091            out.extend_from_slice(rest);
1092            out
1093        }
1094    } else {
1095        s.to_vec()
1096    }
1097}
1098
1099/// Load a file as a Lua chunk. Returns `LUA_OK` on success or an error code.
1100///
1101/// C: `LUALIB_API int luaL_loadfilex(lua_State *L, const char *filename, const char *mode)`
1102///
1103/// PORT NOTE: PORTING.md §1 bans `std::fs` outside `lua-cli`, but C-Lua's
1104/// `luaL_loadfilex` is part of the auxiliary library (`lauxlib.c`) and is
1105/// reachable from the base library (`loadfile`/`dofile`). Phase A's stub
1106/// raised an error here, which broke `loadfile(missing)` returning `nil, err`.
1107/// The real C semantics push an error string onto the stack and return a
1108/// non-zero status, which `load_aux` then converts to `(nil, errmsg)`.
1109pub fn load_filex(
1110    state: &mut LuaState,
1111    filename: Option<&[u8]>,
1112    mode: Option<&[u8]>,
1113) -> Result<i32, LuaError> {
1114    let _ = mode;
1115    let fname = match filename {
1116        Some(f) => f,
1117        None => {
1118            // TODO(port): stdin loading not yet supported in lua-stdlib; return
1119            // an error string matching C's "cannot read stdin" shape.
1120            state.push_string(b"cannot read stdin: no filename given")?;
1121            return Ok(LUA_ERRFILE);
1122        }
1123    };
1124    let path = match std::str::from_utf8(fname) {
1125        Ok(s) => std::path::PathBuf::from(s),
1126        Err(_) => {
1127            state.push_fstring(format_args!(
1128                "cannot open {}: invalid utf-8 in filename",
1129                BStr(fname)
1130            ))?;
1131            return Ok(LUA_ERRFILE);
1132        }
1133    };
1134    let raw = match std::fs::read(&path) {
1135        Ok(bytes) => bytes,
1136        Err(e) => {
1137            state.push_fstring(format_args!(
1138                "cannot open {}: {}",
1139                BStr(fname),
1140                e
1141            ))?;
1142            return Ok(LUA_ERRFILE);
1143        }
1144    };
1145    let payload = skip_bom_and_shebang(&raw);
1146    let mut once = Some(payload);
1147    let boxed: Box<dyn FnMut() -> Option<Vec<u8>>> =
1148        Box::new(move || once.take());
1149    let mut chunkname = b"@".to_vec();
1150    chunkname.extend_from_slice(fname);
1151    let status = lua_vm::api::load(state, boxed, Some(&chunkname), mode)?;
1152    Ok(if status == LuaStatus::Ok { 0 } else { status as i32 })
1153}
1154
1155/// Load a buffer as a Lua chunk.
1156///
1157/// C: `LUALIB_API int luaL_loadbufferx(lua_State *L, const char *buff, size_t size, const char *name, const char *mode)`
1158pub fn load_bufferx(
1159    state: &mut LuaState,
1160    buff: &[u8],
1161    name: &[u8],
1162    mode: Option<&[u8]>,
1163) -> Result<i32, LuaError> {
1164    // C: LoadS ls; ls.s = buff; ls.size = size;
1165    // C: return lua_load(L, getS, &ls, name, mode);
1166    // TODO(phase-b): state.load expects (chunk: &[u8], name, mode) in state_stub; the reader-based loader needs a load_with_reader API match.
1167    let _reader = make_string_reader(buff.to_vec());
1168    let ok = state.load(buff, name, mode)?;
1169    Ok(if ok { 0 } else { 1 })
1170}
1171
1172/// Load a buffer as a Lua chunk (no mode argument).
1173///
1174/// C: `#define luaL_loadbuffer(L,s,sz,n) luaL_loadbufferx(L,s,sz,n,NULL)`
1175pub fn load_buffer(
1176    state: &mut LuaState,
1177    buff: &[u8],
1178    name: &[u8],
1179) -> Result<i32, LuaError> {
1180    load_bufferx(state, buff, name, None)
1181}
1182
1183/// Load a NUL-terminated byte-string as a Lua chunk.
1184///
1185/// C: `LUALIB_API int luaL_loadstring(lua_State *L, const char *s)`
1186pub fn load_string(state: &mut LuaState, s: &[u8]) -> Result<i32, LuaError> {
1187    // C: return luaL_loadbuffer(L, s, strlen(s), s);
1188    load_buffer(state, s, s)
1189}
1190
1191// ── Meta-field and misc helpers ───────────────────────────────────────────────
1192
1193/// Push the metafield `event` of `obj` onto the stack and return its type.
1194/// If there is no metafield, nothing is pushed and `LuaType::Nil` is returned.
1195///
1196/// C: `LUALIB_API int luaL_getmetafield(lua_State *L, int obj, const char *event)`
1197pub fn get_metafield(
1198    state: &mut LuaState,
1199    obj: i32,
1200    event: &[u8],
1201) -> Result<LuaType, LuaError> {
1202    // C: if (!lua_getmetatable(L, obj)) return LUA_TNIL;
1203    if !state.get_metatable(obj)? {
1204        return Ok(LuaType::Nil);
1205    }
1206    // C: lua_pushstring(L, event); tt = lua_rawget(L, -2);
1207    state.push_bytes(event)?;
1208    let tt = state.raw_get(-2)?;
1209    if tt == LuaType::Nil {
1210        // C: lua_pop(L, 2); /* remove metatable and metafield */
1211        state.pop_n(2);
1212    } else {
1213        // C: lua_remove(L, -2); /* remove only metatable */
1214        state.remove(-2)?;
1215    }
1216    Ok(tt)
1217}
1218
1219/// Call the metafield `event` of `obj` with `obj` as argument, pushing one result.
1220/// Returns `true` if the meta-method existed and was called.
1221///
1222/// C: `LUALIB_API int luaL_callmeta(lua_State *L, int obj, const char *event)`
1223pub fn call_meta(state: &mut LuaState, obj: i32, event: &[u8]) -> Result<bool, LuaError> {
1224    let obj = state.abs_index(obj);
1225    // C: if (luaL_getmetafield(L, obj, event) == LUA_TNIL) return 0;
1226    if get_metafield(state, obj, event)? == LuaType::Nil {
1227        return Ok(false);
1228    }
1229    // C: lua_pushvalue(L, obj); lua_call(L, 1, 1);
1230    state.push_value(obj)?;
1231    state.call(1, 1)?;
1232    Ok(true)
1233}
1234
1235/// Return the length of the value at `idx` as a `i64`, raising an error if
1236/// the length is not an integer.
1237///
1238/// C: `LUALIB_API lua_Integer luaL_len(lua_State *L, int idx)`
1239pub fn lua_len(state: &mut LuaState, idx: i32) -> Result<i64, LuaError> {
1240    // C: lua_len(L, idx);
1241    state.len_op(idx)?;
1242    // C: l = lua_tointegerx(L, -1, &isnum);
1243    let l = match state.to_integer_x(-1) {
1244        Some(n) => n,
1245        None => {
1246            return Err(LuaError::runtime(format_args!(
1247                "object length is not an integer"
1248            )));
1249        }
1250    };
1251    // C: lua_pop(L, 1);
1252    state.pop_n(1);
1253    Ok(l)
1254}
1255
1256/// Convert the value at `idx` to a byte-string representation (using `__tostring`
1257/// if available) and push it onto the stack.
1258///
1259/// C: `LUALIB_API const char *luaL_tolstring(lua_State *L, int idx, size_t *len)`
1260pub fn to_lua_string(state: &mut LuaState, idx: i32) -> Result<Vec<u8>, LuaError> {
1261    let idx = state.abs_index(idx);
1262    // C: if (luaL_callmeta(L, idx, "__tostring"))
1263    if call_meta(state, idx, b"__tostring")? {
1264        // C: if (!lua_isstring(L, -1)) luaL_error(...)
1265        if state.type_at(-1) != LuaType::String {
1266            return Err(LuaError::runtime(format_args!(
1267                "'__tostring' must return a string"
1268            )));
1269        }
1270    } else {
1271        match state.type_at(idx) {
1272            LuaType::Number => {
1273                // C: if (lua_isinteger(L, idx)) lua_pushfstring(L, "%I", ...)
1274                if state.is_integer(idx) {
1275                    let i = state.to_integer_x(idx).unwrap_or(0);
1276                    state.push_fstring(format_args!("{}", i))?;
1277                } else {
1278                    let f = state.to_number_x(idx).unwrap_or(0.0);
1279                    state.push_fstring(format_args!("{:?}", f))?;
1280                }
1281            }
1282            LuaType::String => {
1283                // C: lua_pushvalue(L, idx);
1284                state.push_value(idx)?;
1285            }
1286            LuaType::Boolean => {
1287                let b = state.to_boolean(idx);
1288                state.push_string(if b { b"true" } else { b"false" })?;
1289            }
1290            LuaType::Nil => {
1291                state.push_string(b"nil")?;
1292            }
1293            _ => {
1294                // C: int tt = luaL_getmetafield(L, idx, "__name");
1295                let tt = get_metafield(state, idx, b"__name")?;
1296                let kind: Vec<u8> = if tt == LuaType::String {
1297                    state.peek_bytes(-1).unwrap_or_else(|| b"?".to_vec())
1298                } else {
1299                    state.type_name_at(idx).to_vec()
1300                };
1301                // C: lua_pushfstring(L, "%s: %p", kind, lua_topointer(L, idx));
1302                // TODO(port): lua_topointer gives a pointer address; in Rust use
1303                // a hash or allocation address for a stable identifier.
1304                state.push_fstring(format_args!("{}: 0x?", BStr(&kind)))?;
1305                if tt != LuaType::Nil {
1306                    // C: lua_remove(L, -2);
1307                    state.remove(-2)?;
1308                }
1309            }
1310        }
1311    }
1312    // C: return lua_tolstring(L, -1, len);
1313    Ok(state.peek_bytes(-1).unwrap_or_default())
1314}
1315
1316/// Register the functions in `l` into the table at `-(nup + 1)`, giving each
1317/// closure the `nup` upvalues currently at the top of the stack.
1318///
1319/// C: `LUALIB_API void luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup)`
1320pub fn set_funcs(
1321    state: &mut LuaState,
1322    l: &[LuaReg],
1323    nup: i32,
1324) -> Result<(), LuaError> {
1325    check_stack(state, nup, Some(b"too many upvalues"))?;
1326    for reg in l {
1327        match reg.func {
1328            None => {
1329                // C: lua_pushboolean(L, 0);
1330                state.push(LuaValue::Bool(false));
1331            }
1332            Some(f) => {
1333                // C: for (i = 0; i < nup; i++) lua_pushvalue(L, -nup);
1334                for _ in 0..nup {
1335                    state.push_value(-nup)?;
1336                }
1337                // C: lua_pushcclosure(L, l->func, nup);
1338                state.push_c_closure(f, nup)?;
1339            }
1340        }
1341        // C: lua_setfield(L, -(nup + 2), l->name);
1342        state.set_field(-(nup + 2), reg.name)?;
1343    }
1344    // C: lua_pop(L, nup);
1345    state.pop_n(nup as usize);
1346    Ok(())
1347}
1348
1349/// Ensure `state[idx][fname]` is a table; push it.
1350/// Returns `true` if the table already existed, `false` if newly created.
1351///
1352/// C: `LUALIB_API int luaL_getsubtable(lua_State *L, int idx, const char *fname)`
1353pub fn get_subtable(
1354    state: &mut LuaState,
1355    idx: i32,
1356    fname: &[u8],
1357) -> Result<bool, LuaError> {
1358    if state.get_field(idx, fname)? == LuaType::Table {
1359        return Ok(true);
1360    }
1361    state.pop_n(1);
1362    let idx = state.abs_index(idx);
1363    let new_tbl = state.new_table();
1364    state.push(LuaValue::Table(new_tbl));
1365    state.push_value(-1)?;
1366    state.set_field(idx, fname)?;
1367    Ok(false)
1368}
1369
1370/// Simplified `require`: open module `modname` via `openf`, register it in
1371/// `package.loaded`, and (if `glb`) in the global table.
1372/// Leaves the module on top of the stack.
1373///
1374/// C: `LUALIB_API void luaL_requiref(lua_State *L, const char *modname, lua_CFunction openf, int glb)`
1375pub fn requiref(
1376    state: &mut LuaState,
1377    modname: &[u8],
1378    openf: fn(&mut LuaState) -> Result<usize, LuaError>,
1379    glb: bool,
1380) -> Result<(), LuaError> {
1381    get_subtable(state, LUA_REGISTRYINDEX, LUA_LOADED_TABLE)?;
1382    state.get_field(-1, modname)?;
1383    if !state.to_boolean(-1) {
1384        state.pop_n(1);
1385        state.push_c_function(openf)?;
1386        state.push_bytes(modname)?;
1387        state.call(1, 1)?;
1388        state.push_value(-1)?;
1389        state.set_field(-3, modname)?;
1390    }
1391    state.remove(-2)?;
1392    if glb {
1393        // C: lua_pushvalue(L, -1); lua_setglobal(L, modname);
1394        state.push_value(-1)?;
1395        state.set_global(modname)?;
1396    }
1397    Ok(())
1398}
1399
1400// ── Helper for registry-based metatable lookup ─────────────────────────────────
1401
1402/// Push `registry[tname]` and return its type.
1403///
1404/// C: `#define luaL_getmetatable(L,n) (lua_getfield(L, LUA_REGISTRYINDEX, (n)))`
1405pub fn get_metatable(state: &mut LuaState, tname: &[u8]) -> Result<LuaType, LuaError> {
1406    state.get_field(LUA_REGISTRYINDEX, tname)
1407}
1408
1409// ── State creation and version check ─────────────────────────────────────────
1410
1411/// Create a new `LuaState` with the default allocator, a panic handler, and
1412/// warnings disabled.
1413///
1414/// C: `LUALIB_API lua_State *luaL_newstate(void)`
1415pub fn new_state() -> Result<LuaState, LuaError> {
1416    // C: lua_State *L = lua_newstate(l_alloc, NULL);
1417    // PORT NOTE: Rust's allocator is used implicitly; no l_alloc hook needed.
1418    // TODO(phase-b): LuaState::new() / set_panic_handler / set_warn_fn need a real LuaState constructor in lua-vm. Stub for Phase A.
1419    let _ = default_panic_handler;
1420    let _ = warn_off;
1421    todo!("phase-b: LuaState::new()")
1422}
1423
1424/// Default panic handler: print message to stderr and return to abort.
1425///
1426/// C: `static int panic(lua_State *L)`
1427fn default_panic_handler(state: &mut LuaState) -> Result<usize, LuaError> {
1428    // C: const char *msg = (lua_type(L, -1) == LUA_TSTRING) ? lua_tostring(L, -1) : "...";
1429    let msg = if state.type_at(-1) == LuaType::String {
1430        state.peek_bytes(-1).unwrap_or_else(|| b"?".to_vec())
1431    } else {
1432        b"error object is not a string".to_vec()
1433    };
1434    // C: lua_writestringerror(...)
1435    eprintln!("PANIC: unprotected error in call to Lua API ({})", BStr(&msg));
1436    Ok(0) // return to Lua to abort
1437}
1438
1439/// Warning function: warnings are off.
1440///
1441/// C: `static void warnfoff(void *ud, const char *message, int tocont)`
1442fn warn_off(state: &mut LuaState, message: &[u8], tocont: bool) -> Result<(), LuaError> {
1443    check_control(state, message, tocont)?;
1444    Ok(())
1445}
1446
1447/// Warning function: ready to start a new message.
1448///
1449/// C: `static void warnfon(void *ud, const char *message, int tocont)`
1450fn warn_on(state: &mut LuaState, message: &[u8], tocont: bool) -> Result<(), LuaError> {
1451    if check_control(state, message, tocont)? {
1452        return Ok(());
1453    }
1454    eprint!("Lua warning: ");
1455    warn_cont(state, message, tocont)
1456}
1457
1458/// Warning function: continue writing a previous warning message.
1459///
1460/// C: `static void warnfcont(void *ud, const char *message, int tocont)`
1461fn warn_cont(state: &mut LuaState, message: &[u8], tocont: bool) -> Result<(), LuaError> {
1462    // C: lua_writestringerror("%s", message);
1463    eprint!("{}", BStr(message));
1464    // TODO(phase-b): set_warn_fn expects lua_CFunction in state_stub; warn_cont/warn_on take (msg, tocont). Wire after warn-fn API lands in lua-vm.
1465    if tocont {
1466        let _ = (warn_cont as fn(&mut LuaState, &[u8], bool) -> Result<(), LuaError>,);
1467    } else {
1468        eprintln!();
1469        let _ = (warn_on as fn(&mut LuaState, &[u8], bool) -> Result<(), LuaError>,);
1470    }
1471    Ok(())
1472}
1473
1474/// Handle a warning control message (e.g. `"@on"`, `"@off"`).
1475/// Returns `true` if the message was a recognised control message.
1476///
1477/// C: `static int checkcontrol(lua_State *L, const char *message, int tocont)`
1478fn check_control(
1479    state: &mut LuaState,
1480    message: &[u8],
1481    tocont: bool,
1482) -> Result<bool, LuaError> {
1483    // C: if (tocont || *(message++) != '@') return 0;
1484    if tocont || message.first() != Some(&b'@') {
1485        return Ok(false);
1486    }
1487    let cmd = &message[1..];
1488    // TODO(phase-b): set_warn_fn expects lua_CFunction in state_stub; warn_off/warn_on take (msg, tocont). Wire after warn-fn API lands in lua-vm.
1489    let _ = state;
1490    if cmd == b"off" {
1491        let _ = warn_off as fn(&mut LuaState, &[u8], bool) -> Result<(), LuaError>;
1492    } else if cmd == b"on" {
1493        let _ = warn_on as fn(&mut LuaState, &[u8], bool) -> Result<(), LuaError>;
1494    }
1495    Ok(true)
1496}
1497
1498/// Version-compatibility check: error if numeric type sizes or version mismatch.
1499///
1500/// C: `LUALIB_API void luaL_checkversion_(lua_State *L, lua_Number ver, size_t sz)`
1501pub fn check_version(state: &mut LuaState, ver: f64, sz: usize) -> Result<(), LuaError> {
1502    // C: LUAL_NUMSIZES = sizeof(lua_Integer)*16 + sizeof(lua_Number)
1503    const LUAL_NUMSIZES: usize = std::mem::size_of::<i64>() * 16 + std::mem::size_of::<f64>();
1504    if sz != LUAL_NUMSIZES {
1505        return Err(LuaError::runtime(format_args!(
1506            "core and library have incompatible numeric types"
1507        )));
1508    }
1509    let v = state.lua_version();
1510    if (v - ver).abs() > f64::EPSILON {
1511        return Err(LuaError::runtime(format_args!(
1512            "version mismatch: app. needs {}, Lua core provides {}",
1513            ver, v
1514        )));
1515    }
1516    Ok(())
1517}
1518
1519// ── Internal display helper ────────────────────────────────────────────────────
1520
1521/// Wrapper that implements `Display` for `&[u8]` as a lossy byte string.
1522/// Used to embed byte slices in `format_args!` without allocating a `String`.
1523///
1524/// PORT NOTE: not used for Lua string data; used only for error message
1525/// formatting inside `format_args!` literals.
1526struct BStr<'a>(&'a [u8]);
1527
1528impl<'a> std::fmt::Display for BStr<'a> {
1529    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1530        for &b in self.0 {
1531            if b.is_ascii() {
1532                f.write_char(b as char)?;
1533            } else {
1534                write!(f, "\\x{:02x}", b)?;
1535            }
1536        }
1537        Ok(())
1538    }
1539}
1540
1541// Required for fmt::Display
1542use std::fmt::Write as _;
1543
1544// ── LuaDebug Default ─────────────────────────────────────────────────────────
1545
1546
1547// ──────────────────────────────────────────────────────────────────────────
1548// PORT STATUS
1549//   source:        src/lauxlib.c  (1127 lines, ~50 functions)
1550//   target_crate:  lua-stdlib
1551//   confidence:    medium
1552//   todos:         10
1553//   port_notes:    8
1554//   unsafe_blocks: 0
1555//   notes:         Buffer simplified from stack-based C UBox/box-on-Lua-stack to
1556//                  plain Vec<u8> (LuaBuffer); UBox/resizebox/boxgc/boxmt/newbox
1557//                  machinery dropped entirely — Rust Drop handles deallocation.
1558//                  load_filex now reads via std::fs::read and pushes an error
1559//                  string on open failure so loadfile/dofile return (nil, err)
1560//                  per C semantics (stdin loading still TODO).
1561//                  Warning system uses fn-ptr callbacks matching lua_WarnFunction
1562//                  type; warnfoff/warnfon/warnfcont translated faithfully.
1563//                  LuaState / LuaDebug / GcRef are Phase-A stubs; Phase B replaces
1564//                  with real imports from lua-vm / lua-types.
1565//                  add_size() is a no-op in Phase A (Vec tracks length implicitly);
1566//                  direct buffer writes via spare capacity need revisit in Phase B.
1567//                  int_error() return type changed from `!` to `Result<usize,_>` as
1568//                  the never type is nightly-only on stable Rust.
1569// ──────────────────────────────────────────────────────────────────────────