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