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