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