Skip to main content

lua_stdlib/
auxlib.rs

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