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/// Pseudo-reference returned by `lua_ref` when the pushed value was `nil`.
34pub const LUA_REFNIL: i32 = -1;
35
36/// Pseudo-reference meaning "no reference" (never created by `lua_ref`).
37pub const LUA_NOREF: i32 = -2;
38
39/// Extended error code: file-related I/O error from `load_filex`.
40pub const LUA_ERRFILE: i32 = 6;
41
42/// Registry key for the table of loaded modules.
43pub const LUA_LOADED_TABLE: &[u8] = b"_LOADED";
44
45/// Registry key for the table of preloaded loaders.
46pub const LUA_PRELOAD_TABLE: &[u8] = b"_PRELOAD";
47
48/// Name of the global environment table.
49pub const LUA_GNAME: &[u8] = b"_G";
50
51/// Metatable name / file-handle key for the IO library.
52pub const LUA_FILE_HANDLE: &[u8] = b"FILE*";
53
54/// Pseudo-index for the Lua registry.
55const LUA_REGISTRYINDEX: i32 = -1_001_000;
56
57/// Minimum number of extra stack slots `lua_checkstack` guarantees per call.
58#[expect(
59    dead_code,
60    reason = "ported stdlib helper; not yet wired into the runtime"
61)]
62const LUA_MINSTACK: i32 = 20;
63
64// ── Public types ──────────────────────────────────────────────────────────────
65
66/// A function-registration entry for `set_funcs`.
67///
68///
69/// In Rust, `name` is `&'static [u8]` (never `&str`). A `None` func is a
70/// placeholder that pushes `false` rather than a closure.
71pub struct LuaReg {
72    pub name: &'static [u8],
73    pub func: Option<fn(&mut LuaState) -> Result<usize, LuaError>>,
74}
75
76/// Growable byte-buffer used by the auxiliary library for building strings.
77///
78///
79/// The C version uses a small inline initial buffer with overflow managed via
80/// a Lua-stack userdata box. The Rust port collapses this to a plain `Vec<u8>`.
81/// All buffer mutating functions take `&mut LuaState` as a separate parameter.
82pub struct LuaBuffer {
83    pub data: Vec<u8>,
84}
85
86/// File-stream handle used by the IO library.
87///
88///
89/// `closef` in C is a `lua_CFunction`.
90pub struct LuaStream {
91    /// The underlying file handle. `None` for incompletely opened or closed streams.
92    pub f: Option<Box<dyn std::io::Read>>,
93    /// Optional close function (None for already-closed streams).
94    pub closef: Option<fn(&mut LuaState) -> Result<usize, LuaError>>,
95}
96
97// ── Traceback ─────────────────────────────────────────────────────────────────
98
99/// Search for `objidx` in the table at the top of the stack.
100/// `objidx` must be an absolute API stack index.
101/// Returns `true` (and leaves name string on top) when found.
102///
103fn find_field(state: &mut LuaState, objidx: i32, level: i32) -> Result<bool, LuaError> {
104    if level == 0 || state.type_at(-1) != LuaType::Table {
105        return Ok(false);
106    }
107    state.push(LuaValue::Nil);
108    while state.table_next(-2)? {
109        if state.type_at(-2) == LuaType::String {
110            if state.raw_equal(objidx, -1)? {
111                state.pop_n(1); // remove value (keep name)
112                return Ok(true);
113            } else if find_field(state, objidx, level - 1)? {
114                // stack: lib_name, lib_table, field_name (top)
115                state.push_string(b".")?; // place '.' between the two names
116                state.replace(-3)?; // in the slot occupied by table
117                state.concat(3)?; // lib_name.field_name
118                return Ok(true);
119            }
120        }
121        state.pop_n(1); // remove value
122    }
123    Ok(false)
124}
125
126/// Search all loaded modules for a global name for the function at `top+1`.
127/// Returns `true` and leaves name string on top (at `top+1`) if found.
128///
129fn push_global_func_name(state: &mut LuaState, ar: &mut LuaDebug) -> Result<bool, LuaError> {
130    if state.global().lua_version == lua_types::LuaVersion::V51 {
131        return Ok(false);
132    }
133    let top = state.top_count();
134    state.get_info(b"f", ar)?;
135    state.get_field(LUA_REGISTRYINDEX, LUA_LOADED_TABLE)?;
136    check_stack(state, 6, Some(b"not enough stack"))?;
137    if find_field(state, top + 1, 2)? {
138        if state
139            .peek_bytes(-1)
140            .map_or(false, |n| n.starts_with(b"_G."))
141        {
142            let suffix = state
143                .peek_bytes(-1)
144                .map(|n| n[3..].to_vec())
145                .unwrap_or_default();
146            state.push_bytes(&suffix)?;
147            state.remove(-2)?;
148        }
149        state.copy_value(-1, top + 1)?;
150        lua_vm::api::set_top(state, top + 1)?;
151        Ok(true)
152    } else {
153        lua_vm::api::set_top(state, top)?;
154        Ok(false)
155    }
156}
157
158fn push_global_func_name_from_target(
159    state: &mut LuaState,
160    target: &mut LuaState,
161    ar: &mut LuaDebug,
162) -> Result<bool, LuaError> {
163    if state.global().lua_version == lua_types::LuaVersion::V51 {
164        return Ok(false);
165    }
166    let top = state.top_count();
167    target.get_info(b"f", ar)?;
168    let func = target.get_at(target.top_idx() - 1);
169    target.pop_n(1);
170    state.push(func);
171    state.get_field(LUA_REGISTRYINDEX, LUA_LOADED_TABLE)?;
172    check_stack(state, 6, Some(b"not enough stack"))?;
173    if find_field(state, top + 1, 2)? {
174        if state
175            .peek_bytes(-1)
176            .map_or(false, |n| n.starts_with(b"_G."))
177        {
178            let suffix = state
179                .peek_bytes(-1)
180                .map(|n| n[3..].to_vec())
181                .unwrap_or_default();
182            state.push_bytes(&suffix)?;
183            state.remove(-2)?;
184        }
185        state.copy_value(-1, top + 1)?;
186        lua_vm::api::set_top(state, top + 1)?;
187        Ok(true)
188    } else {
189        lua_vm::api::set_top(state, top)?;
190        Ok(false)
191    }
192}
193
194/// Push a human-readable name for the function described by `ar`.
195///
196/// Lua 5.2's `pushfuncname` predates the global-name lookup
197/// (`pushglobalfuncname`/`findfield`), which was introduced in 5.3. It renders a
198/// named function as `function 'name'` directly from `namewhat`/`name`, so a C
199/// function reached by a `debug.traceback`-style call shows the unqualified
200/// `function 'traceback'` rather than the 5.3+ qualified `function
201/// 'debug.traceback'`. 5.1 has its own traceback path (`traceback_51`).
202fn push_func_name(
203    state: &mut LuaState,
204    ar: &mut LuaDebug,
205    global_lookup_target: Option<&mut LuaState>,
206) -> Result<(), LuaError> {
207    if state.global().lua_version == lua_types::LuaVersion::V52 {
208        if !ar.namewhat.is_empty() {
209            let name = ar.name.clone().unwrap_or_else(|| b"?".to_vec());
210            state.push_fstring(format_args!("function '{}'", BStr(&name)))?;
211        } else if ar.what == b'm' {
212            state.push_string(b"main chunk")?;
213        } else if ar.what == b'C' {
214            state.push_string(b"?")?;
215        } else {
216            let src = ar.short_src.clone();
217            let line = ar.linedefined;
218            state.push_fstring(format_args!("function <{}:{}>", BStr(&src), line))?;
219        }
220        return Ok(());
221    }
222    // Lua 5.5 reordered `pushfuncname` to prefer the `namewhat`
223    // (`global`/`field`/`method`/`local`/`upvalue`) over the global-name
224    // lookup, so a global C/Lua function renders `in global 'name'` rather than
225    // `in function 'name'`. 5.3/5.4 try the global-name lookup first.
226    let namewhat_first = state.global().lua_version == lua_types::LuaVersion::V55;
227    if namewhat_first && !ar.namewhat.is_empty() {
228        let namewhat = ar.namewhat.clone();
229        let name = ar.name.clone().unwrap_or_else(|| b"?".to_vec());
230        state.push_fstring(format_args!("{} '{}'", BStr(&namewhat), BStr(&name)))?;
231        return Ok(());
232    }
233    let found_global = match global_lookup_target {
234        Some(target) => push_global_func_name_from_target(state, target, ar)?,
235        None => push_global_func_name(state, ar)?,
236    };
237    if found_global {
238        let name = state.peek_bytes(-1).unwrap_or_else(|| b"?".to_vec());
239        state.push_fstring(format_args!("function '{}'", BStr(&name)))?;
240        state.remove(-2)?;
241    } else if !ar.namewhat.is_empty() {
242        let namewhat = ar.namewhat.clone();
243        let name = ar.name.clone().unwrap_or_else(|| b"?".to_vec());
244        state.push_fstring(format_args!("{} '{}'", BStr(&namewhat), BStr(&name)))?;
245    } else if ar.what == b'm' {
246        state.push_string(b"main chunk")?;
247    } else if ar.what != b'C' {
248        let src = ar.short_src.clone();
249        let line = ar.linedefined;
250        state.push_fstring(format_args!("function <{}:{}>", BStr(&src), line))?;
251    } else {
252        state.push_string(b"?")?;
253    }
254    Ok(())
255}
256
257/// Binary-search for the last valid stack level in `state`.
258///
259fn last_level(state: &mut LuaState) -> i32 {
260    let mut ar = LuaDebug::default();
261    let mut li: i32 = 1;
262    let mut le: i32 = 1;
263    while state.get_stack(le, &mut ar) {
264        li = le;
265        le *= 2;
266    }
267    // binary search
268    while li < le {
269        let m = (li + le) / 2;
270        if state.get_stack(m, &mut ar) {
271            li = m + 1;
272        } else {
273            le = m;
274        }
275    }
276    le - 1
277}
278
279/// Build a stack traceback string from thread `other` starting at `level`.
280/// If `msg` is non-None it is prepended on its own line.
281/// Leaves the result string on top of `state`.
282///
283/// When `other` is `None`, the traceback is built for `state` itself (the
284/// common single-thread case). Rust's borrow checker forbids passing the same
285/// `&mut LuaState` twice, so we use an `Option` to express the aliasing intent
286/// rather than a separate parameter.
287///
288pub fn traceback(
289    state: &mut LuaState,
290    mut other: Option<&mut LuaState>,
291    msg: Option<&[u8]>,
292    level: i32,
293) -> Result<(), LuaError> {
294    if state.global().lua_version == lua_types::LuaVersion::V51 {
295        return traceback_51(state, other, msg, level);
296    }
297
298    let mut b = LuaBuffer::new();
299    let mut ar = LuaDebug::default();
300    let last = match &mut other {
301        Some(o) => last_level(o),
302        None => last_level(state),
303    };
304    let mut limit2show: i32 = if last - level > LEVELS1 + LEVELS2 {
305        LEVELS1
306    } else {
307        -1
308    };
309    buf_init(state, &mut b);
310    if let Some(m) = msg {
311        add_lstring(&mut b, m);
312        add_char(&mut b, b'\n');
313    }
314    add_lstring(&mut b, b"stack traceback:");
315    let mut level = level;
316    loop {
317        let got = match &mut other {
318            Some(o) => o.get_stack(level, &mut ar),
319            None => state.get_stack(level, &mut ar),
320        };
321        if !got {
322            break;
323        }
324        level += 1;
325        if limit2show == 0 {
326            let n = last - level - LEVELS2 + 1;
327            state.push_fstring(format_args!("\n\t...\t(skipping {} levels)", n))?;
328            add_value(state, &mut b)?;
329            level += n;
330            limit2show = LEVELS2;
331        } else {
332            limit2show -= 1;
333            match &mut other {
334                Some(o) => o.get_info(b"Slnt", &mut ar)?,
335                None => state.get_info(b"Slnt", &mut ar)?,
336            }
337            if ar.currentline <= 0 {
338                let src = ar.short_src.clone();
339                state.push_fstring(format_args!("\n\t{}: in ", BStr(&src)))?;
340            } else {
341                let src = ar.short_src.clone();
342                let line = ar.currentline;
343                state.push_fstring(format_args!("\n\t{}:{}: in ", BStr(&src), line))?;
344            }
345            add_value(state, &mut b)?;
346            match &mut other {
347                Some(o) => push_func_name(state, &mut ar, Some(&mut **o))?,
348                None => push_func_name(state, &mut ar, None)?,
349            }
350            add_value(state, &mut b)?;
351            if ar.istailcall {
352                add_lstring(&mut b, b"\n\t(...tail calls...)");
353            }
354        }
355    }
356    push_result(state, &mut b)?;
357    Ok(())
358}
359
360fn traceback_51(
361    state: &mut LuaState,
362    mut other: Option<&mut LuaState>,
363    msg: Option<&[u8]>,
364    level: i32,
365) -> Result<(), LuaError> {
366    const LEVELS1_51: i32 = 12;
367    const LEVELS2_51: i32 = 10;
368
369    let mut b = LuaBuffer::new();
370    let mut ar = LuaDebug::default();
371    let mut firstpart = true;
372    buf_init(state, &mut b);
373    if let Some(m) = msg {
374        add_lstring(&mut b, m);
375        add_char(&mut b, b'\n');
376    }
377    add_lstring(&mut b, b"stack traceback:");
378
379    let mut level = level;
380    loop {
381        let got = match &mut other {
382            Some(o) => o.get_stack(level, &mut ar),
383            None => state.get_stack(level, &mut ar),
384        };
385        if !got {
386            break;
387        }
388        level += 1;
389        if level > LEVELS1_51 && firstpart {
390            let has_tail = match &mut other {
391                Some(o) => o.get_stack(level + LEVELS2_51, &mut ar),
392                None => state.get_stack(level + LEVELS2_51, &mut ar),
393            };
394            if !has_tail {
395                level -= 1;
396            } else {
397                add_lstring(&mut b, b"\n\t...");
398                while match &mut other {
399                    Some(o) => o.get_stack(level + LEVELS2_51, &mut ar),
400                    None => state.get_stack(level + LEVELS2_51, &mut ar),
401                } {
402                    level += 1;
403                }
404            }
405            firstpart = false;
406            continue;
407        }
408
409        match &mut other {
410            Some(o) => o.get_info(b"Snl", &mut ar)?,
411            None => state.get_info(b"Snl", &mut ar)?,
412        }
413        add_lstring(&mut b, b"\n\t");
414        add_lstring(&mut b, &ar.short_src);
415        add_char(&mut b, b':');
416        if ar.currentline > 0 {
417            state.push_fstring(format_args!("{}:", ar.currentline))?;
418            add_value(state, &mut b)?;
419        }
420        if !ar.namewhat.is_empty() {
421            let name = ar.name.clone().unwrap_or_else(|| b"?".to_vec());
422            state.push_fstring(format_args!(" in function '{}'", BStr(&name)))?;
423            add_value(state, &mut b)?;
424        } else if ar.what == b'm' {
425            add_lstring(&mut b, b" in main chunk");
426        } else if ar.what == b'C' || ar.what == b't' {
427            add_lstring(&mut b, b" ?");
428        } else {
429            let src = ar.short_src.clone();
430            let line = ar.linedefined;
431            state.push_fstring(format_args!(" in function <{}:{}>", BStr(&src), line))?;
432            add_value(state, &mut b)?;
433        }
434    }
435
436    push_result(state, &mut b)?;
437    Ok(())
438}
439
440// ── Error-report functions ─────────────────────────────────────────────────────
441
442/// Push an error for argument `arg` with extra message `extramsg`.
443/// Attempts to enrich the message with the calling function's name.
444/// Always returns `Err`.
445///
446pub fn arg_error(state: &mut LuaState, mut arg: i32, extramsg: &[u8]) -> Result<usize, LuaError> {
447    let mut ar = LuaDebug::default();
448    if !state.get_stack(0, &mut ar) {
449        return Err(LuaError::runtime(format_args!(
450            "bad argument #{} ({})",
451            arg,
452            BStr(extramsg)
453        )));
454    }
455    state.get_info(b"n", &mut ar)?;
456    if ar.namewhat == b"method" {
457        arg -= 1; // do not count 'self'
458        if arg == 0 {
459            let name = ar.name.clone().unwrap_or_else(|| b"?".to_vec());
460            return Err(LuaError::runtime(format_args!(
461                "calling '{}' on bad self ({})",
462                BStr(&name),
463                BStr(extramsg)
464            )));
465        }
466    }
467    let fname = if ar.name.is_none() {
468        if push_global_func_name(state, &mut ar)? {
469            state.peek_bytes(-1).unwrap_or_else(|| b"?".to_vec())
470        } else {
471            b"?".to_vec()
472        }
473    } else {
474        ar.name.clone().unwrap_or_else(|| b"?".to_vec())
475    };
476    Err(LuaError::runtime(format_args!(
477        "bad argument #{} to '{}' ({})",
478        arg,
479        BStr(&fname),
480        BStr(extramsg)
481    )))
482}
483
484/// Push a type-mismatch error for argument `arg`, stating `tname` was expected.
485/// Always returns `Err`.
486///
487pub fn type_error_arg(state: &mut LuaState, arg: i32, tname: &[u8]) -> Result<usize, LuaError> {
488    //      typearg = lua_tostring(L, -1);
489    //    else if (lua_type(L, arg) == LUA_TLIGHTUSERDATA)
490    //      typearg = "light userdata";
491    //    else
492    //      typearg = luaL_typename(L, arg);
493    let honors_name = state.global().lua_version.honors_name_metafield();
494    let typearg: Vec<u8> = if honors_name && get_metafield(state, arg, b"__name")? == LuaType::String {
495        let bytes = state.peek_bytes(-1).unwrap_or_else(|| b"?".to_vec());
496        state.pop_n(1);
497        bytes
498    } else if state.type_at(arg) == LuaType::LightUserData {
499        b"light userdata".to_vec()
500    } else if state.type_at(arg) == LuaType::None {
501        b"no value".to_vec()
502    } else {
503        state.type_name_at(arg).to_vec()
504    };
505    let msg_owned = format!("{} expected, got {}", BStr(tname), BStr(&typearg));
506    arg_error(state, arg, msg_owned.as_bytes())
507}
508
509/// Push a type-tag error for `arg`, using the Lua type name for `tag`.
510///
511fn tag_error(state: &mut LuaState, arg: i32, tag: LuaType) -> Result<(), LuaError> {
512    let name = state.type_name(tag);
513    type_error_arg(state, arg, name)?;
514    Ok(())
515}
516
517/// Push a string describing the location of the call at `level` onto the stack.
518/// If no location is available, pushes an empty string.
519///
520pub fn push_where(state: &mut LuaState, level: i32) -> Result<(), LuaError> {
521    let mut ar = LuaDebug::default();
522    if state.get_stack(level, &mut ar) {
523        state.get_info(b"Sl", &mut ar)?;
524        if ar.currentline > 0 {
525            let src = ar.short_src.clone();
526            let line = ar.currentline;
527            state.push_fstring(format_args!("{}:{}: ", BStr(&src), line))?;
528            return Ok(());
529        }
530    }
531    state.push_string(b"")?;
532    Ok(())
533}
534
535/// Format a runtime error with source location and raise it.
536/// Always returns `Err`.
537///
538/// Callers pass a pre-formatted `&[u8]` message; use `format_args!` at the
539/// call site rather than varargs.
540pub fn lua_error(state: &mut LuaState, msg: &[u8]) -> Result<usize, LuaError> {
541    push_where(state, 1)?;
542    let where_str = state.pop_bytes();
543    let full = [where_str.as_slice(), msg].concat();
544    Err(LuaError::runtime(format_args!("{}", BStr(&full))))
545}
546
547/// Push the result of a POSIX-style file operation onto the stack.
548/// On success pushes `true`; on failure pushes `nil, errmsg, errno`.
549/// Returns the number of pushed values.
550///
551pub fn file_result(
552    state: &mut LuaState,
553    stat: bool,
554    fname: Option<&[u8]>,
555) -> Result<usize, LuaError> {
556    if stat {
557        state.push(LuaValue::Bool(true));
558        Ok(1)
559    } else {
560        state.push(LuaValue::Nil);
561        let errmsg = b"(errno unavailable in Rust port)".to_vec();
562        if let Some(name) = fname {
563            let full = [name, b": ".as_slice(), &errmsg].concat();
564            state.push_bytes(&full)?;
565        } else {
566            state.push_bytes(&errmsg)?;
567        }
568        state.push(LuaValue::Int(0));
569        Ok(3)
570    }
571}
572
573/// Push the result of a process-exit status onto the stack.
574/// Returns 3 values: success-bool-or-nil, exit-kind string, status code.
575pub fn exec_result(state: &mut LuaState, stat: i32) -> Result<usize, LuaError> {
576    if stat != 0 {
577        return file_result(state, false, None);
578    }
579    let what = b"exit".as_slice();
580    state.push(LuaValue::Bool(true));
581    state.push_bytes(what)?;
582    state.push(LuaValue::Int(stat as i64));
583    Ok(3)
584}
585
586// ── Userdata / metatable helpers ──────────────────────────────────────────────
587
588/// Create a new metatable for type `tname` and register it in the registry.
589/// Returns `true` (and leaves new metatable on stack) if the table was created;
590/// returns `false` (and leaves existing table on stack) if already existed.
591///
592pub fn new_metatable(state: &mut LuaState, tname: &[u8]) -> Result<bool, LuaError> {
593    if get_metatable(state, tname)? != LuaType::Nil {
594        return Ok(false); // leave previous value on top
595    }
596    state.pop_n(1);
597    state.create_table(0, 2)?;
598    state.push_bytes(tname)?;
599    state.set_field(-2, b"__name")?;
600    state.push_value(-1)?;
601    state.set_field(LUA_REGISTRYINDEX, tname)?;
602    Ok(true)
603}
604
605/// Set the metatable of the value at stack top to the one registered as `tname`.
606///
607pub fn set_metatable(state: &mut LuaState, tname: &[u8]) -> Result<(), LuaError> {
608    get_metatable(state, tname)?;
609    state.set_metatable(-2)?;
610    Ok(())
611}
612
613/// Check whether the value at `ud` is a full userdata with metatable `tname`.
614/// Returns `Some(userdata)` if yes, `None` otherwise.
615///
616pub fn test_udata(
617    state: &mut LuaState,
618    ud: i32,
619    tname: &[u8],
620) -> Result<Option<GcRef<LuaUserData>>, LuaError> {
621    let p = state.to_userdata(ud);
622    if let Some(p) = p {
623        if state.get_metatable(ud)? {
624            get_metatable(state, tname)?;
625            let eq = state.raw_equal(-1, -2)?;
626            state.pop_n(2); // remove both metatables
627            if eq {
628                return Ok(Some(p));
629            }
630        }
631    }
632    Ok(None)
633}
634
635/// Like `test_udata` but raises a type error if the check fails.
636///
637pub fn check_udata(
638    state: &mut LuaState,
639    ud: i32,
640    tname: &[u8],
641) -> Result<GcRef<LuaUserData>, LuaError> {
642    match test_udata(state, ud, tname)? {
643        Some(p) => Ok(p),
644        None => {
645            type_error_arg(state, ud, tname)?;
646            unreachable!()
647        }
648    }
649}
650
651// ── Argument-check functions ──────────────────────────────────────────────────
652
653/// Check that `arg` is one of the strings in `lst` and return its index.
654/// If `def` is `Some` it is used as default when `arg` is absent/nil.
655///
656pub fn check_option(
657    state: &mut LuaState,
658    arg: i32,
659    def: Option<&[u8]>,
660    lst: &[&[u8]],
661) -> Result<usize, LuaError> {
662    let name: Vec<u8> = match def {
663        Some(d) if state.is_none_or_nil(arg) => d.to_vec(),
664        _ => check_lstring(state, arg)?.as_bytes().to_vec(),
665    };
666    for (i, entry) in lst.iter().enumerate() {
667        if *entry == name.as_slice() {
668            return Ok(i);
669        }
670    }
671    Err(LuaError::runtime(format_args!(
672        "invalid option '{}'",
673        BStr(&name)
674    )))
675}
676
677/// Ensure the stack has at least `space` extra slots; raise on failure.
678///
679pub fn check_stack(state: &mut LuaState, space: i32, msg: Option<&[u8]>) -> Result<(), LuaError> {
680    if !state.check_stack_space(space) {
681        match msg {
682            Some(m) => {
683                return Err(LuaError::runtime(format_args!(
684                    "stack overflow ({})",
685                    BStr(m)
686                )));
687            }
688            None => {
689                return Err(LuaError::runtime(format_args!("stack overflow")));
690            }
691        }
692    }
693    Ok(())
694}
695
696/// Assert that the value at `arg` has Lua type `t`; raise type error otherwise.
697///
698pub fn check_type(state: &mut LuaState, arg: i32, t: LuaType) -> Result<(), LuaError> {
699    if state.type_at(arg) != t {
700        tag_error(state, arg, t)?;
701    }
702    Ok(())
703}
704
705/// Assert that a value (not `none`) is present at `arg`.
706///
707pub fn check_any(state: &mut LuaState, arg: i32) -> Result<(), LuaError> {
708    if state.type_at(arg) == LuaType::None {
709        arg_error(state, arg, b"value expected")?;
710    }
711    Ok(())
712}
713
714/// Return the string at `arg` as bytes; raise a type error if not a string.
715///
716pub fn check_lstring(state: &mut LuaState, arg: i32) -> Result<GcRef<LuaString>, LuaError> {
717    match state.to_lua_string(arg) {
718        Some(s) => Ok(s),
719        None => {
720            tag_error(state, arg, LuaType::String)?;
721            unreachable!()
722        }
723    }
724}
725
726/// Return the string at `arg`; if absent/nil return `def`.
727///
728pub fn opt_lstring(
729    state: &mut LuaState,
730    arg: i32,
731    def: Option<&[u8]>,
732) -> Result<Option<Vec<u8>>, LuaError> {
733    if state.is_none_or_nil(arg) {
734        return Ok(def.map(|d| d.to_vec()));
735    }
736    let s = check_lstring(state, arg)?;
737    Ok(Some(s.as_bytes().to_vec()))
738}
739
740/// Return the number at `arg` as `f64`; raise a type error if not a number.
741///
742pub fn check_number(state: &mut LuaState, arg: i32) -> Result<f64, LuaError> {
743    match state.to_number_x(arg) {
744        Some(d) => Ok(d),
745        None => {
746            tag_error(state, arg, LuaType::Number)?;
747            unreachable!()
748        }
749    }
750}
751
752/// Return the number at `arg`; if absent/nil return `def`.
753///
754pub fn opt_number(state: &mut LuaState, arg: i32, def: f64) -> Result<f64, LuaError> {
755    if state.is_none_or_nil(arg) {
756        Ok(def)
757    } else {
758        check_number(state, arg)
759    }
760}
761
762/// Raise an error for a non-integer number argument.
763///
764///
765/// Always returns `Err`. The `Ok` arm uses `unreachable!()` to satisfy the
766/// return type; `!` (never) is nightly-only so we use `Result<usize, LuaError>`.
767fn int_error(state: &mut LuaState, arg: i32) -> Result<usize, LuaError> {
768    if state.is_number(arg) {
769        arg_error(state, arg, b"number has no integer representation")
770    } else {
771        tag_error(state, arg, LuaType::Number)?;
772        unreachable!("tag_error always returns Err")
773    }
774}
775
776/// Return the integer at `arg` as `i64`; raise if not an integer-convertible number.
777///
778pub fn check_integer(state: &mut LuaState, arg: i32) -> Result<i64, LuaError> {
779    match state.to_integer_x(arg) {
780        Some(d) => Ok(d),
781        None => {
782            int_error(state, arg)?;
783            unreachable!("int_error always returns Err")
784        }
785    }
786}
787
788/// Return the integer at `arg`; if absent/nil return `def`.
789///
790pub fn opt_integer(state: &mut LuaState, arg: i32, def: i64) -> Result<i64, LuaError> {
791    if state.is_none_or_nil(arg) {
792        Ok(def)
793    } else {
794        check_integer(state, arg)
795    }
796}
797
798// ── Buffer manipulation ────────────────────────────────────────────────────────
799
800impl LuaBuffer {
801    /// Create a new empty buffer.
802    ///
803    /// Rust uses `Vec::new()` which starts at zero capacity; capacity is managed by Vec.
804    pub fn new() -> Self {
805        LuaBuffer { data: Vec::new() }
806    }
807
808    /// Returns the number of bytes currently in the buffer.
809    pub fn len(&self) -> usize {
810        self.data.len()
811    }
812}
813
814impl Default for LuaBuffer {
815    fn default() -> Self {
816        LuaBuffer::new()
817    }
818}
819
820/// Initialize `buf` and associate it with `state`.
821/// Pushes a placeholder light-userdata onto `state` to anchor the buffer in C.
822/// In Rust the Vec is self-contained; we still push a placeholder for stack-slot
823/// compatibility with code that later calls `add_value` / `push_result`.
824///
825pub fn buf_init(state: &mut LuaState, buf: &mut LuaBuffer) {
826    *buf = LuaBuffer::new();
827    let _ = state.push(LuaValue::Nil);
828}
829
830/// Initialize `buf`, reserve `sz` bytes, and return the writable region.
831///
832pub fn buf_init_size(state: &mut LuaState, buf: &mut LuaBuffer, sz: usize) -> Result<(), LuaError> {
833    buf_init(state, buf);
834    buf.data.reserve(sz);
835    Ok(())
836}
837
838/// Compute a new buffer capacity that accommodates `sz` more bytes,
839/// growing by ×1.5 or more.
840///
841fn new_buff_size(buf: &LuaBuffer, sz: usize) -> Result<usize, LuaError> {
842    if usize::MAX - sz < buf.len() {
843        return Err(LuaError::runtime(format_args!("buffer too large")));
844    }
845    let newsize = (buf.data.capacity() / 2) * 3; // ×1.5
846    if newsize < buf.len() + sz {
847        Ok(buf.len() + sz)
848    } else {
849        Ok(newsize)
850    }
851}
852
853/// Ensure at least `sz` free bytes are available in `buf`.
854///
855pub fn prep_buff_size(buf: &mut LuaBuffer, sz: usize) -> Result<(), LuaError> {
856    if buf.data.capacity() - buf.data.len() < sz {
857        let newcap = new_buff_size(buf, sz)?;
858        buf.data.reserve(newcap - buf.data.len());
859    }
860    Ok(())
861}
862
863/// Append `s` to `buf`.
864///
865pub fn add_lstring(buf: &mut LuaBuffer, s: &[u8]) {
866    if !s.is_empty() {
867        buf.data.extend_from_slice(s);
868    }
869}
870
871/// Append a single byte to `buf`.
872///
873pub fn add_char(buf: &mut LuaBuffer, c: u8) {
874    buf.data.push(c);
875}
876
877/// Append `sz` to the length counter (used after writing directly into the buffer).
878///
879/// Currently a no-op: `Vec`'s length is implicit, so this only matters if a
880/// caller writes directly into the buffer's spare capacity, which is not yet
881/// supported (would need an `unsafe` `set_len` or a buffer redesign).
882pub fn add_size(_buf: &mut LuaBuffer, sz: usize) {
883    let _ = sz;
884}
885
886/// Pop the string at top of `state`'s stack and append it to `buf`.
887///
888pub fn add_value(state: &mut LuaState, buf: &mut LuaBuffer) -> Result<(), LuaError> {
889    if let Some(bytes) = state.peek_bytes(-1) {
890        let owned = bytes.to_vec();
891        add_lstring(buf, &owned);
892    }
893    state.pop_n(1);
894    Ok(())
895}
896
897/// Push the buffer contents as a Lua string onto `state`'s stack.
898///
899pub fn push_result(state: &mut LuaState, buf: &mut LuaBuffer) -> Result<(), LuaError> {
900    state.push_bytes(&buf.data)?;
901    state.remove(-2)?;
902    Ok(())
903}
904
905/// Add `sz` bytes to the buffer count then call `push_result`.
906///
907pub fn push_result_size(
908    state: &mut LuaState,
909    buf: &mut LuaBuffer,
910    sz: usize,
911) -> Result<(), LuaError> {
912    add_size(buf, sz);
913    push_result(state, buf)
914}
915
916/// Perform global byte-string substitution: replace all occurrences of `pat`
917/// with `repl` in `s`, appending results into `buf`.
918///
919pub fn add_gsub(buf: &mut LuaBuffer, s: &[u8], pat: &[u8], repl: &[u8]) {
920    if pat.is_empty() {
921        add_lstring(buf, s);
922        return;
923    }
924    let mut remaining = s;
925    while let Some(pos) = find_bytes(remaining, pat) {
926        add_lstring(buf, &remaining[..pos]);
927        add_lstring(buf, repl);
928        remaining = &remaining[pos + pat.len()..];
929    }
930    add_lstring(buf, remaining);
931}
932
933/// Build a string from `s` by replacing `pat` with `repl`, push it on the stack,
934/// and return the bytes of the pushed string.
935///
936pub fn gsub<'a>(
937    state: &'a mut LuaState,
938    s: &[u8],
939    pat: &[u8],
940    repl: &[u8],
941) -> Result<Vec<u8>, LuaError> {
942    let mut b = LuaBuffer::new();
943    buf_init(state, &mut b);
944    add_gsub(&mut b, s, pat, repl);
945    push_result(state, &mut b)?;
946    Ok(state.peek_bytes(-1).unwrap_or_default())
947}
948
949/// Find `needle` in `haystack`, returning the byte offset or `None`.
950///
951/// Internal helper replacing C's `strstr`.
952fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
953    if needle.is_empty() {
954        return Some(0);
955    }
956    haystack.windows(needle.len()).position(|w| w == needle)
957}
958
959// ── Reference system ──────────────────────────────────────────────────────────
960
961/// The `luaL_ref`/`luaL_unref` free-list head index within the reference
962/// table `t` — the slot C-Lua's `lauxlib.c` calls `FREELIST_REF` (5.1) or
963/// `freelist` (5.2+). Version-gated because the value moved twice across the
964/// C line; every arm below is verified against source, not memory (issue
965/// #291):
966///
967/// - **5.1** — `#define FREELIST_REF 0` (`lua.org/source/5.1/lauxlib.c.html`;
968///   not vendored under `reference/` per issue #282's note, fetched from
969///   lua.org for this fix).
970/// - **5.2** — `#define freelist 0` (`lua.org/source/5.2/lauxlib.c.html`).
971/// - **5.3** — `#define freelist 0` (`reference/lua-5.3.6/src/lauxlib.c`).
972///   Index 0 is safe on any table, registry or otherwise — no `LUA_RIDX_*`
973///   predefined key is ever 0.
974/// - **5.4** — moved to `#define freelist (LUA_RIDX_LAST + 1)`
975///   (`reference/lua-5.4.7/src/lauxlib.c`), i.e. `LUA_RIDX_GLOBALS`(2) + 1 =
976///   **3**.
977/// - **5.5** — hardcodes index **1** directly, no named macro
978///   (`reference/lua-5.5.0/src/lauxlib.c`: `lua_rawgeti(L, t, 1)` /
979///   `lua_rawseti(L, t, 1)`), because 5.5 renumbered `LUA_RIDX_MAINTHREAD` to
980///   3 (`lua.h`), freeing slot 1 for this purpose.
981///
982/// This port previously hardcoded **3** (the 5.4 value) for every version.
983/// That was invisible because `lua_ref`/`lua_unref` have zero callers today,
984/// but it was wrong for four of five versions — only 5.4 matched — and on
985/// 5.5 it collided outright with `LUA_RIDX_MAINTHREAD`
986/// (`set_versioned_registry_slots`, `lua-vm/src/state.rs`, from #275). Fixed
987/// here as cheap insurance ahead of the mechanism ever getting a caller.
988fn freelist_ref(version: lua_types::LuaVersion) -> i64 {
989    match version {
990        lua_types::LuaVersion::V51 => 0,
991        lua_types::LuaVersion::V52 => 0,
992        lua_types::LuaVersion::V53 => 0,
993        lua_types::LuaVersion::V54 => 3,
994        lua_types::LuaVersion::V55 => 1,
995        _ => 3,
996    }
997}
998
999/// Store the value at the top of the stack in table `t` and return a unique
1000/// integer reference. If the value is `nil`, returns `LUA_REFNIL` without
1001/// modifying the table.
1002///
1003/// The free-list head at [`freelist_ref`]'s index counts as "initialized"
1004/// once it holds a `Number`, matching 5.5's explicit `LUA_TNUMBER` check
1005/// (`reference/lua-5.5.0/src/lauxlib.c`) rather than 5.1-5.4's plain `nil`
1006/// check: 5.5's `set_versioned_registry_slots` pre-seeds `registry[1]` with
1007/// `false` (not `nil`) as the free-list's "uninitialized" sentinel, and a
1008/// `nil`-only check would misread that sentinel as already-initialized. The
1009/// two checks agree on 5.1-5.4 (the head there is always either `nil` or a
1010/// `Number`), so a single version-free comparison covers every version.
1011pub fn lua_ref(state: &mut LuaState, t: i32) -> Result<i32, LuaError> {
1012    if state.type_at(-1) == LuaType::Nil {
1013        state.pop_n(1);
1014        return Ok(LUA_REFNIL);
1015    }
1016    let t = state.abs_index(t);
1017    let freelist = freelist_ref(state.global().lua_version);
1018    let ref_val: i32;
1019    if state.raw_get_i(t, freelist)? != LuaType::Number {
1020        ref_val = 0;
1021        state.push(LuaValue::Int(0));
1022        state.raw_set_i(t, freelist)?;
1023    } else {
1024        ref_val = state.to_integer_x(-1).unwrap_or(0) as i32;
1025    }
1026    state.pop_n(1);
1027    let next_ref: i32;
1028    if ref_val != 0 {
1029        state.raw_get_i(t, ref_val as i64)?;
1030        state.raw_set_i(t, freelist)?;
1031        next_ref = ref_val;
1032    } else {
1033        next_ref = (state.raw_len(t) as i32) + 1;
1034    }
1035    state.raw_set_i(t, next_ref as i64)?;
1036    Ok(next_ref)
1037}
1038
1039/// Release reference `ref` from table `t`, adding it to the free list.
1040///
1041pub fn lua_unref(state: &mut LuaState, t: i32, r: i32) -> Result<(), LuaError> {
1042    if r >= 0 {
1043        let t = state.abs_index(t);
1044        let freelist = freelist_ref(state.global().lua_version);
1045        state.raw_get_i(t, freelist)?;
1046        debug_assert!(state.type_at(-1) == LuaType::Number);
1047        state.raw_set_i(t, r as i64)?;
1048        state.push(LuaValue::Int(r as i64));
1049        state.raw_set_i(t, freelist)?;
1050    }
1051    Ok(())
1052}
1053
1054// ── Load functions ─────────────────────────────────────────────────────────────
1055
1056/// Internal chunk reader that returns a single buffer slice then signals EOF.
1057///
1058fn make_string_reader(data: Vec<u8>) -> impl FnMut() -> Option<Vec<u8>> {
1059    let mut remaining = Some(data);
1060    move || remaining.take()
1061}
1062
1063/// Strip an optional UTF-8 BOM (EF BB BF) and any `#`-prefixed first line.
1064///
1065/// The embedder-installed file loader hook supplies raw bytes; this strips
1066/// the BOM and lets `lua_vm::api::load` dispatch text vs. binary by the
1067/// first byte. (C's `luaL_loadfilex` instead reads byte-by-byte and lazily
1068/// reopens in binary mode, because its text mode does newline translation —
1069/// not a concern here since the host loader always returns raw bytes.)
1070fn skip_bom_and_shebang(buf: &[u8]) -> Vec<u8> {
1071    let s = if buf.starts_with(b"\xEF\xBB\xBF") {
1072        &buf[3..]
1073    } else {
1074        buf
1075    };
1076    if s.first() == Some(&b'#') {
1077        let nl = s
1078            .iter()
1079            .position(|&b| b == b'\n')
1080            .map(|p| p + 1)
1081            .unwrap_or(s.len());
1082        let rest = &s[nl..];
1083        if rest.first() == Some(&0x1B) {
1084            rest.to_vec()
1085        } else {
1086            let mut out = Vec::with_capacity(rest.len() + 1);
1087            out.push(b'\n');
1088            out.extend_from_slice(rest);
1089            out
1090        }
1091    } else {
1092        s.to_vec()
1093    }
1094}
1095
1096/// Load a file as a Lua chunk. Returns `LUA_OK` on success or an error code.
1097///
1098/// PORTING.md §1 bans `std::fs` outside `lua-cli`, so this goes through the
1099/// embedder-installed file loader hook instead. A load failure pushes an
1100/// error string and returns a non-zero status, which `load_aux` converts to
1101/// `(nil, errmsg)`.
1102pub fn load_filex(
1103    state: &mut LuaState,
1104    filename: Option<&[u8]>,
1105    mode: Option<&[u8]>,
1106) -> Result<i32, LuaError> {
1107    let _ = mode;
1108    let fname = match filename {
1109        Some(f) => f,
1110        None => {
1111            state.push_string(b"cannot read stdin: no filename given")?;
1112            return Ok(LUA_ERRFILE);
1113        }
1114    };
1115    let raw = match state.global().file_loader_hook {
1116        Some(load_fn) => load_fn(fname),
1117        None => Err(LuaError::runtime(format_args!(
1118            "no file_loader_hook registered"
1119        ))),
1120    };
1121    let raw = match raw {
1122        Ok(bytes) => bytes,
1123        Err(e) => {
1124            let detail = match e.message_bytes() {
1125                Some(b) => String::from_utf8_lossy(b).into_owned(),
1126                None => format!("{:?}", &e),
1127            };
1128            state.push_fstring(format_args!("cannot open {}: {}", BStr(fname), detail))?;
1129            return Ok(LUA_ERRFILE);
1130        }
1131    };
1132    let payload = skip_bom_and_shebang(&raw);
1133    let mut once = Some(payload);
1134    let boxed: lua_vm::zio::ChunkReader = Box::new(move |_state| Ok(once.take()));
1135    let mut chunkname = b"@".to_vec();
1136    chunkname.extend_from_slice(fname);
1137    let status = lua_vm::api::load(state, boxed, Some(&chunkname), mode)?;
1138    Ok(if status == LuaStatus::Ok {
1139        0
1140    } else {
1141        status as i32
1142    })
1143}
1144
1145/// Load a buffer as a Lua chunk.
1146///
1147pub fn load_bufferx(
1148    state: &mut LuaState,
1149    buff: &[u8],
1150    name: &[u8],
1151    mode: Option<&[u8]>,
1152) -> Result<i32, LuaError> {
1153    let _reader = make_string_reader(buff.to_vec());
1154    let ok = state.load(buff, name, mode)?;
1155    Ok(if ok { 0 } else { 1 })
1156}
1157
1158/// Load a buffer as a Lua chunk (no mode argument).
1159///
1160pub fn load_buffer(state: &mut LuaState, buff: &[u8], name: &[u8]) -> Result<i32, LuaError> {
1161    load_bufferx(state, buff, name, None)
1162}
1163
1164/// Load a NUL-terminated byte-string as a Lua chunk.
1165///
1166pub fn load_string(state: &mut LuaState, s: &[u8]) -> Result<i32, LuaError> {
1167    load_buffer(state, s, s)
1168}
1169
1170// ── Meta-field and misc helpers ───────────────────────────────────────────────
1171
1172/// Push the metafield `event` of `obj` onto the stack and return its type.
1173/// If there is no metafield, nothing is pushed and `LuaType::Nil` is returned.
1174///
1175pub fn get_metafield(state: &mut LuaState, obj: i32, event: &[u8]) -> Result<LuaType, LuaError> {
1176    if !state.get_metatable(obj)? {
1177        return Ok(LuaType::Nil);
1178    }
1179    state.push_bytes(event)?;
1180    let tt = state.raw_get(-2)?;
1181    if tt == LuaType::Nil {
1182        state.pop_n(2);
1183    } else {
1184        state.remove(-2)?;
1185    }
1186    Ok(tt)
1187}
1188
1189/// Call the metafield `event` of `obj` with `obj` as argument, pushing one result.
1190/// Returns `true` if the meta-method existed and was called.
1191///
1192pub fn call_meta(state: &mut LuaState, obj: i32, event: &[u8]) -> Result<bool, LuaError> {
1193    let obj = state.abs_index(obj);
1194    if get_metafield(state, obj, event)? == LuaType::Nil {
1195        return Ok(false);
1196    }
1197    state.push_value(obj)?;
1198    state.call(1, 1)?;
1199    Ok(true)
1200}
1201
1202/// Return the length of the value at `idx` as a `i64`, raising an error if
1203/// the length is not an integer.
1204///
1205pub fn lua_len(state: &mut LuaState, idx: i32) -> Result<i64, LuaError> {
1206    state.len_op(idx)?;
1207    let l = match state.to_integer_x(-1) {
1208        Some(n) => n,
1209        None => {
1210            return Err(LuaError::runtime(format_args!(
1211                "object length is not an integer"
1212            )));
1213        }
1214    };
1215    state.pop_n(1);
1216    Ok(l)
1217}
1218
1219/// Convert the value at `idx` to its display byte-string, push the result onto
1220/// the stack, and return its bytes. Mirrors C's `luaL_tolstring`: the
1221/// `__tostring` metamethod is honored (and, from 5.3, the `__name` metafield
1222/// names the type for reference values).
1223///
1224/// This is a thin wrapper over [`lua_vm::state::LuaState::to_display_string`],
1225/// the single implementation of the conversion, so tables/functions/userdata/
1226/// threads render as `"kind: 0x<addr>"` with a real per-value identity — two
1227/// distinct values print distinct addresses, matching the reference.
1228pub fn to_lua_string(state: &mut LuaState, idx: i32) -> Result<Vec<u8>, LuaError> {
1229    state.to_display_string(idx)
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 ─────────────────────────────────────────────────────────
1426
1427#[cfg(test)]
1428mod tests {
1429    use super::*;
1430    use lua_types::LuaVersion;
1431
1432    /// The `freelist_ref` mapping (issue #291), pinned per-version against
1433    /// each C release's `lauxlib.c` (see `freelist_ref`'s doc comment for the
1434    /// exact source citations): 0 on 5.1/5.2/5.3, 3 on 5.4, 1 on 5.5. This is
1435    /// a unit test on the mapping function only — `lua_ref`/`lua_unref`
1436    /// themselves have zero callers in this codebase, so there is nothing to
1437    /// drive an end-to-end probe with.
1438    #[test]
1439    fn freelist_ref_is_version_exact() {
1440        assert_eq!(freelist_ref(LuaVersion::V51), 0);
1441        assert_eq!(freelist_ref(LuaVersion::V52), 0);
1442        assert_eq!(freelist_ref(LuaVersion::V53), 0);
1443        assert_eq!(freelist_ref(LuaVersion::V54), 3);
1444        assert_eq!(freelist_ref(LuaVersion::V55), 1);
1445    }
1446
1447    /// 5.5's free-list index must not collide with `LUA_RIDX_MAINTHREAD`,
1448    /// which `set_versioned_registry_slots` (`lua-vm/src/state.rs`, #275)
1449    /// places at registry index 3 on 5.5. This is the concrete bug #291
1450    /// reported: the port previously hardcoded `FREELIST_REF = 3` for every
1451    /// version, which collided outright on 5.5.
1452    #[test]
1453    fn freelist_ref_does_not_collide_with_5_5_mainthread_slot() {
1454        const V55_MAINTHREAD_REGISTRY_SLOT: i64 = 3;
1455        assert_ne!(freelist_ref(LuaVersion::V55), V55_MAINTHREAD_REGISTRY_SLOT);
1456    }
1457}