Skip to main content

lua_stdlib/
base.rs

1//! Base library — Lua's built-in functions (`print`, `type`, `pairs`, `pcall`, …).
2//!
3//! Translated from: `reference/lua-5.4.7/src/lbaselib.c` (549 lines, 32 functions)
4//! Target crate: `lua-stdlib`
5
6use lua_types::{
7    closure::LuaClosure,
8    error::LuaError,
9    value::LuaValue,
10    LuaType,
11    LuaStatus,
12};
13use crate::state_stub::{LuaState, LuaStateStubExt as _};
14
15// ── Module-level constants ────────────────────────────────────────────────────
16
17/// ASCII whitespace characters used by `b_str2int` for strspn-style skipping.
18const SPACECHARS: &[u8] = b" \x0c\n\r\t\x0b";
19
20/// Reserved stack slot used by `generic_reader` to anchor the current chunk
21/// string so it is not collected while `lua_load` is running.
22const RESERVED_SLOT: i32 = 5;
23
24/// Name of the global environment table stored as a global itself.
25const LUA_GNAME: &[u8] = b"_G";
26
27/// Sentinel indicating "all return values" for call/pcall helpers.
28const LUA_MULTRET: i32 = -1;
29
30// ── GC operation codes ────────────────────────────────────────────────────────
31
32/// Identifies a GC control operation passed to the `collectgarbage` built-in.
33/// Mirrors the `LUA_GC*` integer constants from `lua.h`.
34/// TODO(port): define as a proper type in lua-types once the GC API is finalised.
35#[repr(i32)]
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37enum GcOp {
38    Stop       = 0,
39    Restart    = 1,
40    Collect    = 2,
41    Count      = 3,
42    #[expect(dead_code, reason = "ported stdlib helper; not yet wired into the runtime")]
43    CountB     = 4,
44    Step       = 5,
45    SetPause   = 6,
46    SetStepMul = 7,
47    IsRunning  = 9,
48    Gen        = 10,
49    Inc        = 11,
50    Param      = 12,
51}
52
53// ── LuaState forward declaration ─────────────────────────────────────────────
54
55// LuaState is provided by crate::state_stub.
56
57// ── Type alias for standard Lua-callable functions ────────────────────────────
58
59/// Rust equivalent of `lua_CFunction`: a bare function that receives the
60/// interpreter state and returns a count of pushed results.
61pub(crate) type LuaLibFn = fn(&mut LuaState) -> Result<usize, LuaError>;
62
63// ── Helper: push_mode ─────────────────────────────────────────────────────────
64
65/// Push the GC mode string ("incremental" or "generational") onto the stack,
66/// or push `nil` (fail) when `oldmode == -1` (invalid call inside a finalizer).
67///
68fn push_mode(state: &mut LuaState, oldmode: i32) -> Result<usize, LuaError> {
69    if oldmode == -1 {
70        state.push(LuaValue::Nil);
71    } else {
72        let s: &[u8] = if oldmode == GcOp::Inc as i32 {
73            b"incremental"
74        } else {
75            b"generational"
76        };
77        state.push_string(s)?;
78    }
79    Ok(1)
80}
81
82// ── Helper: finish_pcall ──────────────────────────────────────────────────────
83
84/// Shared result-adjustment logic for `pcall` and `xpcall`.
85///
86/// On success: returns the count of values already on the stack minus `extra`
87/// skipped sentinel values.  On failure: replaces whatever is on the stack
88/// with `[false, error_message]` and returns 2.
89///
90fn finish_pcall(state: &mut LuaState, ok: bool, extra: i32) -> Result<usize, LuaError> {
91    if !ok {
92        state.push(LuaValue::Bool(false));
93        state.push_copy(-2)?;
94        return Ok(2);
95    }
96    Ok((state.top() as i32 - extra) as usize)
97}
98
99// ── Helper: b_str2int ─────────────────────────────────────────────────────────
100
101/// Parse an integer in an arbitrary base from the byte slice `s`.
102///
103/// Returns `Some((consumed, value))` on success, where `consumed` is the number
104/// of bytes from the start of `s` that were processed (leading and trailing
105/// ASCII whitespace included).  Returns `None` when the slice contains no valid
106/// numeral in `base`.
107///
108/// The caller checks `consumed == s.len()` to verify the whole string was used.
109///
110fn b_str2int(s: &[u8], base: u32) -> Option<(usize, i64)> {
111    let mut pos = 0usize;
112    while pos < s.len() && SPACECHARS.contains(&s[pos]) {
113        pos += 1;
114    }
115    let neg = if pos < s.len() && s[pos] == b'-' {
116        pos += 1;
117        true
118    } else {
119        if pos < s.len() && s[pos] == b'+' {
120            pos += 1;
121        }
122        false
123    };
124    if pos >= s.len() || !s[pos].is_ascii_alphanumeric() {
125        return None;
126    }
127    let mut n: u64 = 0u64;
128    loop {
129        let byte = s[pos];
130        let digit = if byte.is_ascii_digit() {
131            (byte - b'0') as u32
132        } else {
133            (byte.to_ascii_uppercase() - b'A') as u32 + 10
134        };
135        if digit >= base {
136            return None;
137        }
138        n = n.wrapping_mul(base as u64).wrapping_add(digit as u64);
139        pos += 1;
140        if pos >= s.len() || !s[pos].is_ascii_alphanumeric() {
141            break;
142        }
143    }
144    while pos < s.len() && SPACECHARS.contains(&s[pos]) {
145        pos += 1;
146    }
147    let value: i64 = if neg {
148        0u64.wrapping_sub(n) as i64
149    } else {
150        n as i64
151    };
152    Some((pos, value))
153}
154
155// ── Helper: load_aux ──────────────────────────────────────────────────────────
156
157/// Shared post-load logic for `load` and `loadfile`.
158///
159/// On success (status_ok == true): optionally installs an environment upvalue,
160/// then returns 1 (the chunk function is on the stack).
161/// On failure: pushes nil then moves it before the error message, returns 2.
162///
163fn load_aux(state: &mut LuaState, status_ok: bool, envidx: i32) -> Result<usize, LuaError> {
164    if status_ok {
165        if envidx != 0 {
166            state.push_copy(envidx)?;
167            if state.set_upvalue(-2, 1)?.is_none() {
168                state.pop_n(1);
169            }
170        }
171        Ok(1)
172    } else {
173        state.push(LuaValue::Nil);
174        state.insert(-2)?;
175        Ok(2)
176    }
177}
178
179// ── print ─────────────────────────────────────────────────────────────────────
180
181/// Converts each argument to a string, separates them with tabs, writes them to
182/// standard output, and finishes with a newline.
183///
184/// The conversion mechanism is a genuine cross-version split:
185///
186/// - Lua 5.1/5.2/5.3 `luaB_print` fetch the **global** `tostring` and *call* it
187///   on each argument. Redefining global `tostring` therefore changes `print`,
188///   a `nil` global makes `print` raise `attempt to call a nil value`, and a
189///   result that is neither a string nor a coercible number raises
190///   `'tostring' must return a string to 'print'`.
191/// - Lua 5.4/5.5 `luaB_print` use `luaL_tolstring` directly: it honors the
192///   `__tostring` / `__name` metafields but ignores the global `tostring`.
193///
194pub(crate) fn print_fn(state: &mut LuaState) -> Result<usize, LuaError> {
195    let calls_global_tostring = matches!(
196        state.global().lua_version,
197        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
198    );
199    if calls_global_tostring {
200        return print_via_global_tostring(state);
201    }
202    let n = state.top();
203    for i in 1..=n {
204        // luaL_tolstring converts via tostring() metamethod, pushes result,
205        // returns a pointer. In Rust we get a GcRef and use its bytes.
206        let display_ref = state.to_display_string(i)?;
207        if i > 1 {
208            state.write_output(b"\t")?;
209        }
210        let bytes = display_ref.clone();
211        state.write_output(&bytes)?;
212        state.pop_n(1);
213    }
214    state.write_output(b"\n")?;
215    Ok(0)
216}
217
218/// Faithful port of the Lua 5.1/5.2/5.3 `luaB_print`: fetch the global
219/// `tostring` once, then call it on each argument.
220///
221fn print_via_global_tostring(state: &mut LuaState) -> Result<usize, LuaError> {
222    let n = state.top();
223    lua_vm::api::get_global(state, b"tostring")?;
224    for i in 1..=n {
225        state.push_copy(-1)?;
226        state.push_copy(i)?;
227        state.call(1, 1)?;
228        // lua_tolstring returns NULL for anything that is neither a string nor a
229        // coercible number; the reference raises in that case.
230        if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
231            return Err(state.where_error(1, b"'tostring' must return a string to 'print'"));
232        }
233        let bytes = state
234            .to_lua_string_bytes(-1)
235            .expect("string/number coerces to bytes");
236        if i > 1 {
237            state.write_output(b"\t")?;
238        }
239        state.write_output(&bytes)?;
240        state.pop_n(1);
241    }
242    state.write_output(b"\n")?;
243    Ok(0)
244}
245
246// ── warn ──────────────────────────────────────────────────────────────────────
247
248/// Validates that every argument is a string, then forwards them as a
249/// multi-part warning message via the state's warning hook.
250///
251pub(crate) fn warn_fn(state: &mut LuaState) -> Result<usize, LuaError> {
252    let n = state.top();
253    state.check_arg_string(1)?;
254    for i in 2..=n {
255        state.check_arg_string(i)?;
256    }
257    for i in 1..n {
258        // Clone bytes before further mutation to avoid borrow conflict.
259        // PORTING.md §8: "No &LuaValue across a stack-mutating call."
260        let s: Vec<u8> = state
261            .to_lua_string_bytes(i)
262            .map(|b| b.to_vec())
263            .unwrap_or_default();
264        // continue = true (1) — more parts follow
265        state.warning(&s, true)?;
266    }
267    let s: Vec<u8> = state
268        .to_lua_string_bytes(n)
269        .map(|b| b.to_vec())
270        .unwrap_or_default();
271    state.warning(&s, false)?;
272    Ok(0)
273}
274
275// ── tonumber ──────────────────────────────────────────────────────────────────
276
277/// Converts a value to a number, optionally in a given numeric base (2–36).
278///
279pub(crate) fn tonumber_fn(state: &mut LuaState) -> Result<usize, LuaError> {
280    if matches!(state.type_at(2), LuaType::None | LuaType::Nil) {
281        if state.type_at(1) == LuaType::Number {
282            lua_vm::api::set_top(state, 1)?;
283            return Ok(1);
284        }
285        // lua_stringtonumber returns bytes consumed including the NUL terminator,
286        // so success iff consumed == string_length + 1.
287        if let Some(len) = state.to_lua_string_len(1) {
288            if let Some(consumed) = state.string_to_number(1) {
289                if consumed == len + 1 {
290                    return Ok(1);
291                }
292            }
293        }
294        state.check_arg_any(1)?;
295    } else {
296        let base = state.check_arg_integer(2)?;
297        state.check_arg_type(1, LuaType::String)?;
298        // Clone before further state ops (PORTING.md §8).
299        let bytes: Vec<u8> = state
300            .to_lua_string_bytes(1)
301            .map(|b| b.to_vec())
302            .unwrap_or_default();
303        if !(2..=36).contains(&base) {
304            return Err(lua_vm::debug::arg_error_impl(state, 2, b"base out of range"));
305        }
306        if let Some((consumed, n)) = b_str2int(&bytes, base as u32) {
307            if consumed == bytes.len() {
308                state.push(LuaValue::Int(n));
309                return Ok(1);
310            }
311        }
312    }
313    state.push(LuaValue::Nil);
314    Ok(1)
315}
316
317// ── error ─────────────────────────────────────────────────────────────────────
318
319/// Raises the value at stack[1] as a Lua error, optionally prepending
320/// source-location information for string errors when `level > 0`.
321///
322pub(crate) fn error_fn(state: &mut LuaState) -> Result<usize, LuaError> {
323    let level = state.opt_arg_integer(2, 1)? as i32;
324    lua_vm::api::set_top(state, 1)?;
325    if state.type_at(1) == LuaType::String && level > 0 {
326        state.push_where(level)?;
327        state.push_copy(1)?;
328        state.concat(2)?;
329    }
330    Err(LuaError::from_value(state.pop()))
331}
332
333// ── getmetatable ──────────────────────────────────────────────────────────────
334
335/// Returns the metatable of the first argument, or the `__metatable` field of
336/// the metatable if that field exists (protecting the raw metatable).
337///
338pub(crate) fn getmetatable_fn(state: &mut LuaState) -> Result<usize, LuaError> {
339    state.check_arg_any(1)?;
340    if !state.get_metatable(1)? {
341        state.push(LuaValue::Nil);
342        return Ok(1);
343    }
344    // Returns LuaType::Nil if metatable has no __metatable; otherwise pushes it.
345    state.get_metafield(1, b"__metatable")?;
346    Ok(1)
347}
348
349// ── setmetatable ──────────────────────────────────────────────────────────────
350
351/// Sets the metatable of the table at argument 1 to the value at argument 2
352/// (nil clears it).  Raises an error if the current metatable is protected via
353/// `__metatable`.
354///
355pub(crate) fn setmetatable_fn(state: &mut LuaState) -> Result<usize, LuaError> {
356    let t = state.type_at(2);
357    state.check_arg_type(1, LuaType::Table)?;
358    if !(t == LuaType::Nil || t == LuaType::Table) {
359        let got = state.value_at(2);
360        return Err(LuaError::type_arg_error(2, "nil or table", &got));
361    }
362    if state.get_metafield(1, b"__metatable")? != LuaType::Nil {
363        return Err(LuaError::runtime(format_args!(
364            "cannot change a protected metatable"
365        )));
366    }
367    lua_vm::api::set_top(state, 2)?;
368    state.set_metatable(1)?;
369    Ok(1)
370}
371
372// ── rawequal ──────────────────────────────────────────────────────────────────
373
374/// Raw equality check (no metamethods).
375///
376pub(crate) fn rawequal_fn(state: &mut LuaState) -> Result<usize, LuaError> {
377    state.check_arg_any(1)?;
378    state.check_arg_any(2)?;
379    let eq = state.raw_equal(1, 2)?;
380    state.push(LuaValue::Bool(eq));
381    Ok(1)
382}
383
384// ── rawlen ────────────────────────────────────────────────────────────────────
385
386/// Raw length (#) without metamethods; accepts tables and strings only.
387///
388pub(crate) fn rawlen_fn(state: &mut LuaState) -> Result<usize, LuaError> {
389    let t = state.type_at(1);
390    if !(t == LuaType::Table || t == LuaType::String) {
391        let got = state.value_at(1);
392        return Err(LuaError::type_arg_error(1, "table or string", &got));
393    }
394    let len = state.raw_len(1);
395    state.push(LuaValue::Int(len));
396    Ok(1)
397}
398
399// ── rawget ────────────────────────────────────────────────────────────────────
400
401/// Raw table read (no metamethods).
402///
403pub(crate) fn rawget_fn(state: &mut LuaState) -> Result<usize, LuaError> {
404    state.check_arg_type(1, LuaType::Table)?;
405    state.check_arg_any(2)?;
406    lua_vm::api::set_top(state, 2)?;
407    state.raw_get(1)?;
408    Ok(1)
409}
410
411// ── rawset ────────────────────────────────────────────────────────────────────
412
413/// Raw table write (no metamethods).
414///
415pub(crate) fn rawset_fn(state: &mut LuaState) -> Result<usize, LuaError> {
416    state.check_arg_type(1, LuaType::Table)?;
417    state.check_arg_any(2)?;
418    state.check_arg_any(3)?;
419    lua_vm::api::set_top(state, 3)?;
420    state.raw_set(1)?;
421    Ok(1)
422}
423
424// ── collectgarbage ────────────────────────────────────────────────────────────
425
426/// Expose GC control to Lua scripts.  The first argument selects the operation;
427/// subsequent arguments are operation-specific parameters.
428///
429///
430/// PORT NOTE: C's `checkvalres(x)` macro breaks out of the `switch` to the
431/// trailing `luaL_pushfail` when `x == -1` (called inside a finalizer).
432/// In Rust we model this with an explicit early-return to the pushfail path
433/// using a boolean flag, avoiding labeled blocks.
434pub(crate) fn collectgarbage_fn(state: &mut LuaState) -> Result<usize, LuaError> {
435    // The option set is version-gated. 5.4/5.3 expose `setpause`/`setstepmul`;
436    // 5.5 removed both and added `param` (lbaselib.c). The version that owns
437    // the running state decides which list/mapping applies.
438    let version = state.global().lua_version;
439    let is_v55 = version == lua_types::LuaVersion::V55;
440    // Lua 5.1's `collectgarbage` accepts only `collect/stop/restart/count/step/
441    // setpause/setstepmul`; the 5.2 `isrunning`/`generational`, the 5.4
442    // `incremental`, and the 5.5 `param` must be rejected with `invalid option`.
443    // Verified against lua5.1.5: `collectgarbage("isrunning")` errors. (5.2 DOES
444    // accept `isrunning`/`generational`, so it stays on OPTS_54.) See
445    // specs/followup/5.1-roster-syntax.md §1.
446    static OPTS_51: &[&[u8]] = &[
447        b"stop", b"restart", b"collect",
448        b"count", b"step", b"setpause", b"setstepmul",
449    ];
450    static OPTS_NUM_51: &[GcOp] = &[
451        GcOp::Stop, GcOp::Restart, GcOp::Collect,
452        GcOp::Count, GcOp::Step, GcOp::SetPause, GcOp::SetStepMul,
453    ];
454    static OPTS_54: &[&[u8]] = &[
455        b"stop", b"restart", b"collect",
456        b"count", b"step", b"setpause", b"setstepmul",
457        b"isrunning", b"generational", b"incremental",
458    ];
459    static OPTS_NUM_54: &[GcOp] = &[
460        GcOp::Stop, GcOp::Restart, GcOp::Collect,
461        GcOp::Count, GcOp::Step, GcOp::SetPause, GcOp::SetStepMul,
462        GcOp::IsRunning, GcOp::Gen, GcOp::Inc,
463    ];
464    static OPTS_55: &[&[u8]] = &[
465        b"stop", b"restart", b"collect",
466        b"count", b"step", b"isrunning",
467        b"generational", b"incremental", b"param",
468    ];
469    static OPTS_NUM_55: &[GcOp] = &[
470        GcOp::Stop, GcOp::Restart, GcOp::Collect,
471        GcOp::Count, GcOp::Step, GcOp::IsRunning,
472        GcOp::Gen, GcOp::Inc, GcOp::Param,
473    ];
474    let (opts, opts_num): (&[&[u8]], &[GcOp]) = if is_v55 {
475        (OPTS_55, OPTS_NUM_55)
476    } else if matches!(version, lua_types::LuaVersion::V51) {
477        (OPTS_51, OPTS_NUM_51)
478    } else {
479        (OPTS_54, OPTS_NUM_54)
480    };
481    let idx = state.check_arg_option(1, Some(b"collect"), opts)?;
482    let op = opts_num[idx];
483
484    // Each arm either returns early on success, or evaluates to `false`
485    // (meaning checkvalres fired — fall through to pushfail).
486    let valid: bool = match op {
487        GcOp::Count => {
488            // TODO(port): gc_count / gc_count_b are stubs in Phase A.
489            let k = state.gc_count()?;
490            let b = state.gc_count_b()?;
491            if k == -1 {
492                false
493            } else {
494                state.push(LuaValue::Float(k as f64 + b as f64 / 1024.0));
495                return Ok(1);
496            }
497        }
498        GcOp::Step => {
499            let step = state.opt_arg_integer(2, 0)? as i32;
500            // TODO(port): gc_step is a stub in Phase A.
501            let res = state.gc_step(step)?;
502            if res == -1 {
503                false
504            } else {
505                state.push(LuaValue::Bool(res != 0));
506                return Ok(1);
507            }
508        }
509        GcOp::SetPause | GcOp::SetStepMul => {
510            let p = state.opt_arg_integer(2, 0)? as i32;
511            // TODO(port): gc_set_param is a stub in Phase A.
512            let previous = state.gc_set_param(op as i32, p)?;
513            if previous == -1 {
514                false
515            } else {
516                state.push(LuaValue::Int(previous as i64));
517                return Ok(1);
518            }
519        }
520        GcOp::IsRunning => {
521            let res = state.gc_is_running()?;
522            state.push(LuaValue::Bool(res));
523            return Ok(1);
524        }
525        GcOp::Gen => {
526            let minormul = state.opt_arg_integer(2, 0)? as i32;
527            let majormul = state.opt_arg_integer(3, 0)? as i32;
528            // TODO(port): gc_gen is a stub in Phase A.
529            let oldmode = state.gc_gen(minormul, majormul)?;
530            return push_mode(state, oldmode);
531        }
532        GcOp::Inc => {
533            let pause    = state.opt_arg_integer(2, 0)? as i32;
534            let stepmul  = state.opt_arg_integer(3, 0)? as i32;
535            let stepsize = state.opt_arg_integer(4, 0)? as i32;
536            // TODO(port): gc_inc is a stub in Phase A.
537            let oldmode = state.gc_inc(pause, stepmul, stepsize)?;
538            return push_mode(state, oldmode);
539        }
540        GcOp::Param => {
541            // 5.5 collectgarbage("param", name [, value]): read or write a GC
542            // parameter, always returning the OLD integer value. arg2 selects
543            // the param; arg3 (default -1 = read-only) is the new value.
544            static PARAMS: &[&[u8]] = &[
545                b"minormul", b"majorminor", b"minormajor",
546                b"pause", b"stepmul", b"stepsize",
547            ];
548            let pidx = state.check_arg_option(2, None, PARAMS)?;
549            let value = state.opt_arg_integer(3, -1)?;
550            let old = state.gc_param(pidx, value)?;
551            state.push(LuaValue::Int(old));
552            return Ok(1);
553        }
554        _ => {
555            // TODO(port): gc_control_simple is a stub in Phase A.
556            let res = state.gc_control_simple(op as i32)?;
557            if res == -1 {
558                false
559            } else {
560                state.push(LuaValue::Int(res as i64));
561                return Ok(1);
562            }
563        }
564    };
565    debug_assert!(!valid, "valid arms return early; reaching here means checkvalres fired");
566    state.push(LuaValue::Nil);
567    Ok(1)
568}
569
570// ── type ──────────────────────────────────────────────────────────────────────
571
572/// Returns the type name of its argument as a string.
573///
574pub(crate) fn type_fn(state: &mut LuaState) -> Result<usize, LuaError> {
575    let t = state.type_at(1);
576    if t == LuaType::None {
577        return Err(lua_vm::debug::arg_error_impl(state, 1, b"value expected"));
578    }
579    // Clone the bytes before the push to avoid borrow conflict with state.
580    let name: Vec<u8> = state.type_name(t).to_vec();
581    state.push_string(&name)?;
582    Ok(1)
583}
584
585// ── getfenv / setfenv (Lua 5.1 fenv globals) ──────────────────────────────────
586
587/// Truncate a numeric `getfenv`/`setfenv` level toward zero.
588///
589/// 5.1's `luaL_checkint` casts `lua_Number` to a C `int`, truncating toward
590/// zero, so `getfenv(1.9)` is level 1 and `getfenv(-0.5)` is level 0. Under the
591/// float-only V51 model every number arrives as a `Float`; the `Int` arm is a
592/// defensive no-op. A non-number never reaches this helper.
593fn fenv_level(v: &LuaValue) -> i64 {
594    match v {
595        LuaValue::Float(f) => f.trunc() as i64,
596        LuaValue::Int(i) => *i,
597        _ => 0,
598    }
599}
600
601/// Resolve the function value targeted by a `getfenv`/`setfenv` first argument.
602///
603/// Returns the `LuaValue::Function` whose environment is being read or written.
604/// `arg1` is interpreted exactly as Lua 5.1's `getfunc`/`setfunc`
605/// (lbaselib.c): a function value targets that function directly; a number is a
606/// stack *level* (floored toward zero), where level 1 is the function calling
607/// `getfenv`/`setfenv`. Level 0 is handled by the callers (it denotes the
608/// running thread's global table, not a function) and never reaches here.
609///
610/// Errors mirror lua5.1.5:
611/// - negative level → `level must be non-negative`
612/// - level past the stack → `invalid level`
613/// - neither number nor function → `number expected, got <type>`
614fn fenv_getfunc(state: &mut LuaState, level: i64) -> Result<LuaValue, LuaError> {
615    if level < 0 {
616        return Err(lua_vm::debug::arg_error_impl(state, 1, b"level must be non-negative"));
617    }
618    let mut ar = lua_vm::debug::LuaDebug::default();
619    if !lua_vm::debug::get_stack(state, level as i32, &mut ar) {
620        return Err(lua_vm::debug::arg_error_impl(state, 1, b"invalid level"));
621    }
622    let ci_idx = ar
623        .i_ci
624        .ok_or_else(|| lua_vm::debug::arg_error_impl(state, 1, b"invalid level"))?;
625    let func_slot = state.get_ci(ci_idx).func;
626    Ok(state.get_at(func_slot))
627}
628
629/// Index of a Lua closure's `_ENV` upvalue, by upvalue name.
630///
631/// The reused modern parser threads an upvalue literally named `_ENV` and
632/// resolves every free (global) name through it; under V51 that upvalue *is* the
633/// function environment. It is NOT always upvalue 0 — a nested closure that
634/// captures locals places those first, with `_ENV` at a later index — so it must
635/// be located by name, not position. A closure that references no free names has
636/// no `_ENV` upvalue and returns `None`.
637fn fenv_env_upval_index(lcl: &lua_types::gc::GcRef<lua_types::closure::LuaLClosure>) -> Option<usize> {
638    lcl.proto
639        .upvalues
640        .iter()
641        .position(|ud| ud.name.as_ref().map(|s| s.as_bytes()) == Some(b"_ENV"))
642}
643
644/// Read the environment of a resolved function value.
645///
646/// A Lua closure's environment is its `_ENV` upvalue. A C/Rust function (or a
647/// Lua closure that references no globals, hence has no `_ENV` upvalue) is given
648/// the thread global table as its environment — matching the common 5.1 case
649/// and the documented `LUA_ENVIRONINDEX` gap (specs/followup/5.1-fenv.md §4).
650fn fenv_read(state: &LuaState, func: &LuaValue) -> LuaValue {
651    if let LuaValue::Function(LuaClosure::Lua(lcl)) = func {
652        if let Some(idx) = fenv_env_upval_index(lcl) {
653            return state.upvalue_get(lcl, idx);
654        }
655    }
656    state.global().globals.clone()
657}
658
659/// `getfenv([f])` — Lua 5.1 only.
660///
661/// Returns the environment of the function `f` (a function value or a stack
662/// level), or the running function's environment when the argument is absent or
663/// `1`. Level `0` returns the running thread's global table. See
664/// `specs/followup/5.1-fenv.md` §2.
665pub(crate) fn getfenv_fn(state: &mut LuaState) -> Result<usize, LuaError> {
666    let arg1 = state.value_at(1);
667    let func = match &arg1 {
668        LuaValue::Function(_) => arg1.clone(),
669        LuaValue::Nil if state.type_at(1) == LuaType::None => {
670            // No argument => level 1 (the running function).
671            fenv_getfunc(state, 1)?
672        }
673        LuaValue::Float(_) | LuaValue::Int(_) => {
674            let level = fenv_level(&arg1);
675            if level == 0 {
676                let g = state.global().globals.clone();
677                state.push(g);
678                return Ok(1);
679            }
680            fenv_getfunc(state, level)?
681        }
682        other => {
683            let got = state.obj_type_name(other);
684            let msg = format!("number expected, got {}", String::from_utf8_lossy(&got));
685            return Err(lua_vm::debug::arg_error_impl(state, 1, msg.as_bytes()));
686        }
687    };
688    let env = fenv_read(state, &func);
689    state.push(env);
690    Ok(1)
691}
692
693/// `setfenv(f, table)` — Lua 5.1 only.
694///
695/// Sets the environment of the function `f` (a function value or a stack level)
696/// to `table`. `setfenv(0, t)` sets the running thread's global table. Returns
697/// the affected function (or the running thread for level 0). A C/Rust function
698/// (or any non-Lua object) cannot have its environment changed and raises,
699/// matching lua5.1.5. See `specs/followup/5.1-fenv.md` §2.
700pub(crate) fn setfenv_fn(state: &mut LuaState) -> Result<usize, LuaError> {
701    state.check_arg_type(2, LuaType::Table)?;
702    let new_env = state.value_at(2);
703
704    let arg1 = state.value_at(1);
705    let is_level_zero = matches!(&arg1, LuaValue::Int(0))
706        || matches!(&arg1, LuaValue::Float(f) if *f == 0.0);
707    if is_level_zero {
708        // Level 0: replace the running thread's global table and return the
709        // running thread. Subsequently-loaded top-level chunks take this env.
710        state.global_mut().globals = new_env;
711        lua_vm::api::push_thread(state);
712        return Ok(1);
713    }
714
715    let func = match &arg1 {
716        LuaValue::Function(_) => arg1.clone(),
717        LuaValue::Float(_) | LuaValue::Int(_) => {
718            let level = fenv_level(&arg1);
719            fenv_getfunc(state, level)?
720        }
721        other => {
722            let got = state.obj_type_name(other);
723            let msg = format!("number expected, got {}", String::from_utf8_lossy(&got));
724            return Err(lua_vm::debug::arg_error_impl(state, 1, msg.as_bytes()));
725        }
726    };
727
728    match &func {
729        LuaValue::Function(LuaClosure::Lua(lcl)) => {
730            if let Some(idx) = fenv_env_upval_index(lcl) {
731                // Give the closure a PRIVATE environment: replace its `_ENV`
732                // upvalue *cell* with a fresh closed upvalue holding `new_env`.
733                // Mutating the existing cell's value (`upvalue_set`) would alter
734                // every closure sharing that upvalue (e.g. the main chunk's
735                // `_G`), which is wrong — `setfenv(f, e)` must not change the
736                // caller's globals. A new cell isolates `f`.
737                let uv = state.new_upval_closed(new_env);
738                lcl.set_upval(idx, uv);
739                state.gc().obj_barrier(lcl, &uv);
740            }
741            // A Lua closure that references no globals has no `_ENV` upvalue and
742            // nothing reads globals through it, so the set is inert; 5.1 still
743            // accepts it and returns the function. (Gap: a subsequent
744            // `getfenv` on such a closure returns the thread globals rather than
745            // the set table — see specs/followup/5.1-fenv.md §4.)
746        }
747        _ => {
748            // C/Rust functions cannot have their environment changed. 5.1
749            // raises this exact message (via luaL_error, so it carries the
750            // caller's source location) for any object whose env is fixed.
751            return Err(state.where_error(1, b"'setfenv' cannot change environment of given object"));
752        }
753    }
754    state.push(func);
755    Ok(1)
756}
757
758/// Set the environment of the Lua closure `level` frames up the running stack
759/// to `new_env`, the internal equivalent of `setfenv(level, new_env)`.
760///
761/// Used by `module` (5.1 `package` library), which sets its caller's
762/// environment to the module table. A non-Lua function (or a closure with no
763/// `_ENV` upvalue) is left unchanged, matching the inert-set behavior of
764/// `setfenv`. See specs/followup/5.1-fenv.md.
765pub(crate) fn set_func_env_at_level(
766    state: &mut LuaState,
767    level: i64,
768    new_env: LuaValue,
769) -> Result<(), LuaError> {
770    let func = fenv_getfunc(state, level)?;
771    if let LuaValue::Function(LuaClosure::Lua(lcl)) = &func {
772        if let Some(idx) = fenv_env_upval_index(lcl) {
773            let uv = state.new_upval_closed(new_env);
774            lcl.set_upval(idx, uv);
775            state.gc().obj_barrier(lcl, &uv);
776        }
777    }
778    Ok(())
779}
780
781// ── next ──────────────────────────────────────────────────────────────────────
782
783/// Table traversal iterator: given a table and a key, pushes the next key-value
784/// pair.  Pushes nil and returns 1 when the traversal is exhausted.
785///
786pub(crate) fn next_fn(state: &mut LuaState) -> Result<usize, LuaError> {
787    state.check_arg_type(1, LuaType::Table)?;
788    lua_vm::api::set_top(state, 2)?;
789    if state.table_next(1)? {
790        Ok(2)
791    } else {
792        state.push(LuaValue::Nil);
793        Ok(1)
794    }
795}
796
797// ── pairs continuation (coroutine stub) ───────────────────────────────────────
798
799/// Continuation for `pairs` when the `__pairs` metamethod yields.
800/// Re-invoked by `finishCcall` after the yielded `__pairs` resumes.
801///
802fn pairs_cont(_state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
803    Ok(3)
804}
805
806// ── pairs ─────────────────────────────────────────────────────────────────────
807
808/// Returns the `next` function, the table, and nil (or invokes a `__pairs`
809/// metamethod).
810///
811pub(crate) fn pairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
812    state.check_arg_any(1)?;
813    // Lua 5.1 has no `__pairs` metamethod; `pairs(t)` always iterates the raw
814    // table even when a `__pairs` is set (it is silently ignored). `__pairs`
815    // was added in 5.2 and removed again in 5.4, so only consult it off V51.
816    let consult_pairs_tm = !matches!(state.global().lua_version, lua_types::LuaVersion::V51);
817    if !consult_pairs_tm || state.get_metafield(1, b"__pairs")? == LuaType::Nil {
818        state.push_c_function(next_fn)?;
819        state.push_copy(1)?;
820        state.push(LuaValue::Nil);
821    } else {
822        state.push_copy(1)?;
823        state.call_k(1, 3, 0, Some(pairs_cont))?;
824    }
825    Ok(3)
826}
827
828// ── ipairs auxiliary ──────────────────────────────────────────────────────────
829
830/// Iterator step function for `ipairs`: increments the counter and fetches
831/// the next array element.  Returns the index + value, or just the index when
832/// the value is nil (signalling end-of-iteration).
833///
834fn ipairs_aux(state: &mut LuaState) -> Result<usize, LuaError> {
835    let i = state.check_arg_integer(2)?;
836    // luaL_intop(+, a, b) → wrapping integer addition (PORTING.md §9 / macros.tsv `intop`)
837    let i = (i as u64).wrapping_add(1u64) as i64;
838    state.push(LuaValue::Int(i));
839    let t = state.get_i(1, i)?;
840    if t == LuaType::Nil {
841        Ok(1)
842    } else {
843        Ok(2)
844    }
845}
846
847// ── ipairs ────────────────────────────────────────────────────────────────────
848
849/// Returns the `ipairsaux` iterator, the table, and 0 as the initial counter.
850///
851pub(crate) fn ipairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
852    state.check_arg_any(1)?;
853    state.push_c_function(ipairs_aux)?;
854    state.push_copy(1)?;
855    state.push(LuaValue::Int(0));
856    Ok(3)
857}
858
859// ── loadfile ──────────────────────────────────────────────────────────────────
860
861/// Loads a Lua chunk from a file.
862///
863pub(crate) fn loadfile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
864    let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
865    let mode: Option<Vec<u8>> = state.opt_arg_lstring(2, None)?;
866    let env = if state.type_at(3) != LuaType::None { 3 } else { 0 };
867    let status_ok = state.load_file_ex(fname.as_deref(), mode.as_deref())?;
868    load_aux(state, status_ok, env)
869}
870
871// ── generic_reader ────────────────────────────────────────────────────────────
872
873/// Reader callback for `luaB_load` when the chunk source is a Lua function.
874/// Calls the function at stack[1] repeatedly to obtain successive chunks.
875///
876///
877/// PORT NOTE: In C this is a `lua_Reader` function pointer passed to
878/// `lua_load`. In Rust, readers are closures — but `generic_reader` itself
879/// needs `&mut LuaState`, which conflicts with `state.load_with_reader`'s
880/// own borrow.  The current translation materialises the reader as a free
881/// function for documentation purposes; Phase B must resolve the design
882/// (e.g., a separate reader-context type, or a split between "advance reader"
883/// and "run Lua call" phases).
884/// TODO(port): generic_reader — self-referential &mut borrow when used as lua_load callback.
885fn generic_reader(state: &mut LuaState) -> Result<Option<Vec<u8>>, LuaError> {
886    state.ensure_stack(2, b"too many nested functions")?;
887    state.push_copy(1)?;
888    state.call(0, 1)?;
889    if state.type_at(-1) == LuaType::Nil {
890        state.pop_n(1);
891        return Ok(None);
892    }
893    //      luaL_error(L, "reader function must return a string");
894    // lua_isstring in C is true for strings AND coercible numbers.
895    if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
896        return Err(LuaError::runtime(format_args!(
897            "reader function must return a string"
898        )));
899    }
900    state.replace(RESERVED_SLOT)?;
901    let bytes = state
902        .to_lua_string_bytes(RESERVED_SLOT)
903        .map(|b| b.to_vec());
904    Ok(bytes)
905}
906
907// ── load ──────────────────────────────────────────────────────────────────────
908
909/// Loads a Lua chunk from a string or a reader function.
910///
911pub(crate) fn load_fn(state: &mut LuaState) -> Result<usize, LuaError> {
912    // Lua 5.1's `load` takes a *reader function only* — string loading is
913    // `loadstring`'s job. `load("...")` errors with `function expected, got
914    // string`. The string-or-function overload is a 5.2 addition. Verified
915    // against lua5.1.5; see specs/followup/5.1-roster-syntax.md §1.
916    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
917        state.check_arg_type(1, LuaType::Function)?;
918    }
919    // Determine whether argument 1 is a string (load from buffer) or a
920    // function (load from reader).
921    let is_string = matches!(state.type_at(1), LuaType::String | LuaType::Number);
922    let mode: Vec<u8> = state.opt_arg_string(3, b"bt")?;
923    let env = if state.type_at(4) != LuaType::None { 4 } else { 0 };
924    let status_ok = if is_string {
925        let chunk: Vec<u8> = state.to_lua_string_bytes(1).unwrap_or_default();
926        let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
927            chunk.clone()
928        } else {
929            state.check_arg_string(2)?
930        };
931        state.load_buffer_ex(&chunk, &chunkname, &mode)?
932    } else {
933        let chunkname: Vec<u8> = state
934            .opt_arg_string_bytes(2)
935            .unwrap_or_else(|_| b"=(load)".to_vec());
936        state.check_arg_type(1, LuaType::Function)?;
937        lua_vm::api::set_top(state, RESERVED_SLOT)?;
938        // TODO(port): generic_reader cannot be passed directly due to self-referential
939        // &mut borrow — see generic_reader's PORT NOTE. Phase B resolves this.
940        state.load_with_reader(generic_reader, &chunkname, &mode)?
941    };
942    load_aux(state, status_ok, env)
943}
944
945/// `loadstring(s [, chunkname])` — Lua 5.1 only.
946///
947/// Loads a string as a Lua chunk. In 5.1 this is the string-loading counterpart
948/// to `load` (which takes a reader function only). The second argument is the
949/// chunk name. Verified against lua5.1.5; see
950/// specs/followup/5.1-roster-syntax.md §1.
951pub(crate) fn loadstring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
952    let chunk: Vec<u8> = state.check_arg_string(1)?;
953    let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
954        chunk.clone()
955    } else {
956        state.check_arg_string(2)?
957    };
958    let status_ok = state.load_buffer_ex(&chunk, &chunkname, b"bt")?;
959    load_aux(state, status_ok, 0)
960}
961
962/// `gcinfo()` — Lua 5.1 only. Returns the amount of memory in use by Lua, in
963/// kilobytes. A deprecated holdover of `collectgarbage("count")` that returns
964/// just the integer KB count. Verified against lua5.1.5: returns a number. See
965/// specs/followup/5.1-roster-syntax.md §1.
966pub(crate) fn gcinfo_fn(state: &mut LuaState) -> Result<usize, LuaError> {
967    let k = state.gc_count()?;
968    state.push(LuaValue::Int(k as i64));
969    Ok(1)
970}
971
972/// `newproxy([boolean | proxy])` — Lua 5.1 only.
973///
974/// Creates a zero-size userdata (a "proxy"). With no argument or `false`, the
975/// proxy has no metatable. With `true`, it gets a fresh empty metatable (so a
976/// host can install `__gc`/`__len`, the userdata idiom these metamethods need
977/// in 5.1). With another proxy, it shares that proxy's metatable. Mirrors
978/// `luaB_newproxy` in 5.1 `lbaselib.c`; see specs/followup/5.1-roster-syntax.md
979/// §1. The C version validates the proxy argument against a weak table of
980/// metatables it created; this port instead accepts any userdata that carries a
981/// metatable, which is observably equivalent for the proxy idiom.
982pub(crate) fn newproxy_fn(state: &mut LuaState) -> Result<usize, LuaError> {
983    lua_vm::api::set_top(state, 1)?;
984    // The new userdata is pushed at stack position 2.
985    state.new_userdata_typed(b"", 0, 0)?;
986    if !state.to_boolean(1) {
987        return Ok(1); // no metatable
988    }
989    if matches!(state.type_at(1), LuaType::Boolean) {
990        // `true`: create and attach a fresh empty metatable.
991        let mt = state.new_table();
992        state.push(LuaValue::Table(mt));
993        state.set_metatable(2)?;
994    } else {
995        // A proxy argument: share its metatable. Validate it is a userdata that
996        // carries one (the C version checks a weak table of valid metatables).
997        let is_proxy =
998            matches!(state.type_at(1), LuaType::UserData) && state.get_metatable(1)?;
999        if !is_proxy {
1000            return Err(lua_vm::debug::arg_error_impl(state, 1, b"boolean or proxy expected"));
1001        }
1002        // get_metatable pushed arg1's metatable on top; attach it to the proxy.
1003        state.set_metatable(2)?;
1004    }
1005    Ok(1)
1006}
1007
1008// ── dofile ────────────────────────────────────────────────────────────────────
1009
1010/// Loads and runs a Lua file, forwarding all return values.
1011///
1012fn dofile_cont(state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
1013    Ok((state.top() as i32 - 1) as usize)
1014}
1015
1016pub(crate) fn dofile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1017    let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
1018    lua_vm::api::set_top(state, 1)?;
1019    if !state.load_file(fname.as_deref())? {
1020        return Err(LuaError::from_value(state.pop()));
1021    }
1022    state.call_k(0, LUA_MULTRET, 0, Some(dofile_cont))?;
1023    dofile_cont(state, 0, 0)
1024}
1025
1026// ── assert ────────────────────────────────────────────────────────────────────
1027
1028/// Raises an error if the first argument is falsy, otherwise passes all
1029/// arguments through as return values.
1030///
1031pub(crate) fn assert_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1032    if state.to_boolean(1) {
1033        return Ok(state.top() as usize);
1034    }
1035    state.check_arg_any(1)?;
1036    state.remove(1)?;
1037    state.push_string(b"assertion failed!")?;
1038    lua_vm::api::set_top(state, 1)?;
1039    error_fn(state)
1040}
1041
1042// ── select ────────────────────────────────────────────────────────────────────
1043
1044/// Returns a slice of its arguments starting at the given index, or returns
1045/// the count of arguments when called with `"#"`.
1046///
1047pub(crate) fn select_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1048    let n = state.top() as i64;
1049    // Check for '#' first byte without holding a borrow across subsequent ops.
1050    let first_is_hash = state.type_at(1) == LuaType::String && {
1051        state
1052            .to_lua_string_bytes(1)
1053            .and_then(|b| b.first().copied())
1054            == Some(b'#')
1055    };
1056    if first_is_hash {
1057        state.push(LuaValue::Int(n - 1));
1058        return Ok(1);
1059    }
1060    let mut i = state.check_arg_integer(1)?;
1061    if i < 0 {
1062        i = n + i;
1063    } else if i > n {
1064        i = n;
1065    }
1066    if i < 1 {
1067        return Err(lua_vm::debug::arg_error_impl(state, 1, b"index out of range"));
1068    }
1069    // The values at stack positions [i+1 .. n] are already in place; the
1070    // runtime picks up the top (n - i) of them as results.
1071    Ok((n - i) as usize)
1072}
1073
1074// ── pcall ─────────────────────────────────────────────────────────────────────
1075
1076/// Protected call: returns true + results on success, or false + error on
1077/// failure.
1078///
1079pub(crate) fn pcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1080    state.check_arg_any(1)?;
1081    // Stack before: [f, a1, …, aN]
1082    // Stack after:  [true, f, a1, …, aN]
1083    state.push(LuaValue::Bool(true));
1084    state.insert(1)?;
1085    // nargs = gettop - 2 (subtract the sentinel `true` and the function).
1086    let nargs = state.top() as i32 - 2;
1087    let yieldable = state.is_yieldable();
1088    let ok = match state.protected_call_k(nargs, LUA_MULTRET, 0, 0, Some(finish_pcall_k)) {
1089        Ok(()) => true,
1090        // `LuaError::Yield` must bubble up to `lua_resume` so the continuation
1091        // saved on this frame can be invoked on resume.
1092        Err(LuaError::Yield) => return Err(LuaError::Yield),
1093        // A sandbox budget trip is uncatchable: re-raise instead of catching so
1094        // untrusted code cannot defeat the budget with `while true do pcall(..) end`.
1095        Err(e) if state.sandbox_aborting() => return Err(e),
1096        Err(e) if yieldable => return Err(e),
1097        Err(e) => {
1098            state.push(e.into_value());
1099            false
1100        }
1101    };
1102    finish_pcall(state, ok, 0)
1103}
1104
1105/// Continuation matching `LuaKFunction`. Invoked by `finishCcall` on the
1106/// resume path after a yield through pcall (or after a `__close` ran during
1107/// pcall error recovery).
1108///
1109fn finish_pcall_k(state: &mut LuaState, status: i32, extra: isize) -> Result<usize, LuaError> {
1110    let ok = status == LuaStatus::Ok as i32 || status == LuaStatus::Yield as i32;
1111    finish_pcall(state, ok, extra as i32)
1112}
1113
1114// ── xpcall ────────────────────────────────────────────────────────────────────
1115
1116/// Protected call with a separate error-handler function.
1117///
1118pub(crate) fn xpcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1119    // Lua 5.1's `xpcall(f, h)` does NOT forward extra arguments to `f` — `f` is
1120    // always called with zero arguments. The extra-argument forwarding is a 5.2
1121    // addition. Verified against lua5.1.5: `xpcall(fn, h, 1,2,3)` calls `fn`
1122    // with `select("#",...) == 0`. Drop any args past the handler. See
1123    // specs/followup/5.1-roster-syntax.md §1.
1124    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) && state.top() > 2 {
1125        lua_vm::api::set_top(state, 2)?;
1126    }
1127    let n = state.top() as i32;
1128    state.check_arg_type(2, LuaType::Function)?;
1129    // Stack before rotate: [f, err, a1, …, aN, true, f]
1130    // Stack after rotate:  [f, err, true, f, a1, …, aN]
1131    state.push(LuaValue::Bool(true));
1132    state.push_copy(1)?;
1133    state.rotate(3, 2)?;
1134    // errfunc is at stack index 2; extra=2 means finishpcall skips 2 values.
1135    let yieldable = state.is_yieldable();
1136    let ok = match state.protected_call_k(n - 2, LUA_MULTRET, 2, 2, Some(finish_pcall_k)) {
1137        Ok(()) => true,
1138        Err(LuaError::Yield) => return Err(LuaError::Yield),
1139        // Uncatchable sandbox abort: re-raise without running the message
1140        // handler, so an `xpcall` handler can neither swallow nor loop on it.
1141        Err(e) if state.sandbox_aborting() => return Err(e),
1142        Err(e) if yieldable => return Err(e),
1143        Err(e) => {
1144            state.push(e.into_value());
1145            false
1146        }
1147    };
1148    finish_pcall(state, ok, 2)
1149}
1150
1151// ── tostring ──────────────────────────────────────────────────────────────────
1152
1153/// Converts any value to its string representation (calls `__tostring` if
1154/// present).
1155///
1156pub(crate) fn tostring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1157    state.check_arg_any(1)?;
1158    // to_display_string pushes the converted string and returns a handle to it.
1159    // TODO(port): to_display_string method needs implementing on LuaState.
1160    state.to_display_string(1)?;
1161    Ok(1)
1162}
1163
1164// ── Registration table ────────────────────────────────────────────────────────
1165
1166/// All base-library functions registered into the global table by `open`.
1167///
1168///
1169/// PORT NOTE: The C table includes placeholder entries
1170/// `{LUA_GNAME, NULL}` and `{"_VERSION", NULL}` that `luaopen_base` fills in
1171/// separately.  Those are omitted here; `open()` sets them explicitly.
1172pub(crate) const BASE_FUNCS: &[(&[u8], LuaLibFn)] = &[
1173    (b"assert",         assert_fn),
1174    (b"collectgarbage", collectgarbage_fn),
1175    (b"dofile",         dofile_fn),
1176    (b"error",          error_fn),
1177    (b"getmetatable",   getmetatable_fn),
1178    (b"ipairs",         ipairs_fn),
1179    (b"loadfile",       loadfile_fn),
1180    (b"load",           load_fn),
1181    (b"next",           next_fn),
1182    (b"pairs",          pairs_fn),
1183    (b"pcall",          pcall_fn),
1184    (b"print",          print_fn),
1185    (b"warn",           warn_fn),
1186    (b"rawequal",       rawequal_fn),
1187    (b"rawlen",         rawlen_fn),
1188    (b"rawget",         rawget_fn),
1189    (b"rawset",         rawset_fn),
1190    (b"select",         select_fn),
1191    (b"setmetatable",   setmetatable_fn),
1192    (b"tonumber",       tonumber_fn),
1193    (b"tostring",       tostring_fn),
1194    (b"type",           type_fn),
1195    (b"xpcall",         xpcall_fn),
1196];
1197
1198// ── Module opener ─────────────────────────────────────────────────────────────
1199
1200/// Open the base library: register all base functions into the global table,
1201/// then set `_G` (a self-reference) and `_VERSION`.
1202///
1203pub fn open(state: &mut LuaState) -> Result<usize, LuaError> {
1204    state.push_globals()?;
1205    state.set_funcs(BASE_FUNCS, 0)?;
1206    state.push_copy(-1)?;
1207    state.set_field(-2, LUA_GNAME)?;
1208    let version_str = state.global().lua_version.version_str();
1209    state.push_string(version_str.as_bytes())?;
1210    state.set_field(-2, b"_VERSION")?;
1211    // `warn` was introduced in Lua 5.4; it is absent on 5.1/5.2/5.3.
1212    if matches!(
1213        state.global().lua_version,
1214        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
1215    ) {
1216        state.push(LuaValue::Nil);
1217        state.set_field(-2, b"warn")?;
1218    }
1219    // Lua 5.1/5.2 carry two globals that were removed in 5.3: `unpack` (an alias
1220    // of `table.unpack`) and `loadstring` (an alias of `load`). Verified against
1221    // lua5.2.4: both are functions. The base table is on the stack top here.
1222    if matches!(
1223        state.global().lua_version,
1224        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1225    ) {
1226        state.push_c_function(crate::table_lib::unpack)?;
1227        state.set_field(-2, b"unpack")?;
1228    }
1229    // `loadstring` aliases `load` in 5.2 (whose `load` accepts a string), but in
1230    // 5.1 `load` is reader-only, so `loadstring` is a distinct string-loader.
1231    // Both are absent in 5.3+. See specs/followup/5.1-roster-syntax.md §1.
1232    if matches!(state.global().lua_version, lua_types::LuaVersion::V52) {
1233        state.push_c_function(load_fn)?;
1234        state.set_field(-2, b"loadstring")?;
1235    }
1236    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1237        state.push_c_function(loadstring_fn)?;
1238        state.set_field(-2, b"loadstring")?;
1239        // `gcinfo()` and `newproxy()` are 5.1 holdovers absent in 5.2+.
1240        state.push_c_function(gcinfo_fn)?;
1241        state.set_field(-2, b"gcinfo")?;
1242        state.push_c_function(newproxy_fn)?;
1243        state.set_field(-2, b"newproxy")?;
1244        // `rawlen` is a Lua 5.2 addition; it is absent in 5.1. Verified against
1245        // lua5.1.5: `type(rawlen)` == "nil". It lives in BASE_FUNCS (registered
1246        // for every version), so withhold it under V51.
1247        state.push(LuaValue::Nil);
1248        state.set_field(-2, b"rawlen")?;
1249    }
1250    // Lua 5.1's fenv-based globals model: `getfenv`/`setfenv` read and write a
1251    // function's environment (its `_ENV` upvalue under the reused modern core)
1252    // or the running thread's global table for level 0. Both were removed in
1253    // 5.2 (which switched to lexical `_ENV`), so they are V51-only. See
1254    // specs/followup/5.1-fenv.md.
1255    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1256        state.push_c_function(getfenv_fn)?;
1257        state.set_field(-2, b"getfenv")?;
1258        state.push_c_function(setfenv_fn)?;
1259        state.set_field(-2, b"setfenv")?;
1260    }
1261    Ok(1)
1262}
1263
1264// ──────────────────────────────────────────────────────────────────────────────
1265// PORT STATUS
1266//   source:        src/lbaselib.c  (549 lines, 32 functions)
1267//   target_crate:  lua-stdlib
1268//   confidence:    medium
1269//   todos:         21
1270//   port_notes:    5
1271//   unsafe_blocks: 0
1272//   notes:         All 32 C functions translated.  Main uncertainties are (1)
1273//                  LuaState method signatures (top/type_at/push/… — resolved
1274//                  in Phase B when lua-vm is compiled), (2) generic_reader's
1275//                  self-referential &mut borrow needs architectural resolution,
1276//                  (3) GC API stubs (gc_count, gc_step, …) need Phase D
1277//                  implementations, (4) I/O host capabilities now route through
1278//                  state/global hooks, but stdin/env/time/temp remain incomplete,
1279//                  (5) pcallk / callk continuations are
1280//                  stubbed pending coroutine support in Phase E.  The fake
1281//                  `struct LuaState;` placeholder here avoids duplicate-definition
1282//                  errors while keeping the file self-contained; Phase B removes it.
1283// ──────────────────────────────────────────────────────────────────────────────