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
33};
34use crate::state_stub::{LuaState, LuaStateStubExt as _, 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.
73#[expect(dead_code, reason = "ported stdlib helper; not yet wired into the runtime")]
74const LUA_MINSTACK: i32 = 20;
75
76// ── Public types ──────────────────────────────────────────────────────────────
77
78/// A function-registration entry for `set_funcs`.
79///
80///
81/// In Rust, `name` is `&'static [u8]` (never `&str`). A `None` func is a
82/// placeholder that pushes `false` rather than a closure.
83pub struct LuaReg {
84    pub name: &'static [u8],
85    pub func: Option<fn(&mut LuaState) -> Result<usize, LuaError>>,
86}
87
88/// Growable byte-buffer used by the auxiliary library for building strings.
89///
90///
91/// The C version uses a small inline initial buffer with overflow managed via
92/// a Lua-stack userdata box. The Rust port collapses this to a plain `Vec<u8>`.
93/// All buffer mutating functions take `&mut LuaState` as a separate parameter.
94pub struct LuaBuffer {
95    pub data: Vec<u8>,
96}
97
98/// File-stream handle used by the IO library.
99///
100///
101/// `closef` in C is a `lua_CFunction`. In Rust we store an optional closer.
102// TODO(port): file I/O belongs in lua-stdlib/src/io_lib.rs; this definition
103// may move there. Keeping here to mirror the C header.
104pub struct LuaStream {
105    /// The underlying file handle. `None` for incompletely opened or closed streams.
106    // TODO(port): use a real File type (e.g. `std::fs::File`) in Phase B,
107    // noting std::fs is allowed in lua-stdlib for I/O library support.
108    pub f: Option<Box<dyn std::io::Read>>,
109    /// Optional close function (None for already-closed streams).
110    pub closef: Option<fn(&mut LuaState) -> Result<usize, LuaError>>,
111}
112
113// ── Traceback ─────────────────────────────────────────────────────────────────
114
115/// Search for `objidx` in the table at the top of the stack.
116/// `objidx` must be an absolute API stack index.
117/// Returns `true` (and leaves name string on top) when found.
118///
119fn find_field(
120    state: &mut LuaState,
121    objidx: i32,
122    level: i32,
123) -> Result<bool, LuaError> {
124    if level == 0 || state.type_at(-1) != LuaType::Table {
125        return Ok(false);
126    }
127    state.push(LuaValue::Nil);
128    while state.table_next(-2)? {
129        if state.type_at(-2) == LuaType::String {
130            if state.raw_equal(objidx, -1)? {
131                state.pop_n(1); // remove value (keep name)
132                return Ok(true);
133            } else if find_field(state, objidx, level - 1)? {
134                // stack: lib_name, lib_table, field_name (top)
135                state.push_string(b".")?; // place '.' between the two names
136                state.replace(-3)?; // in the slot occupied by table
137                state.concat(3)?; // lib_name.field_name
138                return Ok(true);
139            }
140        }
141        state.pop_n(1); // remove value
142    }
143    Ok(false)
144}
145
146/// Search all loaded modules for a global name for the function at `top+1`.
147/// Returns `true` and leaves name string on top (at `top+1`) if found.
148///
149fn push_global_func_name(
150    state: &mut LuaState,
151    ar: &mut LuaDebug,
152) -> Result<bool, LuaError> {
153    let top = state.top_count();
154    state.get_info(b"f", ar)?;
155    state.get_field(LUA_REGISTRYINDEX, LUA_LOADED_TABLE)?;
156    check_stack(state, 6, Some(b"not enough stack"))?;
157    if find_field(state, top + 1, 2)? {
158        if state.peek_bytes(-1).map_or(false, |n| n.starts_with(b"_G.")) {
159            let suffix = state.peek_bytes(-1)
160                .map(|n| n[3..].to_vec())
161                .unwrap_or_default();
162            state.push_bytes(&suffix)?;
163            state.remove(-2)?;
164        }
165        state.copy_value(-1, top + 1)?;
166        lua_vm::api::set_top(state, top + 1)?;
167        Ok(true)
168    } else {
169        lua_vm::api::set_top(state, top)?;
170        Ok(false)
171    }
172}
173
174fn push_global_func_name_from_target(
175    state: &mut LuaState,
176    target: &mut LuaState,
177    ar: &mut LuaDebug,
178) -> Result<bool, LuaError> {
179    let top = state.top_count();
180    target.get_info(b"f", ar)?;
181    let func = target.get_at(target.top_idx() - 1);
182    target.pop_n(1);
183    state.push(func);
184    state.get_field(LUA_REGISTRYINDEX, LUA_LOADED_TABLE)?;
185    check_stack(state, 6, Some(b"not enough stack"))?;
186    if find_field(state, top + 1, 2)? {
187        if state.peek_bytes(-1).map_or(false, |n| n.starts_with(b"_G.")) {
188            let suffix = state.peek_bytes(-1)
189                .map(|n| n[3..].to_vec())
190                .unwrap_or_default();
191            state.push_bytes(&suffix)?;
192            state.remove(-2)?;
193        }
194        state.copy_value(-1, top + 1)?;
195        lua_vm::api::set_top(state, top + 1)?;
196        Ok(true)
197    } else {
198        lua_vm::api::set_top(state, top)?;
199        Ok(false)
200    }
201}
202
203/// Push a human-readable name for the function described by `ar`.
204///
205fn push_func_name(
206    state: &mut LuaState,
207    ar: &mut LuaDebug,
208    global_lookup_target: Option<&mut LuaState>,
209) -> Result<(), LuaError> {
210    let found_global = match global_lookup_target {
211        Some(target) => push_global_func_name_from_target(state, target, ar)?,
212        None => push_global_func_name(state, ar)?,
213    };
214    if found_global {
215        let name = state.peek_bytes(-1).unwrap_or_else(|| b"?".to_vec());
216        state.push_fstring(format_args!("function '{}'", BStr(&name)))?;
217        state.remove(-2)?;
218    } else if !ar.namewhat.is_empty() {
219        let namewhat = ar.namewhat.clone();
220        let name = ar.name.clone().unwrap_or_else(|| b"?".to_vec());
221        state.push_fstring(format_args!("{} '{}'", BStr(&namewhat), BStr(&name)))?;
222    } else if ar.what == b'm' {
223        state.push_string(b"main chunk")?;
224    } else if ar.what != b'C' {
225        let src = ar.short_src.clone();
226        let line = ar.linedefined;
227        state.push_fstring(format_args!("function <{}:{}>", BStr(&src), line))?;
228    } else {
229        state.push_string(b"?")?;
230    }
231    Ok(())
232}
233
234/// Binary-search for the last valid stack level in `state`.
235///
236fn last_level(state: &mut LuaState) -> i32 {
237    let mut ar = LuaDebug::default();
238    let mut li: i32 = 1;
239    let mut le: i32 = 1;
240    while state.get_stack(le, &mut ar) {
241        li = le;
242        le *= 2;
243    }
244    // binary search
245    while li < le {
246        let m = (li + le) / 2;
247        if state.get_stack(m, &mut ar) {
248            li = m + 1;
249        } else {
250            le = m;
251        }
252    }
253    le - 1
254}
255
256/// Build a stack traceback string from thread `other` starting at `level`.
257/// If `msg` is non-None it is prepended on its own line.
258/// Leaves the result string on top of `state`.
259///
260/// When `other` is `None`, the traceback is built for `state` itself (the
261/// common single-thread case). Rust's borrow checker forbids passing the same
262/// `&mut LuaState` twice, so we use an `Option` to express the aliasing intent
263/// rather than a separate parameter.
264///
265pub fn traceback(
266    state: &mut LuaState,
267    mut other: Option<&mut LuaState>,
268    msg: Option<&[u8]>,
269    level: i32,
270) -> Result<(), LuaError> {
271    let mut b = LuaBuffer::new();
272    let mut ar = LuaDebug::default();
273    let last = match &mut other {
274        Some(o) => last_level(o),
275        None => last_level(state),
276    };
277    let mut limit2show: i32 = if last - level > LEVELS1 + LEVELS2 { LEVELS1 } else { -1 };
278    buf_init(state, &mut b);
279    if let Some(m) = msg {
280        add_lstring(&mut b, m);
281        add_char(&mut b, b'\n');
282    }
283    add_lstring(&mut b, b"stack traceback:");
284    let mut level = level;
285    loop {
286        let got = match &mut other {
287            Some(o) => o.get_stack(level, &mut ar),
288            None => state.get_stack(level, &mut ar),
289        };
290        if !got {
291            break;
292        }
293        level += 1;
294        if limit2show == 0 {
295            let n = last - level - LEVELS2 + 1;
296            state.push_fstring(format_args!("\n\t...\t(skipping {} levels)", n))?;
297            add_value(state, &mut b)?;
298            level += n;
299            limit2show = LEVELS2;
300        } else {
301            limit2show -= 1;
302            match &mut other {
303                Some(o) => o.get_info(b"Slnt", &mut ar)?,
304                None => state.get_info(b"Slnt", &mut ar)?,
305            }
306            if ar.currentline <= 0 {
307                let src = ar.short_src.clone();
308                state.push_fstring(format_args!("\n\t{}: in ", BStr(&src)))?;
309            } else {
310                let src = ar.short_src.clone();
311                let line = ar.currentline;
312                state.push_fstring(format_args!("\n\t{}:{}: in ", BStr(&src), line))?;
313            }
314            add_value(state, &mut b)?;
315            match &mut other {
316                Some(o) => push_func_name(state, &mut ar, Some(&mut **o))?,
317                None => push_func_name(state, &mut ar, None)?,
318            }
319            add_value(state, &mut b)?;
320            if ar.istailcall {
321                add_lstring(&mut b, b"\n\t(...tail calls...)");
322            }
323        }
324    }
325    push_result(state, &mut b)?;
326    Ok(())
327}
328
329// ── Error-report functions ─────────────────────────────────────────────────────
330
331/// Push an error for argument `arg` with extra message `extramsg`.
332/// Attempts to enrich the message with the calling function's name.
333/// Always returns `Err`.
334///
335pub fn arg_error(
336    state: &mut LuaState,
337    mut arg: i32,
338    extramsg: &[u8],
339) -> Result<usize, LuaError> {
340    let mut ar = LuaDebug::default();
341    if !state.get_stack(0, &mut ar) {
342        return Err(LuaError::runtime(format_args!(
343            "bad argument #{} ({})",
344            arg,
345            BStr(extramsg)
346        )));
347    }
348    state.get_info(b"n", &mut ar)?;
349    if ar.namewhat == b"method" {
350        arg -= 1; // do not count 'self'
351        if arg == 0 {
352            let name = ar.name.clone().unwrap_or_else(|| b"?".to_vec());
353            return Err(LuaError::runtime(format_args!(
354                "calling '{}' on bad self ({})",
355                BStr(&name),
356                BStr(extramsg)
357            )));
358        }
359    }
360    let fname = if ar.name.is_none() {
361        if push_global_func_name(state, &mut ar)? {
362            state.peek_bytes(-1).unwrap_or_else(|| b"?".to_vec())
363        } else {
364            b"?".to_vec()
365        }
366    } else {
367        ar.name.clone().unwrap_or_else(|| b"?".to_vec())
368    };
369    Err(LuaError::runtime(format_args!(
370        "bad argument #{} to '{}' ({})",
371        arg,
372        BStr(&fname),
373        BStr(extramsg)
374    )))
375}
376
377/// Push a type-mismatch error for argument `arg`, stating `tname` was expected.
378/// Always returns `Err`.
379///
380pub fn type_error_arg(
381    state: &mut LuaState,
382    arg: i32,
383    tname: &[u8],
384) -> Result<usize, LuaError> {
385    //      typearg = lua_tostring(L, -1);
386    //    else if (lua_type(L, arg) == LUA_TLIGHTUSERDATA)
387    //      typearg = "light userdata";
388    //    else
389    //      typearg = luaL_typename(L, arg);
390    let typearg: Vec<u8> = if get_metafield(state, arg, b"__name")? == LuaType::String {
391        let bytes = state.peek_bytes(-1).unwrap_or_else(|| b"?".to_vec());
392        state.pop_n(1);
393        bytes
394    } else if state.type_at(arg) == LuaType::LightUserData {
395        b"light userdata".to_vec()
396    } else {
397        state.type_name_at(arg).to_vec()
398    };
399    let msg_owned = format!(
400        "{} expected, got {}",
401        BStr(tname),
402        BStr(&typearg)
403    );
404    arg_error(state, arg, msg_owned.as_bytes())
405}
406
407/// Push a type-tag error for `arg`, using the Lua type name for `tag`.
408///
409fn tag_error(state: &mut LuaState, arg: i32, tag: LuaType) -> Result<(), LuaError> {
410    let name = state.type_name(tag);
411    type_error_arg(state, arg, name)?;
412    Ok(())
413}
414
415/// Push a string describing the location of the call at `level` onto the stack.
416/// If no location is available, pushes an empty string.
417///
418pub fn push_where(state: &mut LuaState, level: i32) -> Result<(), LuaError> {
419    let mut ar = LuaDebug::default();
420    if state.get_stack(level, &mut ar) {
421        state.get_info(b"Sl", &mut ar)?;
422        if ar.currentline > 0 {
423            let src = ar.short_src.clone();
424            let line = ar.currentline;
425            state.push_fstring(format_args!("{}:{}: ", BStr(&src), line))?;
426            return Ok(());
427        }
428    }
429    state.push_string(b"")?;
430    Ok(())
431}
432
433/// Format a runtime error with source location and raise it.
434/// Always returns `Err`.
435///
436///
437/// PORT NOTE: C uses varargs + `lua_pushvfstring`. Rust callers pass a
438/// pre-formatted `&[u8]` message; use `format_args!` at the call site.
439pub fn lua_error(state: &mut LuaState, msg: &[u8]) -> Result<usize, LuaError> {
440    push_where(state, 1)?;
441    let where_str = state.pop_bytes();
442    let full = [where_str.as_slice(), msg].concat();
443    Err(LuaError::runtime(format_args!("{}", BStr(&full))))
444}
445
446/// Push the result of a POSIX-style file operation onto the stack.
447/// On success pushes `true`; on failure pushes `nil, errmsg, errno`.
448/// Returns the number of pushed values.
449///
450pub fn file_result(
451    state: &mut LuaState,
452    stat: bool,
453    fname: Option<&[u8]>,
454) -> Result<usize, LuaError> {
455    if stat {
456        state.push(LuaValue::Bool(true));
457        Ok(1)
458    } else {
459        state.push(LuaValue::Nil);
460        // TODO(port): use std::io::Error::last_os_error() for errno-style message.
461        let errmsg = b"(errno unavailable in Rust port)".to_vec();
462        if let Some(name) = fname {
463            let full = [name, b": ".as_slice(), &errmsg].concat();
464            state.push_bytes(&full)?;
465        } else {
466            state.push_bytes(&errmsg)?;
467        }
468        // TODO(port): push actual errno integer once os-error helpers are available.
469        state.push(LuaValue::Int(0));
470        Ok(3)
471    }
472}
473
474/// Push the result of a process-exit status onto the stack.
475/// Returns 3 values: success-bool-or-nil, exit-kind string, status code.
476///
477// TODO(port): POSIX WIFEXITED / WIFSIGNALED inspection requires cfg(unix).
478pub fn exec_result(state: &mut LuaState, stat: i32) -> Result<usize, LuaError> {
479    if stat != 0 {
480        return file_result(state, false, None);
481    }
482    let what = b"exit".as_slice();
483    state.push(LuaValue::Bool(true));
484    state.push_bytes(what)?;
485    state.push(LuaValue::Int(stat as i64));
486    Ok(3)
487}
488
489// ── Userdata / metatable helpers ──────────────────────────────────────────────
490
491/// Create a new metatable for type `tname` and register it in the registry.
492/// Returns `true` (and leaves new metatable on stack) if the table was created;
493/// returns `false` (and leaves existing table on stack) if already existed.
494///
495pub fn new_metatable(state: &mut LuaState, tname: &[u8]) -> Result<bool, LuaError> {
496    if get_metatable(state, tname)? != LuaType::Nil {
497        return Ok(false); // leave previous value on top
498    }
499    state.pop_n(1);
500    state.create_table(0, 2)?;
501    state.push_bytes(tname)?;
502    state.set_field(-2, b"__name")?;
503    state.push_value(-1)?;
504    state.set_field(LUA_REGISTRYINDEX, tname)?;
505    Ok(true)
506}
507
508/// Set the metatable of the value at stack top to the one registered as `tname`.
509///
510pub fn set_metatable(state: &mut LuaState, tname: &[u8]) -> Result<(), LuaError> {
511    get_metatable(state, tname)?;
512    state.set_metatable(-2)?;
513    Ok(())
514}
515
516/// Check whether the value at `ud` is a full userdata with metatable `tname`.
517/// Returns `Some(userdata)` if yes, `None` otherwise.
518///
519pub fn test_udata(
520    state: &mut LuaState,
521    ud: i32,
522    tname: &[u8],
523) -> Result<Option<GcRef<LuaUserData>>, LuaError> {
524    let p = state.to_userdata(ud);
525    if let Some(p) = p {
526        if state.get_metatable(ud)? {
527            get_metatable(state, tname)?;
528            let eq = state.raw_equal(-1, -2)?;
529            state.pop_n(2); // remove both metatables
530            if eq {
531                return Ok(Some(p));
532            }
533        }
534    }
535    Ok(None)
536}
537
538/// Like `test_udata` but raises a type error if the check fails.
539///
540pub fn check_udata(
541    state: &mut LuaState,
542    ud: i32,
543    tname: &[u8],
544) -> Result<GcRef<LuaUserData>, LuaError> {
545    match test_udata(state, ud, tname)? {
546        Some(p) => Ok(p),
547        None => {
548            type_error_arg(state, ud, tname)?;
549            unreachable!()
550        }
551    }
552}
553
554// ── Argument-check functions ──────────────────────────────────────────────────
555
556/// Check that `arg` is one of the strings in `lst` and return its index.
557/// If `def` is `Some` it is used as default when `arg` is absent/nil.
558///
559pub fn check_option(
560    state: &mut LuaState,
561    arg: i32,
562    def: Option<&[u8]>,
563    lst: &[&[u8]],
564) -> Result<usize, LuaError> {
565    let name: Vec<u8> = match def {
566        Some(d) if state.is_none_or_nil(arg) => d.to_vec(),
567        _ => check_lstring(state, arg)?.as_bytes().to_vec(),
568    };
569    for (i, entry) in lst.iter().enumerate() {
570        if *entry == name.as_slice() {
571            return Ok(i);
572        }
573    }
574    Err(LuaError::runtime(format_args!(
575        "invalid option '{}'",
576        BStr(&name)
577    )))
578}
579
580/// Ensure the stack has at least `space` extra slots; raise on failure.
581///
582pub fn check_stack(
583    state: &mut LuaState,
584    space: i32,
585    msg: Option<&[u8]>,
586) -> Result<(), LuaError> {
587    if !state.check_stack_space(space) {
588        match msg {
589            Some(m) => {
590                return Err(LuaError::runtime(format_args!(
591                    "stack overflow ({})",
592                    BStr(m)
593                )));
594            }
595            None => {
596                return Err(LuaError::runtime(format_args!("stack overflow")));
597            }
598        }
599    }
600    Ok(())
601}
602
603/// Assert that the value at `arg` has Lua type `t`; raise type error otherwise.
604///
605pub fn check_type(state: &mut LuaState, arg: i32, t: LuaType) -> Result<(), LuaError> {
606    if state.type_at(arg) != t {
607        tag_error(state, arg, t)?;
608    }
609    Ok(())
610}
611
612/// Assert that a value (not `none`) is present at `arg`.
613///
614pub fn check_any(state: &mut LuaState, arg: i32) -> Result<(), LuaError> {
615    if state.type_at(arg) == LuaType::None {
616        return Err(LuaError::arg_error(arg, "value expected"));
617    }
618    Ok(())
619}
620
621/// Return the string at `arg` as bytes; raise a type error if not a string.
622///
623pub fn check_lstring(state: &mut LuaState, arg: i32) -> Result<GcRef<LuaString>, LuaError> {
624    match state.to_lua_string(arg) {
625        Some(s) => Ok(s),
626        None => {
627            tag_error(state, arg, LuaType::String)?;
628            unreachable!()
629        }
630    }
631}
632
633/// Return the string at `arg`; if absent/nil return `def`.
634///
635pub fn opt_lstring(
636    state: &mut LuaState,
637    arg: i32,
638    def: Option<&[u8]>,
639) -> Result<Option<Vec<u8>>, LuaError> {
640    if state.is_none_or_nil(arg) {
641        return Ok(def.map(|d| d.to_vec()));
642    }
643    let s = check_lstring(state, arg)?;
644    Ok(Some(s.as_bytes().to_vec()))
645}
646
647/// Return the number at `arg` as `f64`; raise a type error if not a number.
648///
649pub fn check_number(state: &mut LuaState, arg: i32) -> Result<f64, LuaError> {
650    match state.to_number_x(arg) {
651        Some(d) => Ok(d),
652        None => {
653            tag_error(state, arg, LuaType::Number)?;
654            unreachable!()
655        }
656    }
657}
658
659/// Return the number at `arg`; if absent/nil return `def`.
660///
661pub fn opt_number(state: &mut LuaState, arg: i32, def: f64) -> Result<f64, LuaError> {
662    if state.is_none_or_nil(arg) {
663        Ok(def)
664    } else {
665        check_number(state, arg)
666    }
667}
668
669/// Raise an error for a non-integer number argument.
670///
671///
672/// Always returns `Err`. The `Ok` arm uses `unreachable!()` to satisfy the
673/// return type; `!` (never) is nightly-only so we use `Result<usize, LuaError>`.
674fn int_error(state: &mut LuaState, arg: i32) -> Result<usize, LuaError> {
675    if state.is_number(arg) {
676        Err(LuaError::arg_error(
677            arg,
678            "number has no integer representation",
679        ))
680    } else {
681        tag_error(state, arg, LuaType::Number)?;
682        unreachable!("tag_error always returns Err")
683    }
684}
685
686/// Return the integer at `arg` as `i64`; raise if not an integer-convertible number.
687///
688pub fn check_integer(state: &mut LuaState, arg: i32) -> Result<i64, LuaError> {
689    match state.to_integer_x(arg) {
690        Some(d) => Ok(d),
691        None => {
692            int_error(state, arg)?;
693            unreachable!("int_error always returns Err")
694        }
695    }
696}
697
698/// Return the integer at `arg`; if absent/nil return `def`.
699///
700pub fn opt_integer(state: &mut LuaState, arg: i32, def: i64) -> Result<i64, LuaError> {
701    if state.is_none_or_nil(arg) {
702        Ok(def)
703    } else {
704        check_integer(state, arg)
705    }
706}
707
708// ── Buffer manipulation ────────────────────────────────────────────────────────
709
710impl LuaBuffer {
711    /// Create a new empty buffer.
712    ///
713    /// Rust uses `Vec::new()` which starts at zero capacity; capacity is managed by Vec.
714    pub fn new() -> Self {
715        LuaBuffer { data: Vec::new() }
716    }
717
718    /// Returns the number of bytes currently in the buffer.
719    pub fn len(&self) -> usize {
720        self.data.len()
721    }
722}
723
724impl Default for LuaBuffer {
725    fn default() -> Self {
726        LuaBuffer::new()
727    }
728}
729
730/// Initialize `buf` and associate it with `state`.
731/// Pushes a placeholder light-userdata onto `state` to anchor the buffer in C.
732/// In Rust the Vec is self-contained; we still push a placeholder for stack-slot
733/// compatibility with code that later calls `add_value` / `push_result`.
734///
735pub fn buf_init(state: &mut LuaState, buf: &mut LuaBuffer) {
736    // PORT NOTE: C pushes a light-userdata placeholder onto the stack to hold
737    // the buffer's position. We still push nil as a stack slot placeholder so
738    // that add_value / push_result see the same stack layout.
739    *buf = LuaBuffer::new();
740    // We push nil; Phase B can revisit if this matters for GC interaction.
741    let _ = state.push(LuaValue::Nil);
742}
743
744/// Initialize `buf`, reserve `sz` bytes, and return the writable region.
745///
746pub fn buf_init_size(
747    state: &mut LuaState,
748    buf: &mut LuaBuffer,
749    sz: usize,
750) -> Result<(), LuaError> {
751    buf_init(state, buf);
752    buf.data.reserve(sz);
753    Ok(())
754}
755
756/// Compute a new buffer capacity that accommodates `sz` more bytes,
757/// growing by ×1.5 or more.
758///
759fn new_buff_size(buf: &LuaBuffer, sz: usize) -> Result<usize, LuaError> {
760    if usize::MAX - sz < buf.len() {
761        return Err(LuaError::runtime(format_args!("buffer too large")));
762    }
763    let newsize = (buf.data.capacity() / 2) * 3; // ×1.5
764    if newsize < buf.len() + sz {
765        Ok(buf.len() + sz)
766    } else {
767        Ok(newsize)
768    }
769}
770
771/// Ensure at least `sz` free bytes are available in `buf`.
772///
773pub fn prep_buff_size(buf: &mut LuaBuffer, sz: usize) -> Result<(), LuaError> {
774    if buf.data.capacity() - buf.data.len() < sz {
775        let newcap = new_buff_size(buf, sz)?;
776        buf.data.reserve(newcap - buf.data.len());
777    }
778    Ok(())
779}
780
781/// Append `s` to `buf`.
782///
783pub fn add_lstring(buf: &mut LuaBuffer, s: &[u8]) {
784    if !s.is_empty() {
785        buf.data.extend_from_slice(s);
786    }
787}
788
789/// Append a single byte to `buf`.
790///
791pub fn add_char(buf: &mut LuaBuffer, c: u8) {
792    buf.data.push(c);
793}
794
795/// Append `sz` to the length counter (used after writing directly into the buffer).
796///
797pub fn add_size(_buf: &mut LuaBuffer, sz: usize) {
798    // PORT NOTE: In C this is a direct `n += sz` on the inline length field.
799    // With Vec, length is implicit; this is a no-op unless caller wrote past len.
800    // TODO(port): if direct-write into spare capacity is needed, switch to `unsafe`
801    // set_len or redesign; for Phase A this is a no-op.
802    let _ = sz;
803}
804
805/// Pop the string at top of `state`'s stack and append it to `buf`.
806///
807pub fn add_value(state: &mut LuaState, buf: &mut LuaBuffer) -> Result<(), LuaError> {
808    if let Some(bytes) = state.peek_bytes(-1) {
809        let owned = bytes.to_vec();
810        add_lstring(buf, &owned);
811    }
812    state.pop_n(1);
813    Ok(())
814}
815
816/// Push the buffer contents as a Lua string onto `state`'s stack.
817///
818pub fn push_result(state: &mut LuaState, buf: &mut LuaBuffer) -> Result<(), LuaError> {
819    state.push_bytes(&buf.data)?;
820    state.remove(-2)?;
821    Ok(())
822}
823
824/// Add `sz` bytes to the buffer count then call `push_result`.
825///
826pub fn push_result_size(
827    state: &mut LuaState,
828    buf: &mut LuaBuffer,
829    sz: usize,
830) -> Result<(), LuaError> {
831    add_size(buf, sz);
832    push_result(state, buf)
833}
834
835/// Perform global byte-string substitution: replace all occurrences of `pat`
836/// with `repl` in `s`, appending results into `buf`.
837///
838pub fn add_gsub(buf: &mut LuaBuffer, s: &[u8], pat: &[u8], repl: &[u8]) {
839    if pat.is_empty() {
840        add_lstring(buf, s);
841        return;
842    }
843    let mut remaining = s;
844    while let Some(pos) = find_bytes(remaining, pat) {
845        add_lstring(buf, &remaining[..pos]);
846        add_lstring(buf, repl);
847        remaining = &remaining[pos + pat.len()..];
848    }
849    add_lstring(buf, remaining);
850}
851
852/// Build a string from `s` by replacing `pat` with `repl`, push it on the stack,
853/// and return the bytes of the pushed string.
854///
855pub fn gsub<'a>(
856    state: &'a mut LuaState,
857    s: &[u8],
858    pat: &[u8],
859    repl: &[u8],
860) -> Result<Vec<u8>, LuaError> {
861    let mut b = LuaBuffer::new();
862    buf_init(state, &mut b);
863    add_gsub(&mut b, s, pat, repl);
864    push_result(state, &mut b)?;
865    Ok(state.peek_bytes(-1).unwrap_or_default())
866}
867
868/// Find `needle` in `haystack`, returning the byte offset or `None`.
869///
870/// Internal helper replacing C's `strstr`.
871fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
872    if needle.is_empty() {
873        return Some(0);
874    }
875    haystack.windows(needle.len()).position(|w| w == needle)
876}
877
878// ── Reference system ──────────────────────────────────────────────────────────
879
880/// Store the value at the top of the stack in table `t` and return a unique
881/// integer reference. If the value is `nil`, returns `LUA_REFNIL` without
882/// modifying the table.
883///
884pub fn lua_ref(state: &mut LuaState, t: i32) -> Result<i32, LuaError> {
885    if state.type_at(-1) == LuaType::Nil {
886        state.pop_n(1);
887        return Ok(LUA_REFNIL);
888    }
889    let t = state.abs_index(t);
890    let ref_val: i32;
891    if state.raw_get_i(t, FREELIST_REF)? == LuaType::Nil {
892        ref_val = 0; // list is empty
893        state.push(LuaValue::Int(0));
894        state.raw_set_i(t, FREELIST_REF)?;
895    } else {
896        debug_assert!(state.type_at(-1) == LuaType::Number);
897        ref_val = state.to_integer_x(-1).unwrap_or(0) as i32;
898    }
899    state.pop_n(1); // remove element from stack
900    let next_ref: i32;
901    if ref_val != 0 {
902        state.raw_get_i(t, ref_val as i64)?;
903        state.raw_set_i(t, FREELIST_REF)?;
904        next_ref = ref_val;
905    } else {
906        next_ref = (state.raw_len(t) as i32) + 1;
907    }
908    state.raw_set_i(t, next_ref as i64)?;
909    Ok(next_ref)
910}
911
912/// Release reference `ref` from table `t`, adding it to the free list.
913///
914pub fn lua_unref(state: &mut LuaState, t: i32, r: i32) -> Result<(), LuaError> {
915    if r >= 0 {
916        let t = state.abs_index(t);
917        state.raw_get_i(t, FREELIST_REF)?;
918        debug_assert!(state.type_at(-1) == LuaType::Number);
919        state.raw_set_i(t, r as i64)?;
920        state.push(LuaValue::Int(r as i64));
921        state.raw_set_i(t, FREELIST_REF)?;
922    }
923    Ok(())
924}
925
926// ── Load functions ─────────────────────────────────────────────────────────────
927
928/// Internal chunk reader that returns a single buffer slice then signals EOF.
929///
930fn make_string_reader(data: Vec<u8>) -> impl FnMut() -> Option<Vec<u8>> {
931    let mut remaining = Some(data);
932    move || remaining.take()
933}
934
935/// Strip an optional UTF-8 BOM (EF BB BF) and any `#`-prefixed first line.
936///
937/// PORT NOTE: C reads byte-by-byte with `getc`/`feof` and lazily reopens the
938/// file in binary mode if it looks like a binary chunk. Here we slurp the file
939/// into memory (`std::fs::read`), strip the BOM, and let `lua_vm::api::load`
940/// dispatch text vs. binary by the first byte. The "binary chunk" branch in
941/// `luaL_loadfilex` exists in C because text mode does newline translation;
942/// `std::fs::read` already returns raw bytes on every platform we support.
943fn skip_bom_and_shebang(buf: &[u8]) -> Vec<u8> {
944    let s = if buf.starts_with(b"\xEF\xBB\xBF") { &buf[3..] } else { buf };
945    if s.first() == Some(&b'#') {
946        let nl = s.iter().position(|&b| b == b'\n').map(|p| p + 1).unwrap_or(s.len());
947        let rest = &s[nl..];
948        if rest.first() == Some(&0x1B) {
949            rest.to_vec()
950        } else {
951            let mut out = Vec::with_capacity(rest.len() + 1);
952            out.push(b'\n');
953            out.extend_from_slice(rest);
954            out
955        }
956    } else {
957        s.to_vec()
958    }
959}
960
961/// Load a file as a Lua chunk. Returns `LUA_OK` on success or an error code.
962///
963///
964/// PORT NOTE: PORTING.md §1 bans `std::fs` outside `lua-cli`, but C-Lua's
965/// `luaL_loadfilex` is part of the auxiliary library (`lauxlib.c`) and is
966/// reachable from the base library (`loadfile`/`dofile`). Phase A's stub
967/// raised an error here, which broke `loadfile(missing)` returning `nil, err`.
968/// The real C semantics push an error string onto the stack and return a
969/// non-zero status, which `load_aux` then converts to `(nil, errmsg)`.
970pub fn load_filex(
971    state: &mut LuaState,
972    filename: Option<&[u8]>,
973    mode: Option<&[u8]>,
974) -> Result<i32, LuaError> {
975    let _ = mode;
976    let fname = match filename {
977        Some(f) => f,
978        None => {
979            // TODO(port): stdin loading not yet supported in lua-stdlib; return
980            // an error string matching C's "cannot read stdin" shape.
981            state.push_string(b"cannot read stdin: no filename given")?;
982            return Ok(LUA_ERRFILE);
983        }
984    };
985    let path = match std::str::from_utf8(fname) {
986        Ok(s) => std::path::PathBuf::from(s),
987        Err(_) => {
988            state.push_fstring(format_args!(
989                "cannot open {}: invalid utf-8 in filename",
990                BStr(fname)
991            ))?;
992            return Ok(LUA_ERRFILE);
993        }
994    };
995    let raw = match std::fs::read(&path) {
996        Ok(bytes) => bytes,
997        Err(e) => {
998            state.push_fstring(format_args!(
999                "cannot open {}: {}",
1000                BStr(fname),
1001                e
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 now reads via std::fs::read and pushes an error
1370//                  string on open failure so loadfile/dofile return (nil, err)
1371//                  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// ──────────────────────────────────────────────────────────────────────────