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