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
6// TODO(port): LuaState and related types live in lua-vm; imports resolved in Phase B.
7use lua_types::{
8    closure::LuaClosure,
9    error::LuaError,
10    value::LuaValue,
11    LuaType,
12    LuaStatus,
13    arith::ArithOp,
14    gc::GcRef,
15};
16use crate::state_stub::{LuaState, LuaStateStubExt as _, lua_CFunction, upvalue_index, CompareOp, LuaDebug};
17
18// ── Module-level constants ────────────────────────────────────────────────────
19
20/// ASCII whitespace characters used by `b_str2int` for strspn-style skipping.
21/// C: `#define SPACECHARS " \f\n\r\t\v"`
22const SPACECHARS: &[u8] = b" \x0c\n\r\t\x0b";
23
24/// Reserved stack slot used by `generic_reader` to anchor the current chunk
25/// string so it is not collected while `lua_load` is running.
26/// C: `#define RESERVEDSLOT 5`
27const RESERVED_SLOT: i32 = 5;
28
29/// Lua version string pushed as `_VERSION` in the global table.
30/// C: `LUA_VERSION` from `lua.h`
31const LUA_VERSION_STR: &[u8] = b"Lua 5.4";
32
33/// Name of the global environment table stored as a global itself.
34/// C: `LUA_GNAME` from `lua.h`
35const LUA_GNAME: &[u8] = b"_G";
36
37/// Sentinel indicating "all return values" for call/pcall helpers.
38/// C: `LUA_MULTRET = -1`
39const LUA_MULTRET: i32 = -1;
40
41// ── GC operation codes ────────────────────────────────────────────────────────
42
43/// Identifies a GC control operation passed to the `collectgarbage` built-in.
44/// Mirrors the `LUA_GC*` integer constants from `lua.h`.
45/// TODO(port): define as a proper type in lua-types once the GC API is finalised.
46#[repr(i32)]
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48enum GcOp {
49    Stop       = 0,
50    Restart    = 1,
51    Collect    = 2,
52    Count      = 3,
53    CountB     = 4,
54    Step       = 5,
55    SetPause   = 6,
56    SetStepMul = 7,
57    IsRunning  = 9,
58    Gen        = 10,
59    Inc        = 11,
60}
61
62// ── LuaState forward declaration ─────────────────────────────────────────────
63
64// LuaState is provided by crate::state_stub.
65
66// ── Type alias for standard Lua-callable functions ────────────────────────────
67
68/// Rust equivalent of `lua_CFunction`: a bare function that receives the
69/// interpreter state and returns a count of pushed results.
70/// C: `typedef int (*lua_CFunction)(lua_State *L)`
71pub(crate) type LuaLibFn = fn(&mut LuaState) -> Result<usize, LuaError>;
72
73// ── Helper: push_mode ─────────────────────────────────────────────────────────
74
75/// Push the GC mode string ("incremental" or "generational") onto the stack,
76/// or push `nil` (fail) when `oldmode == -1` (invalid call inside a finalizer).
77///
78/// C: `static int pushmode(lua_State *L, int oldmode)`
79fn push_mode(state: &mut LuaState, oldmode: i32) -> Result<usize, LuaError> {
80    if oldmode == -1 {
81        // C: luaL_pushfail(L);
82        state.push(LuaValue::Nil);
83    } else {
84        // C: lua_pushstring(L, (oldmode == LUA_GCINC) ? "incremental" : "generational");
85        let s: &[u8] = if oldmode == GcOp::Inc as i32 {
86            b"incremental"
87        } else {
88            b"generational"
89        };
90        state.push_string(s)?;
91    }
92    Ok(1)
93}
94
95// ── Helper: finish_pcall ──────────────────────────────────────────────────────
96
97/// Shared result-adjustment logic for `pcall` and `xpcall`.
98///
99/// On success: returns the count of values already on the stack minus `extra`
100/// skipped sentinel values.  On failure: replaces whatever is on the stack
101/// with `[false, error_message]` and returns 2.
102///
103/// C: `static int finishpcall(lua_State *L, int status, lua_KContext extra)`
104fn finish_pcall(state: &mut LuaState, ok: bool, extra: i32) -> Result<usize, LuaError> {
105    // C: if (l_unlikely(status != LUA_OK && status != LUA_YIELD))
106    if !ok {
107        // C: lua_pushboolean(L, 0); lua_pushvalue(L, -2); return 2;
108        state.push(LuaValue::Bool(false));
109        state.push_copy(-2)?;
110        return Ok(2);
111    }
112    // C: return lua_gettop(L) - (int)extra;
113    Ok((state.top() as i32 - extra) as usize)
114}
115
116// ── Helper: b_str2int ─────────────────────────────────────────────────────────
117
118/// Parse an integer in an arbitrary base from the byte slice `s`.
119///
120/// Returns `Some((consumed, value))` on success, where `consumed` is the number
121/// of bytes from the start of `s` that were processed (leading and trailing
122/// ASCII whitespace included).  Returns `None` when the slice contains no valid
123/// numeral in `base`.
124///
125/// The caller checks `consumed == s.len()` to verify the whole string was used.
126///
127/// C: `static const char *b_str2int(const char *s, int base, lua_Integer *pn)`
128fn b_str2int(s: &[u8], base: u32) -> Option<(usize, i64)> {
129    let mut pos = 0usize;
130    // C: s += strspn(s, SPACECHARS); /* skip initial spaces */
131    while pos < s.len() && SPACECHARS.contains(&s[pos]) {
132        pos += 1;
133    }
134    // C: if (*s == '-') { s++; neg = 1; } else if (*s == '+') s++;
135    let neg = if pos < s.len() && s[pos] == b'-' {
136        pos += 1;
137        true
138    } else {
139        if pos < s.len() && s[pos] == b'+' {
140            pos += 1;
141        }
142        false
143    };
144    // C: if (!isalnum((unsigned char)*s)) return NULL; /* no digit? */
145    if pos >= s.len() || !s[pos].is_ascii_alphanumeric() {
146        return None;
147    }
148    let mut n: u64 = 0u64;
149    // C: do { ... } while (isalnum((unsigned char)*s));
150    loop {
151        let byte = s[pos];
152        let digit = if byte.is_ascii_digit() {
153            (byte - b'0') as u32
154        } else {
155            // C: (toupper((unsigned char)*s) - 'A') + 10
156            (byte.to_ascii_uppercase() - b'A') as u32 + 10
157        };
158        // C: if (digit >= base) return NULL; /* invalid numeral */
159        if digit >= base {
160            return None;
161        }
162        // C: n = n * base + digit;
163        n = n.wrapping_mul(base as u64).wrapping_add(digit as u64);
164        pos += 1;
165        if pos >= s.len() || !s[pos].is_ascii_alphanumeric() {
166            break;
167        }
168    }
169    // C: s += strspn(s, SPACECHARS); /* skip trailing spaces */
170    while pos < s.len() && SPACECHARS.contains(&s[pos]) {
171        pos += 1;
172    }
173    // C: *pn = (lua_Integer)((neg) ? (0u - n) : n);
174    let value: i64 = if neg {
175        0u64.wrapping_sub(n) as i64
176    } else {
177        n as i64
178    };
179    Some((pos, value))
180}
181
182// ── Helper: load_aux ──────────────────────────────────────────────────────────
183
184/// Shared post-load logic for `load` and `loadfile`.
185///
186/// On success (status_ok == true): optionally installs an environment upvalue,
187/// then returns 1 (the chunk function is on the stack).
188/// On failure: pushes nil then moves it before the error message, returns 2.
189///
190/// C: `static int load_aux(lua_State *L, int status, int envidx)`
191fn load_aux(state: &mut LuaState, status_ok: bool, envidx: i32) -> Result<usize, LuaError> {
192    if status_ok {
193        // C: if (envidx != 0) { lua_pushvalue(L, envidx); if (!lua_setupvalue(L, -2, 1)) lua_pop(L, 1); }
194        if envidx != 0 {
195            state.push_copy(envidx)?;
196            if state.set_upvalue(-2, 1)?.is_none() {
197                state.pop_n(1);
198            }
199        }
200        Ok(1)
201    } else {
202        // C: luaL_pushfail(L); lua_insert(L, -2); return 2;
203        state.push(LuaValue::Nil);
204        state.insert(-2);
205        Ok(2)
206    }
207}
208
209// ── print ─────────────────────────────────────────────────────────────────────
210
211/// Converts each argument to a string with `tostring()` semantics, separates
212/// them with tabs, writes them to standard output, and finishes with a newline.
213///
214/// C: `static int luaB_print(lua_State *L)`
215pub(crate) fn print_fn(state: &mut LuaState) -> Result<usize, LuaError> {
216    // C: int n = lua_gettop(L);
217    let n = state.top();
218    for i in 1..=n {
219        // C: const char *s = luaL_tolstring(L, i, &l);
220        // luaL_tolstring converts via tostring() metamethod, pushes result,
221        // returns a pointer. In Rust we get a GcRef and use its bytes.
222        // TODO(port): to_display_string method needs implementing on LuaState.
223        let display_ref = state.to_display_string(i)?;
224        // C: if (i > 1) lua_writestring("\t", 1);
225        if i > 1 {
226            // TODO(port): I/O should go through the state's output abstraction.
227            state.write_output(b"\t")?;
228        }
229        // C: lua_writestring(s, l);
230        let bytes = display_ref.clone();
231        state.write_output(&bytes)?;
232        // C: lua_pop(L, 1);  /* pop result from luaL_tolstring */
233        state.pop_n(1);
234    }
235    // C: lua_writeline();
236    state.write_output(b"\n")?;
237    Ok(0)
238}
239
240// ── warn ──────────────────────────────────────────────────────────────────────
241
242/// Validates that every argument is a string, then forwards them as a
243/// multi-part warning message via the state's warning hook.
244///
245/// C: `static int luaB_warn(lua_State *L)`
246pub(crate) fn warn_fn(state: &mut LuaState) -> Result<usize, LuaError> {
247    // C: int n = lua_gettop(L);
248    let n = state.top();
249    // C: luaL_checkstring(L, 1);  /* at least one argument */
250    state.check_arg_string(1)?;
251    // C: for (i = 2; i <= n; i++) luaL_checkstring(L, i);
252    for i in 2..=n {
253        state.check_arg_string(i)?;
254    }
255    // C: for (i = 1; i < n; i++) lua_warning(L, lua_tostring(L, i), 1);
256    for i in 1..n {
257        // Clone bytes before further mutation to avoid borrow conflict.
258        // PORTING.md §8: "No &LuaValue across a stack-mutating call."
259        let s: Vec<u8> = state
260            .to_lua_string_bytes(i)
261            .map(|b| b.to_vec())
262            .unwrap_or_default();
263        // continue = true (1) — more parts follow
264        state.warning(&s, true)?;
265    }
266    // C: lua_warning(L, lua_tostring(L, n), 0);  /* close warning */
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///
279/// C: `static int luaB_tonumber(lua_State *L)`
280pub(crate) fn tonumber_fn(state: &mut LuaState) -> Result<usize, LuaError> {
281    // C: if (lua_isnoneornil(L, 2))  /* standard conversion? */
282    if matches!(state.type_at(2), LuaType::None | LuaType::Nil) {
283        // C: if (lua_type(L, 1) == LUA_TNUMBER) { lua_settop(L, 1); return 1; }
284        if state.type_at(1) == LuaType::Number {
285            lua_vm::api::set_top(state, 1)?;
286            return Ok(1);
287        }
288        // C: const char *s = lua_tolstring(L, 1, &l);
289        // C: if (s != NULL && lua_stringtonumber(L, s) == l + 1) return 1;
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        // C: luaL_checkany(L, 1);  /* (but there must be some parameter) */
300        state.check_arg_any(1)?;
301    } else {
302        // C: lua_Integer base = luaL_checkinteger(L, 2);
303        let base = state.check_arg_integer(2)?;
304        // C: luaL_checktype(L, 1, LUA_TSTRING);  /* no numbers as strings */
305        state.check_arg_type(1, LuaType::String)?;
306        // Clone before further state ops (PORTING.md §8).
307        let bytes: Vec<u8> = state
308            .to_lua_string_bytes(1)
309            .map(|b| b.to_vec())
310            .unwrap_or_default();
311        // C: luaL_argcheck(L, 2 <= base && base <= 36, 2, "base out of range");
312        if !(2..=36).contains(&base) {
313            return Err(LuaError::arg_error(2, "base out of range"));
314        }
315        // C: if (b_str2int(s, (int)base, &n) == s + l) { lua_pushinteger(L, n); return 1; }
316        if let Some((consumed, n)) = b_str2int(&bytes, base as u32) {
317            if consumed == bytes.len() {
318                state.push(LuaValue::Int(n));
319                return Ok(1);
320            }
321        }
322    }
323    // C: luaL_pushfail(L);  /* not a number */
324    state.push(LuaValue::Nil);
325    Ok(1)
326}
327
328// ── error ─────────────────────────────────────────────────────────────────────
329
330/// Raises the value at stack[1] as a Lua error, optionally prepending
331/// source-location information for string errors when `level > 0`.
332///
333/// C: `static int luaB_error(lua_State *L)`
334pub(crate) fn error_fn(state: &mut LuaState) -> Result<usize, LuaError> {
335    // C: int level = (int)luaL_optinteger(L, 2, 1);
336    let level = state.opt_arg_integer(2, 1)? as i32;
337    // C: lua_settop(L, 1);
338    lua_vm::api::set_top(state, 1)?;
339    // C: if (lua_type(L, 1) == LUA_TSTRING && level > 0)
340    if state.type_at(1) == LuaType::String && level > 0 {
341        // C: luaL_where(L, level); lua_pushvalue(L, 1); lua_concat(L, 2);
342        state.push_where(level)?;
343        state.push_copy(1)?;
344        state.concat(2)?;
345    }
346    // C: return lua_error(L);
347    Err(LuaError::from_value(state.pop()))
348}
349
350// ── getmetatable ──────────────────────────────────────────────────────────────
351
352/// Returns the metatable of the first argument, or the `__metatable` field of
353/// the metatable if that field exists (protecting the raw metatable).
354///
355/// C: `static int luaB_getmetatable(lua_State *L)`
356pub(crate) fn getmetatable_fn(state: &mut LuaState) -> Result<usize, LuaError> {
357    // C: luaL_checkany(L, 1);
358    state.check_arg_any(1)?;
359    // C: if (!lua_getmetatable(L, 1)) { lua_pushnil(L); return 1; }
360    if !state.get_metatable(1)? {
361        state.push(LuaValue::Nil);
362        return Ok(1);
363    }
364    // C: luaL_getmetafield(L, 1, "__metatable");
365    // Returns LuaType::Nil if metatable has no __metatable; otherwise pushes it.
366    state.get_metafield(1, b"__metatable")?;
367    Ok(1)
368}
369
370// ── setmetatable ──────────────────────────────────────────────────────────────
371
372/// Sets the metatable of the table at argument 1 to the value at argument 2
373/// (nil clears it).  Raises an error if the current metatable is protected via
374/// `__metatable`.
375///
376/// C: `static int luaB_setmetatable(lua_State *L)`
377pub(crate) fn setmetatable_fn(state: &mut LuaState) -> Result<usize, LuaError> {
378    // C: int t = lua_type(L, 2);
379    let t = state.type_at(2);
380    // C: luaL_checktype(L, 1, LUA_TTABLE);
381    state.check_arg_type(1, LuaType::Table)?;
382    // C: luaL_argexpected(L, t == LUA_TNIL || t == LUA_TTABLE, 2, "nil or table");
383    if !(t == LuaType::Nil || t == LuaType::Table) {
384        let got = state.value_at(2);
385        return Err(LuaError::type_arg_error(2, "nil or table", &got));
386    }
387    // C: if (l_unlikely(luaL_getmetafield(L, 1, "__metatable") != LUA_TNIL))
388    if state.get_metafield(1, b"__metatable")? != LuaType::Nil {
389        // C: return luaL_error(L, "cannot change a protected metatable");
390        return Err(LuaError::runtime(format_args!(
391            "cannot change a protected metatable"
392        )));
393    }
394    // C: lua_settop(L, 2); lua_setmetatable(L, 1);
395    lua_vm::api::set_top(state, 2)?;
396    state.set_metatable(1)?;
397    Ok(1)
398}
399
400// ── rawequal ──────────────────────────────────────────────────────────────────
401
402/// Raw equality check (no metamethods).
403///
404/// C: `static int luaB_rawequal(lua_State *L)`
405pub(crate) fn rawequal_fn(state: &mut LuaState) -> Result<usize, LuaError> {
406    // C: luaL_checkany(L, 1); luaL_checkany(L, 2);
407    state.check_arg_any(1)?;
408    state.check_arg_any(2)?;
409    // C: lua_pushboolean(L, lua_rawequal(L, 1, 2));
410    let eq = state.raw_equal(1, 2)?;
411    state.push(LuaValue::Bool(eq));
412    Ok(1)
413}
414
415// ── rawlen ────────────────────────────────────────────────────────────────────
416
417/// Raw length (#) without metamethods; accepts tables and strings only.
418///
419/// C: `static int luaB_rawlen(lua_State *L)`
420pub(crate) fn rawlen_fn(state: &mut LuaState) -> Result<usize, LuaError> {
421    // C: int t = lua_type(L, 1);
422    let t = state.type_at(1);
423    // C: luaL_argexpected(L, t == LUA_TTABLE || t == LUA_TSTRING, 1, "table or string");
424    if !(t == LuaType::Table || t == LuaType::String) {
425        let got = state.value_at(1);
426        return Err(LuaError::type_arg_error(1, "table or string", &got));
427    }
428    // C: lua_pushinteger(L, lua_rawlen(L, 1));
429    let len = state.raw_len(1);
430    state.push(LuaValue::Int(len));
431    Ok(1)
432}
433
434// ── rawget ────────────────────────────────────────────────────────────────────
435
436/// Raw table read (no metamethods).
437///
438/// C: `static int luaB_rawget(lua_State *L)`
439pub(crate) fn rawget_fn(state: &mut LuaState) -> Result<usize, LuaError> {
440    // C: luaL_checktype(L, 1, LUA_TTABLE); luaL_checkany(L, 2);
441    state.check_arg_type(1, LuaType::Table)?;
442    state.check_arg_any(2)?;
443    // C: lua_settop(L, 2); lua_rawget(L, 1);
444    lua_vm::api::set_top(state, 2)?;
445    state.raw_get(1)?;
446    Ok(1)
447}
448
449// ── rawset ────────────────────────────────────────────────────────────────────
450
451/// Raw table write (no metamethods).
452///
453/// C: `static int luaB_rawset(lua_State *L)`
454pub(crate) fn rawset_fn(state: &mut LuaState) -> Result<usize, LuaError> {
455    // C: luaL_checktype(L, 1, LUA_TTABLE); luaL_checkany(L, 2); luaL_checkany(L, 3);
456    state.check_arg_type(1, LuaType::Table)?;
457    state.check_arg_any(2)?;
458    state.check_arg_any(3)?;
459    // C: lua_settop(L, 3); lua_rawset(L, 1);
460    lua_vm::api::set_top(state, 3)?;
461    state.raw_set(1)?;
462    Ok(1)
463}
464
465// ── collectgarbage ────────────────────────────────────────────────────────────
466
467/// Expose GC control to Lua scripts.  The first argument selects the operation;
468/// subsequent arguments are operation-specific parameters.
469///
470/// C: `static int luaB_collectgarbage(lua_State *L)`
471/// C: `#define checkvalres(res) { if (res == -1) break; }`
472///
473/// PORT NOTE: C's `checkvalres(x)` macro breaks out of the `switch` to the
474/// trailing `luaL_pushfail` when `x == -1` (called inside a finalizer).
475/// In Rust we model this with an explicit early-return to the pushfail path
476/// using a boolean flag, avoiding labeled blocks.
477pub(crate) fn collectgarbage_fn(state: &mut LuaState) -> Result<usize, LuaError> {
478    // C: static const char *const opts[] = {"stop","restart",...,NULL};
479    static OPTS: &[&[u8]] = &[
480        b"stop", b"restart", b"collect",
481        b"count", b"step", b"setpause", b"setstepmul",
482        b"isrunning", b"generational", b"incremental",
483    ];
484    // C: static const int optsnum[] = {LUA_GCSTOP, LUA_GCRESTART, ...};
485    static OPTS_NUM: &[GcOp] = &[
486        GcOp::Stop, GcOp::Restart, GcOp::Collect,
487        GcOp::Count, GcOp::Step, GcOp::SetPause, GcOp::SetStepMul,
488        GcOp::IsRunning, GcOp::Gen, GcOp::Inc,
489    ];
490    // C: int o = optsnum[luaL_checkoption(L, 1, "collect", opts)];
491    let idx = state.check_arg_option(1, Some(b"collect"), OPTS)?;
492    let op = OPTS_NUM[idx];
493
494    // Each arm either returns early on success, or evaluates to `false`
495    // (meaning checkvalres fired — fall through to pushfail).
496    let valid: bool = match op {
497        GcOp::Count => {
498            // C: int k = lua_gc(L, o); int b = lua_gc(L, LUA_GCCOUNTB); checkvalres(k);
499            // C: lua_pushnumber(L, (lua_Number)k + ((lua_Number)b/1024));
500            // TODO(port): gc_count / gc_count_b are stubs in Phase A.
501            let k = state.gc_count()?;
502            let b = state.gc_count_b()?;
503            if k == -1 {
504                false
505            } else {
506                state.push(LuaValue::Float(k as f64 + b as f64 / 1024.0));
507                return Ok(1);
508            }
509        }
510        GcOp::Step => {
511            // C: int step = (int)luaL_optinteger(L, 2, 0); int res = lua_gc(L, o, step);
512            // C: checkvalres(res); lua_pushboolean(L, res);
513            let step = state.opt_arg_integer(2, 0)? as i32;
514            // TODO(port): gc_step is a stub in Phase A.
515            let res = state.gc_step(step)?;
516            if res == -1 {
517                false
518            } else {
519                state.push(LuaValue::Bool(res != 0));
520                return Ok(1);
521            }
522        }
523        GcOp::SetPause | GcOp::SetStepMul => {
524            // C: int p = (int)luaL_optinteger(L, 2, 0); int previous = lua_gc(L, o, p);
525            // C: checkvalres(previous); lua_pushinteger(L, previous);
526            let p = state.opt_arg_integer(2, 0)? as i32;
527            // TODO(port): gc_set_param is a stub in Phase A.
528            let previous = state.gc_set_param(op as i32, p)?;
529            if previous == -1 {
530                false
531            } else {
532                state.push(LuaValue::Int(previous as i64));
533                return Ok(1);
534            }
535        }
536        GcOp::IsRunning => {
537            let res = state.gc_is_running()?;
538            state.push(LuaValue::Bool(res));
539            return Ok(1);
540        }
541        GcOp::Gen => {
542            // C: int minormul = luaL_optinteger(L, 2, 0);
543            // C: int majormul = luaL_optinteger(L, 3, 0);
544            // C: return pushmode(L, lua_gc(L, o, minormul, majormul));
545            let minormul = state.opt_arg_integer(2, 0)? as i32;
546            let majormul = state.opt_arg_integer(3, 0)? as i32;
547            // TODO(port): gc_gen is a stub in Phase A.
548            let oldmode = state.gc_gen(minormul, majormul)?;
549            return push_mode(state, oldmode);
550        }
551        GcOp::Inc => {
552            // C: int pause = ...; int stepmul = ...; int stepsize = ...;
553            // C: return pushmode(L, lua_gc(L, o, pause, stepmul, stepsize));
554            let pause    = state.opt_arg_integer(2, 0)? as i32;
555            let stepmul  = state.opt_arg_integer(3, 0)? as i32;
556            let stepsize = state.opt_arg_integer(4, 0)? as i32;
557            // TODO(port): gc_inc is a stub in Phase A.
558            let oldmode = state.gc_inc(pause, stepmul, stepsize)?;
559            return push_mode(state, oldmode);
560        }
561        _ => {
562            // C: default: int res = lua_gc(L, o); checkvalres(res); lua_pushinteger(L, res);
563            // TODO(port): gc_control_simple is a stub in Phase A.
564            let res = state.gc_control_simple(op as i32)?;
565            if res == -1 {
566                false
567            } else {
568                state.push(LuaValue::Int(res as i64));
569                return Ok(1);
570            }
571        }
572    };
573    debug_assert!(!valid, "valid arms return early; reaching here means checkvalres fired");
574    // C: luaL_pushfail(L);  /* invalid call (inside a finalizer) */
575    state.push(LuaValue::Nil);
576    Ok(1)
577}
578
579// ── type ──────────────────────────────────────────────────────────────────────
580
581/// Returns the type name of its argument as a string.
582///
583/// C: `static int luaB_type(lua_State *L)`
584pub(crate) fn type_fn(state: &mut LuaState) -> Result<usize, LuaError> {
585    // C: int t = lua_type(L, 1);
586    let t = state.type_at(1);
587    // C: luaL_argcheck(L, t != LUA_TNONE, 1, "value expected");
588    if t == LuaType::None {
589        return Err(LuaError::arg_error(1, "value expected"));
590    }
591    // C: lua_pushstring(L, lua_typename(L, t));
592    // Clone the bytes before the push to avoid borrow conflict with state.
593    let name: Vec<u8> = state.type_name(t).to_vec();
594    state.push_string(&name)?;
595    Ok(1)
596}
597
598// ── next ──────────────────────────────────────────────────────────────────────
599
600/// Table traversal iterator: given a table and a key, pushes the next key-value
601/// pair.  Pushes nil and returns 1 when the traversal is exhausted.
602///
603/// C: `static int luaB_next(lua_State *L)`
604pub(crate) fn next_fn(state: &mut LuaState) -> Result<usize, LuaError> {
605    // C: luaL_checktype(L, 1, LUA_TTABLE);
606    state.check_arg_type(1, LuaType::Table)?;
607    // C: lua_settop(L, 2);  /* create a 2nd argument if there isn't one */
608    lua_vm::api::set_top(state, 2)?;
609    // C: if (lua_next(L, 1)) return 2; else { lua_pushnil(L); return 1; }
610    if state.table_next(1)? {
611        Ok(2)
612    } else {
613        state.push(LuaValue::Nil);
614        Ok(1)
615    }
616}
617
618// ── pairs continuation (coroutine stub) ───────────────────────────────────────
619
620/// Continuation for `pairs` when the `__pairs` metamethod yields.
621/// Re-invoked by `finishCcall` after the yielded `__pairs` resumes.
622///
623/// C: `static int pairscont(lua_State *L, int status, lua_KContext k)`
624fn pairs_cont(_state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
625    // C: (void)L; (void)status; (void)k; return 3;
626    Ok(3)
627}
628
629// ── pairs ─────────────────────────────────────────────────────────────────────
630
631/// Returns the `next` function, the table, and nil (or invokes a `__pairs`
632/// metamethod).
633///
634/// C: `static int luaB_pairs(lua_State *L)`
635pub(crate) fn pairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
636    // C: luaL_checkany(L, 1);
637    state.check_arg_any(1)?;
638    // C: if (luaL_getmetafield(L, 1, "__pairs") == LUA_TNIL)
639    if state.get_metafield(1, b"__pairs")? == LuaType::Nil {
640        // C: lua_pushcfunction(L, luaB_next); lua_pushvalue(L, 1); lua_pushnil(L);
641        state.push_c_function(next_fn)?;
642        state.push_copy(1)?;
643        state.push(LuaValue::Nil);
644    } else {
645        // C: lua_pushvalue(L, 1); lua_callk(L, 1, 3, 0, pairscont);
646        state.push_copy(1)?;
647        state.call_k(1, 3, 0, Some(pairs_cont))?;
648    }
649    Ok(3)
650}
651
652// ── ipairs auxiliary ──────────────────────────────────────────────────────────
653
654/// Iterator step function for `ipairs`: increments the counter and fetches
655/// the next array element.  Returns the index + value, or just the index when
656/// the value is nil (signalling end-of-iteration).
657///
658/// C: `static int ipairsaux(lua_State *L)`
659fn ipairs_aux(state: &mut LuaState) -> Result<usize, LuaError> {
660    // C: lua_Integer i = luaL_checkinteger(L, 2);
661    let i = state.check_arg_integer(2)?;
662    // C: i = luaL_intop(+, i, 1);
663    // luaL_intop(+, a, b) → wrapping integer addition (PORTING.md §9 / macros.tsv `intop`)
664    let i = (i as u64).wrapping_add(1u64) as i64;
665    // C: lua_pushinteger(L, i);
666    state.push(LuaValue::Int(i));
667    // C: return (lua_geti(L, 1, i) == LUA_TNIL) ? 1 : 2;
668    let t = state.get_i(1, i)?;
669    if t == LuaType::Nil {
670        Ok(1)
671    } else {
672        Ok(2)
673    }
674}
675
676// ── ipairs ────────────────────────────────────────────────────────────────────
677
678/// Returns the `ipairsaux` iterator, the table, and 0 as the initial counter.
679///
680/// C: `static int luaB_ipairs(lua_State *L)`
681pub(crate) fn ipairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
682    // C: luaL_checkany(L, 1);
683    state.check_arg_any(1)?;
684    // C: lua_pushcfunction(L, ipairsaux); lua_pushvalue(L, 1); lua_pushinteger(L, 0);
685    state.push_c_function(ipairs_aux)?;
686    state.push_copy(1)?;
687    state.push(LuaValue::Int(0));
688    Ok(3)
689}
690
691// ── loadfile ──────────────────────────────────────────────────────────────────
692
693/// Loads a Lua chunk from a file.
694///
695/// C: `static int luaB_loadfile(lua_State *L)`
696pub(crate) fn loadfile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
697    // C: const char *fname = luaL_optstring(L, 1, NULL);
698    let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
699    // C: const char *mode = luaL_optstring(L, 2, NULL);
700    let mode: Option<Vec<u8>> = state.opt_arg_lstring(2, None)?;
701    // C: int env = (!lua_isnone(L, 3) ? 3 : 0);
702    let env = if state.type_at(3) != LuaType::None { 3 } else { 0 };
703    // C: int status = luaL_loadfilex(L, fname, mode);
704    // TODO(port): File I/O must go through state's IO abstraction; std::fs banned outside lua-cli.
705    let status_ok = state.load_file_ex(fname.as_deref(), mode.as_deref())?;
706    load_aux(state, status_ok, env)
707}
708
709// ── generic_reader ────────────────────────────────────────────────────────────
710
711/// Reader callback for `luaB_load` when the chunk source is a Lua function.
712/// Calls the function at stack[1] repeatedly to obtain successive chunks.
713///
714/// C: `static const char *generic_reader(lua_State *L, void *ud, size_t *size)`
715///
716/// PORT NOTE: In C this is a `lua_Reader` function pointer passed to
717/// `lua_load`. In Rust, readers are closures — but `generic_reader` itself
718/// needs `&mut LuaState`, which conflicts with `state.load_with_reader`'s
719/// own borrow.  The current translation materialises the reader as a free
720/// function for documentation purposes; Phase B must resolve the design
721/// (e.g., a separate reader-context type, or a split between "advance reader"
722/// and "run Lua call" phases).
723/// TODO(port): generic_reader — self-referential &mut borrow when used as lua_load callback.
724fn generic_reader(state: &mut LuaState) -> Result<Option<Vec<u8>>, LuaError> {
725    // C: luaL_checkstack(L, 2, "too many nested functions");
726    state.ensure_stack(2, b"too many nested functions")?;
727    // C: lua_pushvalue(L, 1); /* get function */ lua_call(L, 0, 1);
728    state.push_copy(1)?;
729    state.call(0, 1)?;
730    // C: if (lua_isnil(L, -1)) { lua_pop(L, 1); *size = 0; return NULL; }
731    if state.type_at(-1) == LuaType::Nil {
732        state.pop_n(1);
733        return Ok(None);
734    }
735    // C: else if (l_unlikely(!lua_isstring(L, -1)))
736    //      luaL_error(L, "reader function must return a string");
737    // lua_isstring in C is true for strings AND coercible numbers.
738    if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
739        return Err(LuaError::runtime(format_args!(
740            "reader function must return a string"
741        )));
742    }
743    // C: lua_replace(L, RESERVEDSLOT); return lua_tolstring(L, RESERVEDSLOT, size);
744    state.replace(RESERVED_SLOT)?;
745    let bytes = state
746        .to_lua_string_bytes(RESERVED_SLOT)
747        .map(|b| b.to_vec());
748    Ok(bytes)
749}
750
751// ── load ──────────────────────────────────────────────────────────────────────
752
753/// Loads a Lua chunk from a string or a reader function.
754///
755/// C: `static int luaB_load(lua_State *L)`
756pub(crate) fn load_fn(state: &mut LuaState) -> Result<usize, LuaError> {
757    // C: const char *s = lua_tolstring(L, 1, &l);
758    // Determine whether argument 1 is a string (load from buffer) or a
759    // function (load from reader).
760    let is_string = matches!(state.type_at(1), LuaType::String | LuaType::Number);
761    // C: const char *mode = luaL_optstring(L, 3, "bt");
762    let mode: Vec<u8> = state.opt_arg_string(3, b"bt")?;
763    // C: int env = (!lua_isnone(L, 4) ? 4 : 0);
764    let env = if state.type_at(4) != LuaType::None { 4 } else { 0 };
765    let status_ok = if is_string {
766        // C: const char *chunkname = luaL_optstring(L, 2, s);
767        // C: status = luaL_loadbufferx(L, s, l, chunkname, mode);
768        let chunk: Vec<u8> = state.to_lua_string_bytes(1).unwrap_or_default();
769        let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
770            chunk.clone()
771        } else {
772            state.check_arg_string(2)?
773        };
774        state.load_buffer_ex(&chunk, &chunkname, &mode)?
775    } else {
776        let chunkname: Vec<u8> = state
777            .opt_arg_string_bytes(2)
778            .unwrap_or_else(|_| b"=(load)".to_vec());
779        state.check_arg_type(1, LuaType::Function)?;
780        lua_vm::api::set_top(state, RESERVED_SLOT)?;
781        // TODO(port): generic_reader cannot be passed directly due to self-referential
782        // &mut borrow — see generic_reader's PORT NOTE. Phase B resolves this.
783        state.load_with_reader(generic_reader, &chunkname, &mode)?
784    };
785    load_aux(state, status_ok, env)
786}
787
788// ── dofile ────────────────────────────────────────────────────────────────────
789
790/// Loads and runs a Lua file, forwarding all return values.
791///
792/// C: `static int dofilecont(lua_State *L, int d1, lua_KContext d2)`
793/// C: `static int luaB_dofile(lua_State *L)`
794fn dofile_cont(state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
795    // C: (void)d1; (void)d2; return lua_gettop(L) - 1;
796    Ok((state.top() as i32 - 1) as usize)
797}
798
799pub(crate) fn dofile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
800    // C: const char *fname = luaL_optstring(L, 1, NULL);
801    let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
802    // C: lua_settop(L, 1);
803    lua_vm::api::set_top(state, 1)?;
804    // C: if (l_unlikely(luaL_loadfile(L, fname) != LUA_OK)) return lua_error(L);
805    // TODO(port): File I/O must go through state's IO abstraction; std::fs banned outside lua-cli.
806    if !state.load_file(fname.as_deref())? {
807        return Err(LuaError::from_value(state.pop()));
808    }
809    // C: lua_callk(L, 0, LUA_MULTRET, 0, dofilecont);
810    state.call_k(0, LUA_MULTRET, 0, Some(dofile_cont))?;
811    // C: return dofilecont(L, 0, 0);
812    dofile_cont(state, 0, 0)
813}
814
815// ── assert ────────────────────────────────────────────────────────────────────
816
817/// Raises an error if the first argument is falsy, otherwise passes all
818/// arguments through as return values.
819///
820/// C: `static int luaB_assert(lua_State *L)`
821pub(crate) fn assert_fn(state: &mut LuaState) -> Result<usize, LuaError> {
822    // C: if (l_likely(lua_toboolean(L, 1))) return lua_gettop(L);
823    if state.to_boolean(1) {
824        return Ok(state.top() as usize);
825    }
826    // C: luaL_checkany(L, 1); lua_remove(L, 1);
827    state.check_arg_any(1)?;
828    state.remove(1);
829    // C: lua_pushliteral(L, "assertion failed!"); lua_settop(L, 1);
830    state.push_string(b"assertion failed!")?;
831    lua_vm::api::set_top(state, 1)?;
832    // C: return luaB_error(L);
833    error_fn(state)
834}
835
836// ── select ────────────────────────────────────────────────────────────────────
837
838/// Returns a slice of its arguments starting at the given index, or returns
839/// the count of arguments when called with `"#"`.
840///
841/// C: `static int luaB_select(lua_State *L)`
842pub(crate) fn select_fn(state: &mut LuaState) -> Result<usize, LuaError> {
843    // C: int n = lua_gettop(L);
844    let n = state.top() as i64;
845    // C: if (lua_type(L, 1) == LUA_TSTRING && *lua_tostring(L, 1) == '#')
846    // Check for '#' first byte without holding a borrow across subsequent ops.
847    let first_is_hash = state.type_at(1) == LuaType::String && {
848        state
849            .to_lua_string_bytes(1)
850            .and_then(|b| b.first().copied())
851            == Some(b'#')
852    };
853    if first_is_hash {
854        // C: lua_pushinteger(L, n-1);
855        state.push(LuaValue::Int(n - 1));
856        return Ok(1);
857    }
858    // C: lua_Integer i = luaL_checkinteger(L, 1);
859    let mut i = state.check_arg_integer(1)?;
860    // C: if (i < 0) i = n + i; else if (i > n) i = n;
861    if i < 0 {
862        i = n + i;
863    } else if i > n {
864        i = n;
865    }
866    // C: luaL_argcheck(L, 1 <= i, 1, "index out of range");
867    if i < 1 {
868        return Err(LuaError::arg_error(1, "index out of range"));
869    }
870    // C: return n - (int)i;
871    // The values at stack positions [i+1 .. n] are already in place; the
872    // runtime picks up the top (n - i) of them as results.
873    Ok((n - i) as usize)
874}
875
876// ── pcall ─────────────────────────────────────────────────────────────────────
877
878/// Protected call: returns true + results on success, or false + error on
879/// failure.
880///
881/// C: `static int luaB_pcall(lua_State *L)`
882pub(crate) fn pcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
883    // C: luaL_checkany(L, 1);
884    state.check_arg_any(1)?;
885    // C: lua_pushboolean(L, 1); lua_insert(L, 1);
886    // Stack before: [f, a1, …, aN]
887    // Stack after:  [true, f, a1, …, aN]
888    state.push(LuaValue::Bool(true));
889    state.insert(1);
890    // C: status = lua_pcallk(L, lua_gettop(L) - 2, LUA_MULTRET, 0, 0, finishpcall);
891    // nargs = gettop - 2 (subtract the sentinel `true` and the function).
892    let nargs = state.top() as i32 - 2;
893    let yieldable = state.is_yieldable();
894    let ok = match state.protected_call_k(nargs, LUA_MULTRET, 0, 0, Some(finish_pcall_k)) {
895        Ok(()) => true,
896        // `LuaError::Yield` must bubble up to `lua_resume` so the continuation
897        // saved on this frame can be invoked on resume.
898        Err(LuaError::Yield) => return Err(LuaError::Yield),
899        Err(e) if yieldable => return Err(e),
900        Err(e) => {
901            state.push(e.into_value());
902            false
903        }
904    };
905    // C: return finishpcall(L, status, 0);
906    finish_pcall(state, ok, 0)
907}
908
909/// Continuation matching `LuaKFunction`. Invoked by `finishCcall` on the
910/// resume path after a yield through pcall (or after a `__close` ran during
911/// pcall error recovery).
912///
913/// C: `static int finishpcall(lua_State *L, int status, lua_KContext extra)`
914fn finish_pcall_k(state: &mut LuaState, status: i32, extra: isize) -> Result<usize, LuaError> {
915    let ok = status == LuaStatus::Ok as i32 || status == LuaStatus::Yield as i32;
916    finish_pcall(state, ok, extra as i32)
917}
918
919// ── xpcall ────────────────────────────────────────────────────────────────────
920
921/// Protected call with a separate error-handler function.
922///
923/// C: `static int luaB_xpcall(lua_State *L)`
924pub(crate) fn xpcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
925    // C: int n = lua_gettop(L);
926    let n = state.top() as i32;
927    // C: luaL_checktype(L, 2, LUA_TFUNCTION);  /* check error function */
928    state.check_arg_type(2, LuaType::Function)?;
929    // C: lua_pushboolean(L, 1); lua_pushvalue(L, 1); lua_rotate(L, 3, 2);
930    // Stack before rotate: [f, err, a1, …, aN, true, f]
931    // Stack after rotate:  [f, err, true, f, a1, …, aN]
932    state.push(LuaValue::Bool(true));
933    state.push_copy(1)?;
934    state.rotate(3, 2);
935    // C: status = lua_pcallk(L, n - 2, LUA_MULTRET, 2, 2, finishpcall);
936    // errfunc is at stack index 2; extra=2 means finishpcall skips 2 values.
937    let yieldable = state.is_yieldable();
938    let ok = match state.protected_call_k(n - 2, LUA_MULTRET, 2, 2, Some(finish_pcall_k)) {
939        Ok(()) => true,
940        Err(LuaError::Yield) => return Err(LuaError::Yield),
941        Err(e) if yieldable => return Err(e),
942        Err(e) => {
943            state.push(e.into_value());
944            false
945        }
946    };
947    // C: return finishpcall(L, status, 2);
948    finish_pcall(state, ok, 2)
949}
950
951// ── tostring ──────────────────────────────────────────────────────────────────
952
953/// Converts any value to its string representation (calls `__tostring` if
954/// present).
955///
956/// C: `static int luaB_tostring(lua_State *L)`
957pub(crate) fn tostring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
958    // C: luaL_checkany(L, 1); luaL_tolstring(L, 1, NULL);
959    state.check_arg_any(1)?;
960    // to_display_string pushes the converted string and returns a handle to it.
961    // TODO(port): to_display_string method needs implementing on LuaState.
962    state.to_display_string(1)?;
963    Ok(1)
964}
965
966// ── Registration table ────────────────────────────────────────────────────────
967
968/// All base-library functions registered into the global table by `open`.
969///
970/// C: `static const luaL_Reg base_funcs[]`
971///
972/// PORT NOTE: The C table includes placeholder entries
973/// `{LUA_GNAME, NULL}` and `{"_VERSION", NULL}` that `luaopen_base` fills in
974/// separately.  Those are omitted here; `open()` sets them explicitly.
975pub(crate) const BASE_FUNCS: &[(&[u8], LuaLibFn)] = &[
976    (b"assert",         assert_fn),
977    (b"collectgarbage", collectgarbage_fn),
978    (b"dofile",         dofile_fn),
979    (b"error",          error_fn),
980    (b"getmetatable",   getmetatable_fn),
981    (b"ipairs",         ipairs_fn),
982    (b"loadfile",       loadfile_fn),
983    (b"load",           load_fn),
984    (b"next",           next_fn),
985    (b"pairs",          pairs_fn),
986    (b"pcall",          pcall_fn),
987    (b"print",          print_fn),
988    (b"warn",           warn_fn),
989    (b"rawequal",       rawequal_fn),
990    (b"rawlen",         rawlen_fn),
991    (b"rawget",         rawget_fn),
992    (b"rawset",         rawset_fn),
993    (b"select",         select_fn),
994    (b"setmetatable",   setmetatable_fn),
995    (b"tonumber",       tonumber_fn),
996    (b"tostring",       tostring_fn),
997    (b"type",           type_fn),
998    (b"xpcall",         xpcall_fn),
999];
1000
1001// ── Module opener ─────────────────────────────────────────────────────────────
1002
1003/// Open the base library: register all base functions into the global table,
1004/// then set `_G` (a self-reference) and `_VERSION`.
1005///
1006/// C: `LUAMOD_API int luaopen_base(lua_State *L)`
1007pub fn open(state: &mut LuaState) -> Result<usize, LuaError> {
1008    // C: lua_pushglobaltable(L);
1009    state.push_globals()?;
1010    // C: luaL_setfuncs(L, base_funcs, 0);
1011    state.set_funcs(BASE_FUNCS, 0)?;
1012    // C: lua_pushvalue(L, -1); lua_setfield(L, -2, LUA_GNAME);
1013    state.push_copy(-1)?;
1014    state.set_field(-2, LUA_GNAME)?;
1015    // C: lua_pushliteral(L, LUA_VERSION); lua_setfield(L, -2, "_VERSION");
1016    state.push_string(LUA_VERSION_STR)?;
1017    state.set_field(-2, b"_VERSION")?;
1018    Ok(1)
1019}
1020
1021// ──────────────────────────────────────────────────────────────────────────────
1022// PORT STATUS
1023//   source:        src/lbaselib.c  (549 lines, 32 functions)
1024//   target_crate:  lua-stdlib
1025//   confidence:    medium
1026//   todos:         21
1027//   port_notes:    5
1028//   unsafe_blocks: 0
1029//   notes:         All 32 C functions translated.  Main uncertainties are (1)
1030//                  LuaState method signatures (top/type_at/push/… — resolved
1031//                  in Phase B when lua-vm is compiled), (2) generic_reader's
1032//                  self-referential &mut borrow needs architectural resolution,
1033//                  (3) GC API stubs (gc_count, gc_step, …) need Phase D
1034//                  implementations, (4) I/O (write_output, load_file*) must be
1035//                  routed through a state abstraction rather than std::fs/stdout
1036//                  directly (Phase B), (5) pcallk / callk continuations are
1037//                  stubbed pending coroutine support in Phase E.  The fake
1038//                  `struct LuaState;` placeholder here avoids duplicate-definition
1039//                  errors while keeping the file self-contained; Phase B removes it.
1040// ──────────────────────────────────────────────────────────────────────────────