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