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