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            }
740            // A Lua closure that references no globals has no `_ENV` upvalue and
741            // nothing reads globals through it, so the set is inert; 5.1 still
742            // accepts it and returns the function. (Gap: a subsequent
743            // `getfenv` on such a closure returns the thread globals rather than
744            // the set table — see specs/followup/5.1-fenv.md §4.)
745        }
746        _ => {
747            // C/Rust functions cannot have their environment changed. 5.1
748            // raises this exact message (via luaL_error, so it carries the
749            // caller's source location) for any object whose env is fixed.
750            return Err(state.where_error(1, b"'setfenv' cannot change environment of given object"));
751        }
752    }
753    state.push(func);
754    Ok(1)
755}
756
757/// Set the environment of the Lua closure `level` frames up the running stack
758/// to `new_env`, the internal equivalent of `setfenv(level, new_env)`.
759///
760/// Used by `module` (5.1 `package` library), which sets its caller's
761/// environment to the module table. A non-Lua function (or a closure with no
762/// `_ENV` upvalue) is left unchanged, matching the inert-set behavior of
763/// `setfenv`. See specs/followup/5.1-fenv.md.
764pub(crate) fn set_func_env_at_level(
765    state: &mut LuaState,
766    level: i64,
767    new_env: LuaValue,
768) -> Result<(), LuaError> {
769    let func = fenv_getfunc(state, level)?;
770    if let LuaValue::Function(LuaClosure::Lua(lcl)) = &func {
771        if let Some(idx) = fenv_env_upval_index(lcl) {
772            let uv = state.new_upval_closed(new_env);
773            lcl.set_upval(idx, uv);
774        }
775    }
776    Ok(())
777}
778
779// ── next ──────────────────────────────────────────────────────────────────────
780
781/// Table traversal iterator: given a table and a key, pushes the next key-value
782/// pair.  Pushes nil and returns 1 when the traversal is exhausted.
783///
784pub(crate) fn next_fn(state: &mut LuaState) -> Result<usize, LuaError> {
785    state.check_arg_type(1, LuaType::Table)?;
786    lua_vm::api::set_top(state, 2)?;
787    if state.table_next(1)? {
788        Ok(2)
789    } else {
790        state.push(LuaValue::Nil);
791        Ok(1)
792    }
793}
794
795// ── pairs continuation (coroutine stub) ───────────────────────────────────────
796
797/// Continuation for `pairs` when the `__pairs` metamethod yields.
798/// Re-invoked by `finishCcall` after the yielded `__pairs` resumes.
799///
800fn pairs_cont(_state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
801    Ok(3)
802}
803
804// ── pairs ─────────────────────────────────────────────────────────────────────
805
806/// Returns the `next` function, the table, and nil (or invokes a `__pairs`
807/// metamethod).
808///
809pub(crate) fn pairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
810    state.check_arg_any(1)?;
811    // Lua 5.1 has no `__pairs` metamethod; `pairs(t)` always iterates the raw
812    // table even when a `__pairs` is set (it is silently ignored). `__pairs`
813    // was added in 5.2 and removed again in 5.4, so only consult it off V51.
814    let consult_pairs_tm = !matches!(state.global().lua_version, lua_types::LuaVersion::V51);
815    if !consult_pairs_tm || state.get_metafield(1, b"__pairs")? == LuaType::Nil {
816        state.push_c_function(next_fn)?;
817        state.push_copy(1)?;
818        state.push(LuaValue::Nil);
819    } else {
820        state.push_copy(1)?;
821        state.call_k(1, 3, 0, Some(pairs_cont))?;
822    }
823    Ok(3)
824}
825
826// ── ipairs auxiliary ──────────────────────────────────────────────────────────
827
828/// Iterator step function for `ipairs`: increments the counter and fetches
829/// the next array element.  Returns the index + value, or just the index when
830/// the value is nil (signalling end-of-iteration).
831///
832fn ipairs_aux(state: &mut LuaState) -> Result<usize, LuaError> {
833    let i = state.check_arg_integer(2)?;
834    // luaL_intop(+, a, b) → wrapping integer addition (PORTING.md §9 / macros.tsv `intop`)
835    let i = (i as u64).wrapping_add(1u64) as i64;
836    state.push(LuaValue::Int(i));
837    let t = state.get_i(1, i)?;
838    if t == LuaType::Nil {
839        Ok(1)
840    } else {
841        Ok(2)
842    }
843}
844
845// ── ipairs ────────────────────────────────────────────────────────────────────
846
847/// Returns the `ipairsaux` iterator, the table, and 0 as the initial counter.
848///
849pub(crate) fn ipairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
850    state.check_arg_any(1)?;
851    state.push_c_function(ipairs_aux)?;
852    state.push_copy(1)?;
853    state.push(LuaValue::Int(0));
854    Ok(3)
855}
856
857// ── loadfile ──────────────────────────────────────────────────────────────────
858
859/// Loads a Lua chunk from a file.
860///
861pub(crate) fn loadfile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
862    let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
863    let mode: Option<Vec<u8>> = state.opt_arg_lstring(2, None)?;
864    let env = if state.type_at(3) != LuaType::None { 3 } else { 0 };
865    let status_ok = state.load_file_ex(fname.as_deref(), mode.as_deref())?;
866    load_aux(state, status_ok, env)
867}
868
869// ── generic_reader ────────────────────────────────────────────────────────────
870
871/// Reader callback for `luaB_load` when the chunk source is a Lua function.
872/// Calls the function at stack[1] repeatedly to obtain successive chunks.
873///
874///
875/// PORT NOTE: In C this is a `lua_Reader` function pointer passed to
876/// `lua_load`. In Rust, readers are closures — but `generic_reader` itself
877/// needs `&mut LuaState`, which conflicts with `state.load_with_reader`'s
878/// own borrow.  The current translation materialises the reader as a free
879/// function for documentation purposes; Phase B must resolve the design
880/// (e.g., a separate reader-context type, or a split between "advance reader"
881/// and "run Lua call" phases).
882/// TODO(port): generic_reader — self-referential &mut borrow when used as lua_load callback.
883fn generic_reader(state: &mut LuaState) -> Result<Option<Vec<u8>>, LuaError> {
884    state.ensure_stack(2, b"too many nested functions")?;
885    state.push_copy(1)?;
886    state.call(0, 1)?;
887    if state.type_at(-1) == LuaType::Nil {
888        state.pop_n(1);
889        return Ok(None);
890    }
891    //      luaL_error(L, "reader function must return a string");
892    // lua_isstring in C is true for strings AND coercible numbers.
893    if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
894        return Err(LuaError::runtime(format_args!(
895            "reader function must return a string"
896        )));
897    }
898    state.replace(RESERVED_SLOT)?;
899    let bytes = state
900        .to_lua_string_bytes(RESERVED_SLOT)
901        .map(|b| b.to_vec());
902    Ok(bytes)
903}
904
905// ── load ──────────────────────────────────────────────────────────────────────
906
907/// Loads a Lua chunk from a string or a reader function.
908///
909pub(crate) fn load_fn(state: &mut LuaState) -> Result<usize, LuaError> {
910    // Lua 5.1's `load` takes a *reader function only* — string loading is
911    // `loadstring`'s job. `load("...")` errors with `function expected, got
912    // string`. The string-or-function overload is a 5.2 addition. Verified
913    // against lua5.1.5; see specs/followup/5.1-roster-syntax.md §1.
914    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
915        state.check_arg_type(1, LuaType::Function)?;
916    }
917    // Determine whether argument 1 is a string (load from buffer) or a
918    // function (load from reader).
919    let is_string = matches!(state.type_at(1), LuaType::String | LuaType::Number);
920    let mode: Vec<u8> = state.opt_arg_string(3, b"bt")?;
921    let env = if state.type_at(4) != LuaType::None { 4 } else { 0 };
922    let status_ok = if is_string {
923        let chunk: Vec<u8> = state.to_lua_string_bytes(1).unwrap_or_default();
924        let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
925            chunk.clone()
926        } else {
927            state.check_arg_string(2)?
928        };
929        state.load_buffer_ex(&chunk, &chunkname, &mode)?
930    } else {
931        let chunkname: Vec<u8> = state
932            .opt_arg_string_bytes(2)
933            .unwrap_or_else(|_| b"=(load)".to_vec());
934        state.check_arg_type(1, LuaType::Function)?;
935        lua_vm::api::set_top(state, RESERVED_SLOT)?;
936        // TODO(port): generic_reader cannot be passed directly due to self-referential
937        // &mut borrow — see generic_reader's PORT NOTE. Phase B resolves this.
938        state.load_with_reader(generic_reader, &chunkname, &mode)?
939    };
940    load_aux(state, status_ok, env)
941}
942
943/// `loadstring(s [, chunkname])` — Lua 5.1 only.
944///
945/// Loads a string as a Lua chunk. In 5.1 this is the string-loading counterpart
946/// to `load` (which takes a reader function only). The second argument is the
947/// chunk name. Verified against lua5.1.5; see
948/// specs/followup/5.1-roster-syntax.md §1.
949pub(crate) fn loadstring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
950    let chunk: Vec<u8> = state.check_arg_string(1)?;
951    let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
952        chunk.clone()
953    } else {
954        state.check_arg_string(2)?
955    };
956    let status_ok = state.load_buffer_ex(&chunk, &chunkname, b"bt")?;
957    load_aux(state, status_ok, 0)
958}
959
960/// `gcinfo()` — Lua 5.1 only. Returns the amount of memory in use by Lua, in
961/// kilobytes. A deprecated holdover of `collectgarbage("count")` that returns
962/// just the integer KB count. Verified against lua5.1.5: returns a number. See
963/// specs/followup/5.1-roster-syntax.md §1.
964pub(crate) fn gcinfo_fn(state: &mut LuaState) -> Result<usize, LuaError> {
965    let k = state.gc_count()?;
966    state.push(LuaValue::Int(k as i64));
967    Ok(1)
968}
969
970/// `newproxy([boolean | proxy])` — Lua 5.1 only.
971///
972/// Creates a zero-size userdata (a "proxy"). With no argument or `false`, the
973/// proxy has no metatable. With `true`, it gets a fresh empty metatable (so a
974/// host can install `__gc`/`__len`, the userdata idiom these metamethods need
975/// in 5.1). With another proxy, it shares that proxy's metatable. Mirrors
976/// `luaB_newproxy` in 5.1 `lbaselib.c`; see specs/followup/5.1-roster-syntax.md
977/// §1. The C version validates the proxy argument against a weak table of
978/// metatables it created; this port instead accepts any userdata that carries a
979/// metatable, which is observably equivalent for the proxy idiom.
980pub(crate) fn newproxy_fn(state: &mut LuaState) -> Result<usize, LuaError> {
981    lua_vm::api::set_top(state, 1)?;
982    // The new userdata is pushed at stack position 2.
983    state.new_userdata_typed(b"", 0, 0)?;
984    if !state.to_boolean(1) {
985        return Ok(1); // no metatable
986    }
987    if matches!(state.type_at(1), LuaType::Boolean) {
988        // `true`: create and attach a fresh empty metatable.
989        let mt = state.new_table();
990        state.push(LuaValue::Table(mt));
991        state.set_metatable(2)?;
992    } else {
993        // A proxy argument: share its metatable. Validate it is a userdata that
994        // carries one (the C version checks a weak table of valid metatables).
995        let is_proxy =
996            matches!(state.type_at(1), LuaType::UserData) && state.get_metatable(1)?;
997        if !is_proxy {
998            return Err(lua_vm::debug::arg_error_impl(state, 1, b"boolean or proxy expected"));
999        }
1000        // get_metatable pushed arg1's metatable on top; attach it to the proxy.
1001        state.set_metatable(2)?;
1002    }
1003    Ok(1)
1004}
1005
1006// ── dofile ────────────────────────────────────────────────────────────────────
1007
1008/// Loads and runs a Lua file, forwarding all return values.
1009///
1010fn dofile_cont(state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
1011    Ok((state.top() as i32 - 1) as usize)
1012}
1013
1014pub(crate) fn dofile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1015    let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
1016    lua_vm::api::set_top(state, 1)?;
1017    if !state.load_file(fname.as_deref())? {
1018        return Err(LuaError::from_value(state.pop()));
1019    }
1020    state.call_k(0, LUA_MULTRET, 0, Some(dofile_cont))?;
1021    dofile_cont(state, 0, 0)
1022}
1023
1024// ── assert ────────────────────────────────────────────────────────────────────
1025
1026/// Raises an error if the first argument is falsy, otherwise passes all
1027/// arguments through as return values.
1028///
1029pub(crate) fn assert_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1030    if state.to_boolean(1) {
1031        return Ok(state.top() as usize);
1032    }
1033    state.check_arg_any(1)?;
1034    state.remove(1)?;
1035    state.push_string(b"assertion failed!")?;
1036    lua_vm::api::set_top(state, 1)?;
1037    error_fn(state)
1038}
1039
1040// ── select ────────────────────────────────────────────────────────────────────
1041
1042/// Returns a slice of its arguments starting at the given index, or returns
1043/// the count of arguments when called with `"#"`.
1044///
1045pub(crate) fn select_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1046    let n = state.top() as i64;
1047    // Check for '#' first byte without holding a borrow across subsequent ops.
1048    let first_is_hash = state.type_at(1) == LuaType::String && {
1049        state
1050            .to_lua_string_bytes(1)
1051            .and_then(|b| b.first().copied())
1052            == Some(b'#')
1053    };
1054    if first_is_hash {
1055        state.push(LuaValue::Int(n - 1));
1056        return Ok(1);
1057    }
1058    let mut i = state.check_arg_integer(1)?;
1059    if i < 0 {
1060        i = n + i;
1061    } else if i > n {
1062        i = n;
1063    }
1064    if i < 1 {
1065        return Err(lua_vm::debug::arg_error_impl(state, 1, b"index out of range"));
1066    }
1067    // The values at stack positions [i+1 .. n] are already in place; the
1068    // runtime picks up the top (n - i) of them as results.
1069    Ok((n - i) as usize)
1070}
1071
1072// ── pcall ─────────────────────────────────────────────────────────────────────
1073
1074/// Protected call: returns true + results on success, or false + error on
1075/// failure.
1076///
1077pub(crate) fn pcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1078    state.check_arg_any(1)?;
1079    // Stack before: [f, a1, …, aN]
1080    // Stack after:  [true, f, a1, …, aN]
1081    state.push(LuaValue::Bool(true));
1082    state.insert(1)?;
1083    // nargs = gettop - 2 (subtract the sentinel `true` and the function).
1084    let nargs = state.top() as i32 - 2;
1085    let yieldable = state.is_yieldable();
1086    let ok = match state.protected_call_k(nargs, LUA_MULTRET, 0, 0, Some(finish_pcall_k)) {
1087        Ok(()) => true,
1088        // `LuaError::Yield` must bubble up to `lua_resume` so the continuation
1089        // saved on this frame can be invoked on resume.
1090        Err(LuaError::Yield) => return Err(LuaError::Yield),
1091        // A sandbox budget trip is uncatchable: re-raise instead of catching so
1092        // untrusted code cannot defeat the budget with `while true do pcall(..) end`.
1093        Err(e) if state.sandbox_aborting() => return Err(e),
1094        Err(e) if yieldable => return Err(e),
1095        Err(e) => {
1096            state.push(e.into_value());
1097            false
1098        }
1099    };
1100    finish_pcall(state, ok, 0)
1101}
1102
1103/// Continuation matching `LuaKFunction`. Invoked by `finishCcall` on the
1104/// resume path after a yield through pcall (or after a `__close` ran during
1105/// pcall error recovery).
1106///
1107fn finish_pcall_k(state: &mut LuaState, status: i32, extra: isize) -> Result<usize, LuaError> {
1108    let ok = status == LuaStatus::Ok as i32 || status == LuaStatus::Yield as i32;
1109    finish_pcall(state, ok, extra as i32)
1110}
1111
1112// ── xpcall ────────────────────────────────────────────────────────────────────
1113
1114/// Protected call with a separate error-handler function.
1115///
1116pub(crate) fn xpcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1117    // Lua 5.1's `xpcall(f, h)` does NOT forward extra arguments to `f` — `f` is
1118    // always called with zero arguments. The extra-argument forwarding is a 5.2
1119    // addition. Verified against lua5.1.5: `xpcall(fn, h, 1,2,3)` calls `fn`
1120    // with `select("#",...) == 0`. Drop any args past the handler. See
1121    // specs/followup/5.1-roster-syntax.md §1.
1122    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) && state.top() > 2 {
1123        lua_vm::api::set_top(state, 2)?;
1124    }
1125    let n = state.top() as i32;
1126    state.check_arg_type(2, LuaType::Function)?;
1127    // Stack before rotate: [f, err, a1, …, aN, true, f]
1128    // Stack after rotate:  [f, err, true, f, a1, …, aN]
1129    state.push(LuaValue::Bool(true));
1130    state.push_copy(1)?;
1131    state.rotate(3, 2)?;
1132    // errfunc is at stack index 2; extra=2 means finishpcall skips 2 values.
1133    let yieldable = state.is_yieldable();
1134    let ok = match state.protected_call_k(n - 2, LUA_MULTRET, 2, 2, Some(finish_pcall_k)) {
1135        Ok(()) => true,
1136        Err(LuaError::Yield) => return Err(LuaError::Yield),
1137        // Uncatchable sandbox abort: re-raise without running the message
1138        // handler, so an `xpcall` handler can neither swallow nor loop on it.
1139        Err(e) if state.sandbox_aborting() => return Err(e),
1140        Err(e) if yieldable => return Err(e),
1141        Err(e) => {
1142            state.push(e.into_value());
1143            false
1144        }
1145    };
1146    finish_pcall(state, ok, 2)
1147}
1148
1149// ── tostring ──────────────────────────────────────────────────────────────────
1150
1151/// Converts any value to its string representation (calls `__tostring` if
1152/// present).
1153///
1154pub(crate) fn tostring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1155    state.check_arg_any(1)?;
1156    // to_display_string pushes the converted string and returns a handle to it.
1157    // TODO(port): to_display_string method needs implementing on LuaState.
1158    state.to_display_string(1)?;
1159    Ok(1)
1160}
1161
1162// ── Registration table ────────────────────────────────────────────────────────
1163
1164/// All base-library functions registered into the global table by `open`.
1165///
1166///
1167/// PORT NOTE: The C table includes placeholder entries
1168/// `{LUA_GNAME, NULL}` and `{"_VERSION", NULL}` that `luaopen_base` fills in
1169/// separately.  Those are omitted here; `open()` sets them explicitly.
1170pub(crate) const BASE_FUNCS: &[(&[u8], LuaLibFn)] = &[
1171    (b"assert",         assert_fn),
1172    (b"collectgarbage", collectgarbage_fn),
1173    (b"dofile",         dofile_fn),
1174    (b"error",          error_fn),
1175    (b"getmetatable",   getmetatable_fn),
1176    (b"ipairs",         ipairs_fn),
1177    (b"loadfile",       loadfile_fn),
1178    (b"load",           load_fn),
1179    (b"next",           next_fn),
1180    (b"pairs",          pairs_fn),
1181    (b"pcall",          pcall_fn),
1182    (b"print",          print_fn),
1183    (b"warn",           warn_fn),
1184    (b"rawequal",       rawequal_fn),
1185    (b"rawlen",         rawlen_fn),
1186    (b"rawget",         rawget_fn),
1187    (b"rawset",         rawset_fn),
1188    (b"select",         select_fn),
1189    (b"setmetatable",   setmetatable_fn),
1190    (b"tonumber",       tonumber_fn),
1191    (b"tostring",       tostring_fn),
1192    (b"type",           type_fn),
1193    (b"xpcall",         xpcall_fn),
1194];
1195
1196// ── Module opener ─────────────────────────────────────────────────────────────
1197
1198/// Open the base library: register all base functions into the global table,
1199/// then set `_G` (a self-reference) and `_VERSION`.
1200///
1201pub fn open(state: &mut LuaState) -> Result<usize, LuaError> {
1202    state.push_globals()?;
1203    state.set_funcs(BASE_FUNCS, 0)?;
1204    state.push_copy(-1)?;
1205    state.set_field(-2, LUA_GNAME)?;
1206    let version_str = state.global().lua_version.version_str();
1207    state.push_string(version_str.as_bytes())?;
1208    state.set_field(-2, b"_VERSION")?;
1209    // `warn` was introduced in Lua 5.4; it is absent on 5.1/5.2/5.3.
1210    if matches!(
1211        state.global().lua_version,
1212        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
1213    ) {
1214        state.push(LuaValue::Nil);
1215        state.set_field(-2, b"warn")?;
1216    }
1217    // Lua 5.1/5.2 carry two globals that were removed in 5.3: `unpack` (an alias
1218    // of `table.unpack`) and `loadstring` (an alias of `load`). Verified against
1219    // lua5.2.4: both are functions. The base table is on the stack top here.
1220    if matches!(
1221        state.global().lua_version,
1222        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1223    ) {
1224        state.push_c_function(crate::table_lib::unpack)?;
1225        state.set_field(-2, b"unpack")?;
1226    }
1227    // `loadstring` aliases `load` in 5.2 (whose `load` accepts a string), but in
1228    // 5.1 `load` is reader-only, so `loadstring` is a distinct string-loader.
1229    // Both are absent in 5.3+. See specs/followup/5.1-roster-syntax.md §1.
1230    if matches!(state.global().lua_version, lua_types::LuaVersion::V52) {
1231        state.push_c_function(load_fn)?;
1232        state.set_field(-2, b"loadstring")?;
1233    }
1234    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1235        state.push_c_function(loadstring_fn)?;
1236        state.set_field(-2, b"loadstring")?;
1237        // `gcinfo()` and `newproxy()` are 5.1 holdovers absent in 5.2+.
1238        state.push_c_function(gcinfo_fn)?;
1239        state.set_field(-2, b"gcinfo")?;
1240        state.push_c_function(newproxy_fn)?;
1241        state.set_field(-2, b"newproxy")?;
1242        // `rawlen` is a Lua 5.2 addition; it is absent in 5.1. Verified against
1243        // lua5.1.5: `type(rawlen)` == "nil". It lives in BASE_FUNCS (registered
1244        // for every version), so withhold it under V51.
1245        state.push(LuaValue::Nil);
1246        state.set_field(-2, b"rawlen")?;
1247    }
1248    // Lua 5.1's fenv-based globals model: `getfenv`/`setfenv` read and write a
1249    // function's environment (its `_ENV` upvalue under the reused modern core)
1250    // or the running thread's global table for level 0. Both were removed in
1251    // 5.2 (which switched to lexical `_ENV`), so they are V51-only. See
1252    // specs/followup/5.1-fenv.md.
1253    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1254        state.push_c_function(getfenv_fn)?;
1255        state.set_field(-2, b"getfenv")?;
1256        state.push_c_function(setfenv_fn)?;
1257        state.set_field(-2, b"setfenv")?;
1258    }
1259    Ok(1)
1260}
1261
1262// ──────────────────────────────────────────────────────────────────────────────
1263// PORT STATUS
1264//   source:        src/lbaselib.c  (549 lines, 32 functions)
1265//   target_crate:  lua-stdlib
1266//   confidence:    medium
1267//   todos:         21
1268//   port_notes:    5
1269//   unsafe_blocks: 0
1270//   notes:         All 32 C functions translated.  Main uncertainties are (1)
1271//                  LuaState method signatures (top/type_at/push/… — resolved
1272//                  in Phase B when lua-vm is compiled), (2) generic_reader's
1273//                  self-referential &mut borrow needs architectural resolution,
1274//                  (3) GC API stubs (gc_count, gc_step, …) need Phase D
1275//                  implementations, (4) I/O host capabilities now route through
1276//                  state/global hooks, but stdin/env/time/temp remain incomplete,
1277//                  (5) pcallk / callk continuations are
1278//                  stubbed pending coroutine support in Phase E.  The fake
1279//                  `struct LuaState;` placeholder here avoids duplicate-definition
1280//                  errors while keeping the file self-contained; Phase B removes it.
1281// ──────────────────────────────────────────────────────────────────────────────