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    // Explicit collections bypass the checkpoint wrappers, so the dead
445    // stack slices must be cleared here before any collect dispatch
446    // (C parity: traversethread's atomic clear; see #140 / GC_ROOTS.md).
447    state.gc_clear_dead_stack_tails();
448    // The option set is version-gated. 5.4/5.3 expose `setpause`/`setstepmul`;
449    // 5.5 removed both and added `param` (lbaselib.c). The version that owns
450    // the running state decides which list/mapping applies.
451    let version = state.global().lua_version;
452    let is_v55 = version == lua_types::LuaVersion::V55;
453    // Lua 5.1's `collectgarbage` accepts only `collect/stop/restart/count/step/
454    // setpause/setstepmul`; the 5.2 `isrunning`/`generational`, the 5.4
455    // `incremental`, and the 5.5 `param` must be rejected with `invalid option`.
456    // Verified against lua5.1.5: `collectgarbage("isrunning")` errors. (5.2 DOES
457    // accept `isrunning`/`generational`, so it stays on OPTS_54.) See
458    // specs/followup/5.1-roster-syntax.md §1.
459    static OPTS_51: &[&[u8]] = &[
460        b"stop",
461        b"restart",
462        b"collect",
463        b"count",
464        b"step",
465        b"setpause",
466        b"setstepmul",
467    ];
468    static OPTS_NUM_51: &[GcOp] = &[
469        GcOp::Stop,
470        GcOp::Restart,
471        GcOp::Collect,
472        GcOp::Count,
473        GcOp::Step,
474        GcOp::SetPause,
475        GcOp::SetStepMul,
476    ];
477    static OPTS_54: &[&[u8]] = &[
478        b"stop",
479        b"restart",
480        b"collect",
481        b"count",
482        b"step",
483        b"setpause",
484        b"setstepmul",
485        b"isrunning",
486        b"generational",
487        b"incremental",
488    ];
489    static OPTS_NUM_54: &[GcOp] = &[
490        GcOp::Stop,
491        GcOp::Restart,
492        GcOp::Collect,
493        GcOp::Count,
494        GcOp::Step,
495        GcOp::SetPause,
496        GcOp::SetStepMul,
497        GcOp::IsRunning,
498        GcOp::Gen,
499        GcOp::Inc,
500    ];
501    static OPTS_55: &[&[u8]] = &[
502        b"stop",
503        b"restart",
504        b"collect",
505        b"count",
506        b"step",
507        b"isrunning",
508        b"generational",
509        b"incremental",
510        b"param",
511    ];
512    static OPTS_NUM_55: &[GcOp] = &[
513        GcOp::Stop,
514        GcOp::Restart,
515        GcOp::Collect,
516        GcOp::Count,
517        GcOp::Step,
518        GcOp::IsRunning,
519        GcOp::Gen,
520        GcOp::Inc,
521        GcOp::Param,
522    ];
523    let (opts, opts_num): (&[&[u8]], &[GcOp]) = if is_v55 {
524        (OPTS_55, OPTS_NUM_55)
525    } else if matches!(version, lua_types::LuaVersion::V51) {
526        (OPTS_51, OPTS_NUM_51)
527    } else {
528        (OPTS_54, OPTS_NUM_54)
529    };
530    let idx = state.check_arg_option(1, Some(b"collect"), opts)?;
531    let op = opts_num[idx];
532
533    // Each arm either returns early on success, or evaluates to `false`
534    // (meaning checkvalres fired — fall through to pushfail).
535    let valid: bool = match op {
536        GcOp::Count => {
537            // TODO(port): gc_count / gc_count_b are stubs in Phase A.
538            let k = state.gc_count()?;
539            let b = state.gc_count_b()?;
540            if k == -1 {
541                false
542            } else {
543                state.push(LuaValue::Float(k as f64 + b as f64 / 1024.0));
544                return Ok(1);
545            }
546        }
547        GcOp::Step => {
548            let step = state.opt_arg_integer(2, 0)? as i32;
549            // TODO(port): gc_step is a stub in Phase A.
550            let res = state.gc_step(step)?;
551            if res == -1 {
552                false
553            } else {
554                state.push(LuaValue::Bool(res != 0));
555                return Ok(1);
556            }
557        }
558        GcOp::SetPause | GcOp::SetStepMul => {
559            let p = state.opt_arg_integer(2, 0)? as i32;
560            // TODO(port): gc_set_param is a stub in Phase A.
561            let previous = state.gc_set_param(op as i32, p)?;
562            if previous == -1 {
563                false
564            } else {
565                state.push(LuaValue::Int(previous as i64));
566                return Ok(1);
567            }
568        }
569        GcOp::IsRunning => {
570            let res = state.gc_is_running()?;
571            state.push(LuaValue::Bool(res));
572            return Ok(1);
573        }
574        GcOp::Gen => {
575            let minormul = state.opt_arg_integer(2, 0)? as i32;
576            let majormul = state.opt_arg_integer(3, 0)? as i32;
577            // TODO(port): gc_gen is a stub in Phase A.
578            let oldmode = state.gc_gen(minormul, majormul)?;
579            return push_mode(state, oldmode);
580        }
581        GcOp::Inc => {
582            let pause = state.opt_arg_integer(2, 0)? as i32;
583            let stepmul = state.opt_arg_integer(3, 0)? as i32;
584            let stepsize = state.opt_arg_integer(4, 0)? as i32;
585            // TODO(port): gc_inc is a stub in Phase A.
586            let oldmode = state.gc_inc(pause, stepmul, stepsize)?;
587            return push_mode(state, oldmode);
588        }
589        GcOp::Param => {
590            // 5.5 collectgarbage("param", name [, value]): read or write a GC
591            // parameter, always returning the OLD integer value. arg2 selects
592            // the param; arg3 (default -1 = read-only) is the new value.
593            static PARAMS: &[&[u8]] = &[
594                b"minormul",
595                b"majorminor",
596                b"minormajor",
597                b"pause",
598                b"stepmul",
599                b"stepsize",
600            ];
601            let pidx = state.check_arg_option(2, None, PARAMS)?;
602            let value = state.opt_arg_integer(3, -1)?;
603            let old = state.gc_param(pidx, value)?;
604            state.push(LuaValue::Int(old));
605            return Ok(1);
606        }
607        _ => {
608            // TODO(port): gc_control_simple is a stub in Phase A.
609            let res = state.gc_control_simple(op as i32)?;
610            if res == -1 {
611                false
612            } else {
613                state.push(LuaValue::Int(res as i64));
614                return Ok(1);
615            }
616        }
617    };
618    debug_assert!(
619        !valid,
620        "valid arms return early; reaching here means checkvalres fired"
621    );
622    state.push(LuaValue::Nil);
623    Ok(1)
624}
625
626// ── type ──────────────────────────────────────────────────────────────────────
627
628/// Returns the type name of its argument as a string.
629///
630pub(crate) fn type_fn(state: &mut LuaState) -> Result<usize, LuaError> {
631    let t = state.type_at(1);
632    if t == LuaType::None {
633        return Err(lua_vm::debug::arg_error_impl(state, 1, b"value expected"));
634    }
635    // Clone the bytes before the push to avoid borrow conflict with state.
636    let name: Vec<u8> = state.type_name(t).to_vec();
637    state.push_string(&name)?;
638    Ok(1)
639}
640
641// ── getfenv / setfenv (Lua 5.1 fenv globals) ──────────────────────────────────
642
643/// Truncate a numeric `getfenv`/`setfenv` level toward zero.
644///
645/// 5.1's `luaL_checkint` casts `lua_Number` to a C `int`, truncating toward
646/// zero, so `getfenv(1.9)` is level 1 and `getfenv(-0.5)` is level 0. Under the
647/// float-only V51 model every number arrives as a `Float`; the `Int` arm is a
648/// defensive no-op. A non-number never reaches this helper.
649fn fenv_level(v: &LuaValue) -> i64 {
650    match v {
651        LuaValue::Float(f) => f.trunc() as i64,
652        LuaValue::Int(i) => *i,
653        _ => 0,
654    }
655}
656
657/// Resolve the function value targeted by a `getfenv`/`setfenv` first argument.
658///
659/// Returns the `LuaValue::Function` whose environment is being read or written.
660/// `arg1` is interpreted exactly as Lua 5.1's `getfunc`/`setfunc`
661/// (lbaselib.c): a function value targets that function directly; a number is a
662/// stack *level* (floored toward zero), where level 1 is the function calling
663/// `getfenv`/`setfenv`. Level 0 is handled by the callers (it denotes the
664/// running thread's global table, not a function) and never reaches here.
665///
666/// Errors mirror lua5.1.5:
667/// - negative level → `level must be non-negative`
668/// - level past the stack → `invalid level`
669/// - neither number nor function → `number expected, got <type>`
670fn fenv_getfunc(state: &mut LuaState, level: i64) -> Result<LuaValue, LuaError> {
671    if level < 0 {
672        return Err(lua_vm::debug::arg_error_impl(
673            state,
674            1,
675            b"level must be non-negative",
676        ));
677    }
678    let mut ar = lua_vm::debug::LuaDebug::default();
679    if !lua_vm::debug::get_stack(state, level as i32, &mut ar) {
680        return Err(lua_vm::debug::arg_error_impl(state, 1, b"invalid level"));
681    }
682    let ci_idx = ar
683        .i_ci
684        .ok_or_else(|| lua_vm::debug::arg_error_impl(state, 1, b"invalid level"))?;
685    let func_slot = state.get_ci(ci_idx).func;
686    Ok(state.get_at(func_slot))
687}
688
689/// Index of a Lua closure's `_ENV` upvalue, by upvalue name.
690///
691/// The reused modern parser threads an upvalue literally named `_ENV` and
692/// resolves every free (global) name through it; under V51 that upvalue *is* the
693/// function environment. It is NOT always upvalue 0 — a nested closure that
694/// captures locals places those first, with `_ENV` at a later index — so it must
695/// be located by name, not position. A closure that references no free names has
696/// no `_ENV` upvalue and returns `None`.
697fn fenv_env_upval_index(
698    lcl: &lua_types::gc::GcRef<lua_types::closure::LuaLClosure>,
699) -> Option<usize> {
700    lcl.proto
701        .upvalues
702        .iter()
703        .position(|ud| ud.name.as_ref().map(|s| s.as_bytes()) == Some(b"_ENV"))
704}
705
706/// Read the environment of a resolved function value.
707///
708/// A Lua closure's environment is its `_ENV` upvalue. A C/Rust function (or a
709/// Lua closure that references no globals, hence has no `_ENV` upvalue) is given
710/// the thread global table as its environment — matching the common 5.1 case
711/// and the documented `LUA_ENVIRONINDEX` gap (specs/followup/5.1-fenv.md §4).
712fn fenv_read(state: &LuaState, func: &LuaValue) -> LuaValue {
713    if let LuaValue::Function(LuaClosure::Lua(lcl)) = func {
714        if let Some(idx) = fenv_env_upval_index(lcl) {
715            return state.upvalue_get(lcl, idx);
716        }
717    }
718    state.global().globals.clone()
719}
720
721/// `getfenv([f])` — Lua 5.1 only.
722///
723/// Returns the environment of the function `f` (a function value or a stack
724/// level), or the running function's environment when the argument is absent or
725/// `1`. Level `0` returns the running thread's global table. See
726/// `specs/followup/5.1-fenv.md` §2.
727pub(crate) fn getfenv_fn(state: &mut LuaState) -> Result<usize, LuaError> {
728    let arg1 = state.value_at(1);
729    let func = match &arg1 {
730        LuaValue::Function(_) => arg1.clone(),
731        LuaValue::Nil if state.type_at(1) == LuaType::None => {
732            // No argument => level 1 (the running function).
733            fenv_getfunc(state, 1)?
734        }
735        LuaValue::Float(_) | LuaValue::Int(_) => {
736            let level = fenv_level(&arg1);
737            if level == 0 {
738                let g = state.global().globals.clone();
739                state.push(g);
740                return Ok(1);
741            }
742            fenv_getfunc(state, level)?
743        }
744        other => {
745            let got = state.obj_type_name(other);
746            let msg = format!("number expected, got {}", String::from_utf8_lossy(&got));
747            return Err(lua_vm::debug::arg_error_impl(state, 1, msg.as_bytes()));
748        }
749    };
750    let env = fenv_read(state, &func);
751    state.push(env);
752    Ok(1)
753}
754
755/// `setfenv(f, table)` — Lua 5.1 only.
756///
757/// Sets the environment of the function `f` (a function value or a stack level)
758/// to `table`. `setfenv(0, t)` sets the running thread's global table. Returns
759/// the affected function (or the running thread for level 0). A C/Rust function
760/// (or any non-Lua object) cannot have its environment changed and raises,
761/// matching lua5.1.5. See `specs/followup/5.1-fenv.md` §2.
762pub(crate) fn setfenv_fn(state: &mut LuaState) -> Result<usize, LuaError> {
763    state.check_arg_type(2, LuaType::Table)?;
764    let new_env = state.value_at(2);
765
766    let arg1 = state.value_at(1);
767    let is_level_zero =
768        matches!(&arg1, LuaValue::Int(0)) || matches!(&arg1, LuaValue::Float(f) if *f == 0.0);
769    if is_level_zero {
770        // Level 0: replace the running thread's global table and return the
771        // running thread. Subsequently-loaded top-level chunks take this env.
772        state.global_mut().globals = new_env;
773        lua_vm::api::push_thread(state);
774        return Ok(1);
775    }
776
777    let func = match &arg1 {
778        LuaValue::Function(_) => arg1.clone(),
779        LuaValue::Float(_) | LuaValue::Int(_) => {
780            let level = fenv_level(&arg1);
781            fenv_getfunc(state, level)?
782        }
783        other => {
784            let got = state.obj_type_name(other);
785            let msg = format!("number expected, got {}", String::from_utf8_lossy(&got));
786            return Err(lua_vm::debug::arg_error_impl(state, 1, msg.as_bytes()));
787        }
788    };
789
790    match &func {
791        LuaValue::Function(LuaClosure::Lua(lcl)) => {
792            if let Some(idx) = fenv_env_upval_index(lcl) {
793                // Give the closure a PRIVATE environment: replace its `_ENV`
794                // upvalue *cell* with a fresh closed upvalue holding `new_env`.
795                // Mutating the existing cell's value (`upvalue_set`) would alter
796                // every closure sharing that upvalue (e.g. the main chunk's
797                // `_G`), which is wrong — `setfenv(f, e)` must not change the
798                // caller's globals. A new cell isolates `f`.
799                let uv = state.new_upval_closed(new_env);
800                lcl.set_upval(idx, uv);
801                state.gc().obj_barrier(lcl, &uv);
802            }
803            // A Lua closure that references no globals has no `_ENV` upvalue and
804            // nothing reads globals through it, so the set is inert; 5.1 still
805            // accepts it and returns the function. (Gap: a subsequent
806            // `getfenv` on such a closure returns the thread globals rather than
807            // the set table — see specs/followup/5.1-fenv.md §4.)
808        }
809        _ => {
810            // C/Rust functions cannot have their environment changed. 5.1
811            // raises this exact message (via luaL_error, so it carries the
812            // caller's source location) for any object whose env is fixed.
813            return Err(
814                state.where_error(1, b"'setfenv' cannot change environment of given object")
815            );
816        }
817    }
818    state.push(func);
819    Ok(1)
820}
821
822/// Set the environment of the Lua closure `level` frames up the running stack
823/// to `new_env`, the internal equivalent of `setfenv(level, new_env)`.
824///
825/// Used by `module` (5.1 `package` library), which sets its caller's
826/// environment to the module table. A non-Lua function (or a closure with no
827/// `_ENV` upvalue) is left unchanged, matching the inert-set behavior of
828/// `setfenv`. See specs/followup/5.1-fenv.md.
829pub(crate) fn set_func_env_at_level(
830    state: &mut LuaState,
831    level: i64,
832    new_env: LuaValue,
833) -> Result<(), LuaError> {
834    let func = fenv_getfunc(state, level)?;
835    if let LuaValue::Function(LuaClosure::Lua(lcl)) = &func {
836        if let Some(idx) = fenv_env_upval_index(lcl) {
837            let uv = state.new_upval_closed(new_env);
838            lcl.set_upval(idx, uv);
839            state.gc().obj_barrier(lcl, &uv);
840        }
841    }
842    Ok(())
843}
844
845// ── next ──────────────────────────────────────────────────────────────────────
846
847/// Table traversal iterator: given a table and a key, pushes the next key-value
848/// pair.  Pushes nil and returns 1 when the traversal is exhausted.
849///
850pub(crate) fn next_fn(state: &mut LuaState) -> Result<usize, LuaError> {
851    state.check_arg_type(1, LuaType::Table)?;
852    lua_vm::api::set_top(state, 2)?;
853    if state.table_next(1)? {
854        Ok(2)
855    } else {
856        state.push(LuaValue::Nil);
857        Ok(1)
858    }
859}
860
861// ── pairs continuation (coroutine stub) ───────────────────────────────────────
862
863/// Continuation for `pairs` when the `__pairs` metamethod yields.
864/// Re-invoked by `finishCcall` after the yielded `__pairs` resumes.
865///
866fn pairs_cont(state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
867    if state.global().lua_version == lua_types::LuaVersion::V55 {
868        Ok(4)
869    } else {
870        Ok(3)
871    }
872}
873
874// ── pairs ─────────────────────────────────────────────────────────────────────
875
876/// Returns the `next` function, the table, and nil (or invokes a `__pairs`
877/// metamethod).
878///
879pub(crate) fn pairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
880    state.check_arg_any(1)?;
881    // Lua 5.1 has no `__pairs` metamethod; `pairs(t)` always iterates the raw
882    // table even when a `__pairs` is set (it is silently ignored). Lua 5.5
883    // extends the result list with a fourth to-be-closed object.
884    let consult_pairs_tm = !matches!(state.global().lua_version, lua_types::LuaVersion::V51);
885    let nresults = if state.global().lua_version == lua_types::LuaVersion::V55 {
886        4
887    } else {
888        3
889    };
890    if !consult_pairs_tm || state.get_metafield(1, b"__pairs")? == LuaType::Nil {
891        state.push_c_function(next_fn)?;
892        state.push_copy(1)?;
893        state.push(LuaValue::Nil);
894        if nresults == 4 {
895            state.push(LuaValue::Nil);
896        }
897    } else {
898        state.push_copy(1)?;
899        state.call_k(1, nresults as i32, 0, Some(pairs_cont))?;
900    }
901    Ok(nresults)
902}
903
904// ── ipairs auxiliary ──────────────────────────────────────────────────────────
905
906/// Iterator step function for `ipairs`: increments the counter and fetches
907/// the next array element.  Returns the index + value, or just the index when
908/// the value is nil (signalling end-of-iteration).
909///
910fn ipairs_aux(state: &mut LuaState) -> Result<usize, LuaError> {
911    let i = match lua_vm::api::positive_index_value(state, 2) {
912        LuaValue::Int(i) => i,
913        _ => state.check_arg_integer(2)?,
914    };
915    // luaL_intop(+, a, b) → wrapping integer addition (PORTING.md §9 / macros.tsv `intop`)
916    let i = (i as u64).wrapping_add(1u64) as i64;
917    state.push(LuaValue::Int(i));
918    let table = lua_vm::api::positive_index_value(state, 1);
919    let t = state.table_get_i_value(&table, i)?;
920    if t == LuaType::Nil {
921        Ok(1)
922    } else {
923        Ok(2)
924    }
925}
926
927// ── ipairs ────────────────────────────────────────────────────────────────────
928
929/// Returns the `ipairsaux` iterator, the table, and 0 as the initial counter.
930///
931pub(crate) fn ipairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
932    state.check_arg_any(1)?;
933    state.push_c_function(ipairs_aux)?;
934    state.push_copy(1)?;
935    state.push(LuaValue::Int(0));
936    Ok(3)
937}
938
939// ── loadfile ──────────────────────────────────────────────────────────────────
940
941/// Loads a Lua chunk from a file.
942///
943pub(crate) fn loadfile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
944    let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
945    let mode: Option<Vec<u8>> = if state.is_none_or_nil(2) {
946        None
947    } else {
948        Some(check_load_mode(state, 2, b"bt")?)
949    };
950    let env = if state.type_at(3) != LuaType::None {
951        3
952    } else {
953        0
954    };
955    let status_ok = state.load_file_ex(fname.as_deref(), mode.as_deref())?;
956    load_aux(state, status_ok, env)
957}
958
959// ── generic_reader ────────────────────────────────────────────────────────────
960
961/// Reader callback for `luaB_load` when the chunk source is a Lua function.
962/// Calls the function at stack[1] repeatedly to obtain successive chunks.
963///
964///
965/// PORT NOTE: In C this is a `lua_Reader` function pointer passed to
966/// `lua_load`. In Rust, readers are closures — but `generic_reader` itself
967/// needs `&mut LuaState`, which conflicts with `state.load_with_reader`'s
968/// own borrow.  The current translation materialises the reader as a free
969/// function for documentation purposes; Phase B must resolve the design
970/// (e.g., a separate reader-context type, or a split between "advance reader"
971/// and "run Lua call" phases).
972/// TODO(port): generic_reader — self-referential &mut borrow when used as lua_load callback.
973fn generic_reader(state: &mut LuaState) -> Result<Option<Vec<u8>>, LuaError> {
974    state.ensure_stack(2, b"too many nested functions")?;
975    state.push_copy(1)?;
976    state.call(0, 1)?;
977    if state.type_at(-1) == LuaType::Nil {
978        state.pop_n(1);
979        return Ok(None);
980    }
981    //      luaL_error(L, "reader function must return a string");
982    // lua_isstring in C is true for strings AND coercible numbers.
983    if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
984        return Err(LuaError::runtime(format_args!(
985            "reader function must return a string"
986        )));
987    }
988    state.replace(RESERVED_SLOT)?;
989    let bytes = state.to_lua_string_bytes(RESERVED_SLOT).map(|b| b.to_vec());
990    Ok(bytes)
991}
992
993// ── load ──────────────────────────────────────────────────────────────────────
994
995/// Loads a Lua chunk from a string or a reader function.
996///
997pub(crate) fn load_fn(state: &mut LuaState) -> Result<usize, LuaError> {
998    // Lua 5.1's `load` takes a *reader function only* — string loading is
999    // `loadstring`'s job. `load("...")` errors with `function expected, got
1000    // string`. The string-or-function overload is a 5.2 addition. Verified
1001    // against lua5.1.5; see specs/followup/5.1-roster-syntax.md §1.
1002    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1003        state.check_arg_type(1, LuaType::Function)?;
1004    }
1005    // Determine whether argument 1 is a string (load from buffer) or a
1006    // function (load from reader).
1007    let is_string = matches!(state.type_at(1), LuaType::String | LuaType::Number);
1008    let mode: Vec<u8> = check_load_mode(state, 3, b"bt")?;
1009    let env = if state.type_at(4) != LuaType::None {
1010        4
1011    } else {
1012        0
1013    };
1014    let status_ok = if is_string {
1015        let chunk: Vec<u8> = state.to_lua_string_bytes(1).unwrap_or_default();
1016        let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
1017            chunk.clone()
1018        } else {
1019            state.check_arg_string(2)?
1020        };
1021        state.load_buffer_ex(&chunk, &chunkname, &mode)?
1022    } else {
1023        let chunkname: Vec<u8> = state
1024            .opt_arg_string_bytes(2)
1025            .unwrap_or_else(|_| b"=(load)".to_vec());
1026        state.check_arg_type(1, LuaType::Function)?;
1027        lua_vm::api::set_top(state, RESERVED_SLOT)?;
1028        // TODO(port): generic_reader cannot be passed directly due to self-referential
1029        // &mut borrow — see generic_reader's PORT NOTE. Phase B resolves this.
1030        state.load_with_reader(generic_reader, &chunkname, &mode)?
1031    };
1032    load_aux(state, status_ok, env)
1033}
1034
1035/// `loadstring(s [, chunkname])` — Lua 5.1 only.
1036///
1037/// Loads a string as a Lua chunk. In 5.1 this is the string-loading counterpart
1038/// to `load` (which takes a reader function only). The second argument is the
1039/// chunk name. Verified against lua5.1.5; see
1040/// specs/followup/5.1-roster-syntax.md §1.
1041pub(crate) fn loadstring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1042    let chunk: Vec<u8> = state.check_arg_string(1)?;
1043    let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
1044        chunk.clone()
1045    } else {
1046        state.check_arg_string(2)?
1047    };
1048    let status_ok = state.load_buffer_ex(&chunk, &chunkname, b"bt")?;
1049    load_aux(state, status_ok, 0)
1050}
1051
1052/// `gcinfo()` — Lua 5.1 only. Returns the amount of memory in use by Lua, in
1053/// kilobytes. A deprecated holdover of `collectgarbage("count")` that returns
1054/// just the integer KB count. Verified against lua5.1.5: returns a number. See
1055/// specs/followup/5.1-roster-syntax.md §1.
1056pub(crate) fn gcinfo_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1057    let k = state.gc_count()?;
1058    state.push(LuaValue::Int(k as i64));
1059    Ok(1)
1060}
1061
1062/// `newproxy([boolean | proxy])` — Lua 5.1 only.
1063///
1064/// Creates a zero-size userdata (a "proxy"). With no argument or `false`, the
1065/// proxy has no metatable. With `true`, it gets a fresh empty metatable (so a
1066/// host can install `__gc`/`__len`, the userdata idiom these metamethods need
1067/// in 5.1). With another proxy, it shares that proxy's metatable. Mirrors
1068/// `luaB_newproxy` in 5.1 `lbaselib.c`; see specs/followup/5.1-roster-syntax.md
1069/// §1. The C version validates the proxy argument against a weak table of
1070/// metatables it created; this port instead accepts any userdata that carries a
1071/// metatable, which is observably equivalent for the proxy idiom.
1072pub(crate) fn newproxy_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1073    lua_vm::api::set_top(state, 1)?;
1074    // The new userdata is pushed at stack position 2.
1075    state.new_userdata_typed(b"", 0, 0)?;
1076    if !state.to_boolean(1) {
1077        return Ok(1); // no metatable
1078    }
1079    if matches!(state.type_at(1), LuaType::Boolean) {
1080        // `true`: create and attach a fresh empty metatable.
1081        let mt = state.new_table();
1082        state.push(LuaValue::Table(mt));
1083        state.set_metatable(2)?;
1084    } else {
1085        // A proxy argument: share its metatable. Validate it is a userdata that
1086        // carries one (the C version checks a weak table of valid metatables).
1087        let is_proxy = matches!(state.type_at(1), LuaType::UserData) && state.get_metatable(1)?;
1088        if !is_proxy {
1089            return Err(lua_vm::debug::arg_error_impl(
1090                state,
1091                1,
1092                b"boolean or proxy expected",
1093            ));
1094        }
1095        // get_metatable pushed arg1's metatable on top; attach it to the proxy.
1096        state.set_metatable(2)?;
1097    }
1098    Ok(1)
1099}
1100
1101// ── dofile ────────────────────────────────────────────────────────────────────
1102
1103/// Loads and runs a Lua file, forwarding all return values.
1104///
1105fn dofile_cont(state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
1106    Ok((state.top() as i32 - 1) as usize)
1107}
1108
1109pub(crate) fn dofile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1110    let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
1111    lua_vm::api::set_top(state, 1)?;
1112    if !state.load_file(fname.as_deref())? {
1113        return Err(LuaError::from_value(state.pop()));
1114    }
1115    state.call_k(0, LUA_MULTRET, 0, Some(dofile_cont))?;
1116    dofile_cont(state, 0, 0)
1117}
1118
1119// ── assert ────────────────────────────────────────────────────────────────────
1120
1121/// Raises an error if the first argument is falsy, otherwise passes all
1122/// arguments through as return values.
1123///
1124pub(crate) fn assert_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1125    if state.to_boolean(1) {
1126        return Ok(state.top() as usize);
1127    }
1128    state.check_arg_any(1)?;
1129    state.remove(1)?;
1130    state.push_string(b"assertion failed!")?;
1131    lua_vm::api::set_top(state, 1)?;
1132    error_fn(state)
1133}
1134
1135// ── select ────────────────────────────────────────────────────────────────────
1136
1137/// Returns a slice of its arguments starting at the given index, or returns
1138/// the count of arguments when called with `"#"`.
1139///
1140pub(crate) fn select_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1141    let n = state.top() as i64;
1142    // Check for '#' first byte without holding a borrow across subsequent ops.
1143    let first_is_hash = state.type_at(1) == LuaType::String && {
1144        state
1145            .to_lua_string_bytes(1)
1146            .and_then(|b| b.first().copied())
1147            == Some(b'#')
1148    };
1149    if first_is_hash {
1150        state.push(LuaValue::Int(n - 1));
1151        return Ok(1);
1152    }
1153    let mut i = state.check_arg_integer(1)?;
1154    if i < 0 {
1155        i = n + i;
1156    } else if i > n {
1157        i = n;
1158    }
1159    if i < 1 {
1160        return Err(lua_vm::debug::arg_error_impl(
1161            state,
1162            1,
1163            b"index out of range",
1164        ));
1165    }
1166    // The values at stack positions [i+1 .. n] are already in place; the
1167    // runtime picks up the top (n - i) of them as results.
1168    Ok((n - i) as usize)
1169}
1170
1171// ── pcall ─────────────────────────────────────────────────────────────────────
1172
1173/// Protected call: returns true + results on success, or false + error on
1174/// failure.
1175///
1176pub(crate) fn pcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1177    state.check_arg_any(1)?;
1178    // Stack before: [f, a1, …, aN]
1179    // Stack after:  [true, f, a1, …, aN]
1180    state.push(LuaValue::Bool(true));
1181    state.insert(1)?;
1182    // nargs = gettop - 2 (subtract the sentinel `true` and the function).
1183    let nargs = state.top() as i32 - 2;
1184    let yieldable = state.is_yieldable();
1185    let ok = match state.protected_call_k(nargs, LUA_MULTRET, 0, 0, Some(finish_pcall_k)) {
1186        Ok(()) => true,
1187        // `LuaError::Yield` must bubble up to `lua_resume` so the continuation
1188        // saved on this frame can be invoked on resume.
1189        Err(LuaError::Yield) => return Err(LuaError::Yield),
1190        // A sandbox budget trip is uncatchable: re-raise instead of catching so
1191        // untrusted code cannot defeat the budget with `while true do pcall(..) end`.
1192        Err(e) if state.sandbox_aborting() => return Err(e),
1193        Err(e) if yieldable => return Err(e),
1194        Err(e) => {
1195            state.push(e.into_value());
1196            false
1197        }
1198    };
1199    finish_pcall(state, ok, 0)
1200}
1201
1202/// Continuation matching `LuaKFunction`. Invoked by `finishCcall` on the
1203/// resume path after a yield through pcall (or after a `__close` ran during
1204/// pcall error recovery).
1205///
1206fn finish_pcall_k(state: &mut LuaState, status: i32, extra: isize) -> Result<usize, LuaError> {
1207    let ok = status == LuaStatus::Ok as i32 || status == LuaStatus::Yield as i32;
1208    finish_pcall(state, ok, extra as i32)
1209}
1210
1211// ── xpcall ────────────────────────────────────────────────────────────────────
1212
1213/// Protected call with a separate error-handler function.
1214///
1215pub(crate) fn xpcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1216    // Lua 5.1's `xpcall(f, h)` does NOT forward extra arguments to `f` — `f` is
1217    // always called with zero arguments. The extra-argument forwarding is a 5.2
1218    // addition. Verified against lua5.1.5: `xpcall(fn, h, 1,2,3)` calls `fn`
1219    // with `select("#",...) == 0`. Drop any args past the handler. See
1220    // specs/followup/5.1-roster-syntax.md §1.
1221    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) && state.top() > 2 {
1222        lua_vm::api::set_top(state, 2)?;
1223    }
1224    let n = state.top() as i32;
1225    state.check_arg_type(2, LuaType::Function)?;
1226    // Stack before rotate: [f, err, a1, …, aN, true, f]
1227    // Stack after rotate:  [f, err, true, f, a1, …, aN]
1228    state.push(LuaValue::Bool(true));
1229    state.push_copy(1)?;
1230    state.rotate(3, 2)?;
1231    // errfunc is at stack index 2; extra=2 means finishpcall skips 2 values.
1232    let yieldable = state.is_yieldable();
1233    let ok = match state.protected_call_k(n - 2, LUA_MULTRET, 2, 2, Some(finish_pcall_k)) {
1234        Ok(()) => true,
1235        Err(LuaError::Yield) => return Err(LuaError::Yield),
1236        // Uncatchable sandbox abort: re-raise without running the message
1237        // handler, so an `xpcall` handler can neither swallow nor loop on it.
1238        Err(e) if state.sandbox_aborting() => return Err(e),
1239        Err(e) if yieldable => return Err(e),
1240        Err(e) => {
1241            state.push(e.into_value());
1242            false
1243        }
1244    };
1245    finish_pcall(state, ok, 2)
1246}
1247
1248// ── tostring ──────────────────────────────────────────────────────────────────
1249
1250/// Converts any value to its string representation (calls `__tostring` if
1251/// present).
1252///
1253pub(crate) fn tostring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1254    state.check_arg_any(1)?;
1255    // to_display_string pushes the converted string and returns a handle to it.
1256    // TODO(port): to_display_string method needs implementing on LuaState.
1257    state.to_display_string(1)?;
1258    Ok(1)
1259}
1260
1261// ── Registration table ────────────────────────────────────────────────────────
1262
1263/// All base-library functions registered into the global table by `open`.
1264///
1265///
1266/// PORT NOTE: The C table includes placeholder entries
1267/// `{LUA_GNAME, NULL}` and `{"_VERSION", NULL}` that `luaopen_base` fills in
1268/// separately.  Those are omitted here; `open()` sets them explicitly.
1269pub(crate) const BASE_FUNCS: &[(&[u8], LuaLibFn)] = &[
1270    (b"assert", assert_fn),
1271    (b"collectgarbage", collectgarbage_fn),
1272    (b"dofile", dofile_fn),
1273    (b"error", error_fn),
1274    (b"getmetatable", getmetatable_fn),
1275    (b"ipairs", ipairs_fn),
1276    (b"loadfile", loadfile_fn),
1277    (b"load", load_fn),
1278    (b"next", next_fn),
1279    (b"pairs", pairs_fn),
1280    (b"pcall", pcall_fn),
1281    (b"print", print_fn),
1282    (b"warn", warn_fn),
1283    (b"rawequal", rawequal_fn),
1284    (b"rawlen", rawlen_fn),
1285    (b"rawget", rawget_fn),
1286    (b"rawset", rawset_fn),
1287    (b"select", select_fn),
1288    (b"setmetatable", setmetatable_fn),
1289    (b"tonumber", tonumber_fn),
1290    (b"tostring", tostring_fn),
1291    (b"type", type_fn),
1292    (b"xpcall", xpcall_fn),
1293];
1294
1295// ── Module opener ─────────────────────────────────────────────────────────────
1296
1297/// Open the base library: register all base functions into the global table,
1298/// then set `_G` (a self-reference) and `_VERSION`.
1299///
1300pub fn open(state: &mut LuaState) -> Result<usize, LuaError> {
1301    state.push_globals()?;
1302    state.set_funcs(BASE_FUNCS, 0)?;
1303    state.push_copy(-1)?;
1304    state.set_field(-2, LUA_GNAME)?;
1305    let version_str = state.global().lua_version.version_str();
1306    state.push_string(version_str.as_bytes())?;
1307    state.set_field(-2, b"_VERSION")?;
1308    // `warn` was introduced in Lua 5.4; it is absent on 5.1/5.2/5.3.
1309    if matches!(
1310        state.global().lua_version,
1311        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
1312    ) {
1313        state.push(LuaValue::Nil);
1314        state.set_field(-2, b"warn")?;
1315    }
1316    // Lua 5.1/5.2 carry two globals that were removed in 5.3: `unpack` (an alias
1317    // of `table.unpack`) and `loadstring` (an alias of `load`). Verified against
1318    // lua5.2.4: both are functions. The base table is on the stack top here.
1319    if matches!(
1320        state.global().lua_version,
1321        lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1322    ) {
1323        state.push_c_function(crate::table_lib::unpack)?;
1324        state.set_field(-2, b"unpack")?;
1325    }
1326    // `loadstring` aliases `load` in 5.2 (whose `load` accepts a string), but in
1327    // 5.1 `load` is reader-only, so `loadstring` is a distinct string-loader.
1328    // Both are absent in 5.3+. See specs/followup/5.1-roster-syntax.md §1.
1329    if matches!(state.global().lua_version, lua_types::LuaVersion::V52) {
1330        state.push_c_function(load_fn)?;
1331        state.set_field(-2, b"loadstring")?;
1332    }
1333    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1334        state.push_c_function(loadstring_fn)?;
1335        state.set_field(-2, b"loadstring")?;
1336        // `gcinfo()` and `newproxy()` are 5.1 holdovers absent in 5.2+.
1337        state.push_c_function(gcinfo_fn)?;
1338        state.set_field(-2, b"gcinfo")?;
1339        state.push_c_function(newproxy_fn)?;
1340        state.set_field(-2, b"newproxy")?;
1341        // `rawlen` is a Lua 5.2 addition; it is absent in 5.1. Verified against
1342        // lua5.1.5: `type(rawlen)` == "nil". It lives in BASE_FUNCS (registered
1343        // for every version), so withhold it under V51.
1344        state.push(LuaValue::Nil);
1345        state.set_field(-2, b"rawlen")?;
1346    }
1347    // Lua 5.1's fenv-based globals model: `getfenv`/`setfenv` read and write a
1348    // function's environment (its `_ENV` upvalue under the reused modern core)
1349    // or the running thread's global table for level 0. Both were removed in
1350    // 5.2 (which switched to lexical `_ENV`), so they are V51-only. See
1351    // specs/followup/5.1-fenv.md.
1352    if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1353        state.push_c_function(getfenv_fn)?;
1354        state.set_field(-2, b"getfenv")?;
1355        state.push_c_function(setfenv_fn)?;
1356        state.set_field(-2, b"setfenv")?;
1357    }
1358    Ok(1)
1359}
1360
1361// ──────────────────────────────────────────────────────────────────────────────
1362// PORT STATUS
1363//   source:        src/lbaselib.c  (549 lines, 32 functions)
1364//   target_crate:  lua-stdlib
1365//   confidence:    medium
1366//   todos:         21
1367//   port_notes:    5
1368//   unsafe_blocks: 0
1369//   notes:         All 32 C functions translated.  Main uncertainties are (1)
1370//                  LuaState method signatures (top/type_at/push/… — resolved
1371//                  in Phase B when lua-vm is compiled), (2) generic_reader's
1372//                  self-referential &mut borrow needs architectural resolution,
1373//                  (3) GC API stubs (gc_count, gc_step, …) need Phase D
1374//                  implementations, (4) I/O host capabilities now route through
1375//                  state/global hooks, but stdin/env/time/temp remain incomplete,
1376//                  (5) pcallk / callk continuations are
1377//                  stubbed pending coroutine support in Phase E.  The fake
1378//                  `struct LuaState;` placeholder here avoids duplicate-definition
1379//                  errors while keeping the file self-contained; Phase B removes it.
1380// ──────────────────────────────────────────────────────────────────────────────