lua_stdlib/base.rs
1//! Base library — Lua's built-in functions (`print`, `type`, `pairs`, `pcall`, …),
2//! covering Lua 5.1–5.5 from one source.
3//!
4//! C source: `reference/lua-5.4.7/src/lbaselib.c`.
5//!
6//! GRADUATED (Phase-2 idiomatization, 2026-06-14, `idiom/base`). base is the
7//! most VM-adjacent stdlib module: `pcall`/`xpcall`/`error` drive unwinding,
8//! `load` compiles, `next`/`pairs`/`ipairs` iterate, `type`/`tostring`/`raw*`
9//! are hot. All of that plumbing is **load-bearing** and was idiomatized
10//! AROUND, never through — the only edits are in the cold arg-checking /
11//! result-shaping / version-dispatch / registration layers. The behavioral net
12//! that now guards it: `tests/base_strengthen.rs` (reference-pinned across all
13//! five versions), `multiversion_oracle`, the official `calls`/`errors`/
14//! `nextvar`/`constructs` suites, and `check.sh` ×5. Net-strengthening FIRST
15//! caught three cross-version bugs the weak net hid — `ipairs` (raw read +
16//! table-check + `__ipairs` on 5.1/5.2), `assert` (5.1/5.2 string-coercible
17//! message), `rawlen` (function-named, version-gated reject) — all fixed in the
18//! cold seam layer. Two bugs needing VM-internal changes were reported, not
19//! forced: `__name` honored pre-5.3 (lives in `obj_type_name_cow`) and the
20//! 5.1/5.2 `'?'`/`'_G.'` arg-error function-name resolution.
21
22use crate::state_stub::{LuaState, LuaStateStubExt as _};
23use lua_types::{closure::LuaClosure, error::LuaError, value::LuaValue, LuaStatus, LuaType};
24
25// ── Module-level constants ────────────────────────────────────────────────────
26
27/// ASCII whitespace characters used by `b_str2int` for strspn-style skipping.
28const SPACECHARS: &[u8] = b" \x0c\n\r\t\x0b";
29
30/// Reserved stack slot used by `generic_reader` to anchor the current chunk
31/// string so it is not collected while `lua_load` is running.
32const RESERVED_SLOT: i32 = 5;
33
34/// Name of the global environment table stored as a global itself.
35const LUA_GNAME: &[u8] = b"_G";
36
37/// Sentinel indicating "all return values" for call/pcall helpers.
38const LUA_MULTRET: i32 = -1;
39
40// ── GC operation codes ────────────────────────────────────────────────────────
41
42/// Identifies a GC control operation passed to the `collectgarbage` built-in.
43/// The discriminants are the integer codes the `lua-vm` GC API accepts.
44#[repr(i32)]
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46enum GcOp {
47 Stop = 0,
48 Restart = 1,
49 Collect = 2,
50 Count = 3,
51 #[expect(
52 dead_code,
53 reason = "ported stdlib helper; not yet wired into the runtime"
54 )]
55 CountB = 4,
56 Step = 5,
57 SetPause = 6,
58 SetStepMul = 7,
59 IsRunning = 9,
60 Gen = 10,
61 Inc = 11,
62 Param = 12,
63}
64
65// ── LuaState forward declaration ─────────────────────────────────────────────
66
67// LuaState is provided by crate::state_stub.
68
69// ── Type alias for standard Lua-callable functions ────────────────────────────
70
71/// Rust equivalent of `lua_CFunction`: a bare function that receives the
72/// interpreter state and returns a count of pushed results.
73pub(crate) type LuaLibFn = fn(&mut LuaState) -> Result<usize, LuaError>;
74
75// ── Helper: push_mode ─────────────────────────────────────────────────────────
76
77/// Push the GC mode string ("incremental" or "generational") onto the stack,
78/// or push `nil` (fail) when `oldmode == -1` (invalid call inside a finalizer).
79///
80fn push_mode(state: &mut LuaState, oldmode: i32) -> Result<usize, LuaError> {
81 if oldmode == -1 {
82 state.push(LuaValue::Nil);
83 } else {
84 let s: &[u8] = if oldmode == GcOp::Inc as i32 {
85 b"incremental"
86 } else {
87 b"generational"
88 };
89 state.push_string(s)?;
90 }
91 Ok(1)
92}
93
94/// Push the result of `collectgarbage("generational"|"incremental")`.
95///
96/// 5.4/5.5 return the previous mode as a STRING name (`"incremental"` /
97/// `"generational"`) via [`push_mode`]. 5.2 — the only pre-5.4 family that
98/// accepts these options — instead returns the previous mode as the INTEGER 0
99/// (`lua_pushinteger(L, lua_gc(...))` in lua5.2.4's `lbaselib.c`, where the GC
100/// mode is the integer constant `0`). The version that owns the running state
101/// selects the form.
102fn push_gc_mode(
103 state: &mut LuaState,
104 version: lua_types::LuaVersion,
105 oldmode: i32,
106) -> Result<usize, LuaError> {
107 if matches!(version, lua_types::LuaVersion::V52) {
108 state.push(LuaValue::Int(0));
109 return Ok(1);
110 }
111 push_mode(state, oldmode)
112}
113
114// ── Helper: finish_pcall ──────────────────────────────────────────────────────
115
116/// Shared result-adjustment logic for `pcall` and `xpcall`.
117///
118/// On success: returns the count of values already on the stack minus `extra`
119/// skipped sentinel values. On failure: replaces whatever is on the stack
120/// with `[false, error_message]` and returns 2.
121///
122fn finish_pcall(state: &mut LuaState, ok: bool, extra: i32) -> Result<usize, LuaError> {
123 if !ok {
124 state.push(LuaValue::Bool(false));
125 state.push_copy(-2)?;
126 return Ok(2);
127 }
128 Ok((state.top() as i32 - extra) as usize)
129}
130
131// ── Helper: b_str2int ─────────────────────────────────────────────────────────
132
133/// Parse an integer in an arbitrary base from the byte slice `s`.
134///
135/// Returns `Some((consumed, value))` on success, where `consumed` is the number
136/// of bytes from the start of `s` that were processed (leading and trailing
137/// ASCII whitespace included). Returns `None` when the slice contains no valid
138/// numeral in `base`.
139///
140/// The caller checks `consumed == s.len()` to verify the whole string was used.
141///
142fn b_str2int(s: &[u8], base: u32) -> Option<(usize, i64)> {
143 let mut pos = 0usize;
144 while pos < s.len() && SPACECHARS.contains(&s[pos]) {
145 pos += 1;
146 }
147 let neg = if pos < s.len() && s[pos] == b'-' {
148 pos += 1;
149 true
150 } else {
151 if pos < s.len() && s[pos] == b'+' {
152 pos += 1;
153 }
154 false
155 };
156 if pos >= s.len() || !s[pos].is_ascii_alphanumeric() {
157 return None;
158 }
159 let mut n: u64 = 0u64;
160 loop {
161 let byte = s[pos];
162 let digit = if byte.is_ascii_digit() {
163 (byte - b'0') as u32
164 } else {
165 (byte.to_ascii_uppercase() - b'A') as u32 + 10
166 };
167 if digit >= base {
168 return None;
169 }
170 n = n.wrapping_mul(base as u64).wrapping_add(digit as u64);
171 pos += 1;
172 if pos >= s.len() || !s[pos].is_ascii_alphanumeric() {
173 break;
174 }
175 }
176 while pos < s.len() && SPACECHARS.contains(&s[pos]) {
177 pos += 1;
178 }
179 let value: i64 = if neg {
180 0u64.wrapping_sub(n) as i64
181 } else {
182 n as i64
183 };
184 Some((pos, value))
185}
186
187// ── Helper: load_aux ──────────────────────────────────────────────────────────
188
189/// Shared post-load logic for `load` and `loadfile`.
190///
191/// On success (status_ok == true): optionally installs an environment upvalue,
192/// then returns 1 (the chunk function is on the stack).
193/// On failure: pushes nil then moves it before the error message, returns 2.
194///
195fn load_aux(state: &mut LuaState, status_ok: bool, envidx: i32) -> Result<usize, LuaError> {
196 if status_ok {
197 if envidx != 0 {
198 state.push_copy(envidx)?;
199 if state.set_upvalue(-2, 1)?.is_none() {
200 state.pop_n(1);
201 }
202 }
203 Ok(1)
204 } else {
205 state.push(LuaValue::Nil);
206 state.insert(-2)?;
207 Ok(2)
208 }
209}
210
211fn check_load_mode(state: &mut LuaState, idx: i32, default: &[u8]) -> Result<Vec<u8>, LuaError> {
212 let mode = state.opt_arg_string(idx, default)?;
213 if matches!(state.global().lua_version, lua_types::LuaVersion::V55) && mode.contains(&b'B') {
214 return Err(lua_vm::debug::arg_error_impl(state, idx, b"invalid mode"));
215 }
216 Ok(mode)
217}
218
219// ── print ─────────────────────────────────────────────────────────────────────
220
221/// Converts each argument to a string, separates them with tabs, writes them to
222/// standard output, and finishes with a newline.
223///
224/// The conversion mechanism is a genuine cross-version split:
225///
226/// - Lua 5.1/5.2/5.3 `luaB_print` fetch the **global** `tostring` and *call* it
227/// on each argument. Redefining global `tostring` therefore changes `print`,
228/// a `nil` global makes `print` raise `attempt to call a nil value`, and a
229/// result that is neither a string nor a coercible number raises
230/// `'tostring' must return a string to 'print'`.
231/// - Lua 5.4/5.5 `luaB_print` use `luaL_tolstring` directly: it honors the
232/// `__tostring` / `__name` metafields but ignores the global `tostring`.
233///
234pub(crate) fn print_fn(state: &mut LuaState) -> Result<usize, LuaError> {
235 let calls_global_tostring = matches!(
236 state.global().lua_version,
237 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
238 );
239 if calls_global_tostring {
240 return print_via_global_tostring(state);
241 }
242 let n = state.top();
243 for i in 1..=n {
244 let bytes = state.to_display_string(i)?;
245 if i > 1 {
246 state.write_output(b"\t")?;
247 }
248 state.write_output(&bytes)?;
249 state.pop_n(1);
250 }
251 state.write_output(b"\n")?;
252 Ok(0)
253}
254
255/// Implements the Lua 5.1/5.2/5.3 `print` behavior (`luaB_print`): fetch the
256/// global `tostring` once, then call it on each argument.
257///
258fn print_via_global_tostring(state: &mut LuaState) -> Result<usize, LuaError> {
259 let n = state.top();
260 lua_vm::api::get_global(state, b"tostring")?;
261 for i in 1..=n {
262 state.push_copy(-1)?;
263 state.push_copy(i)?;
264 state.call(1, 1)?;
265 // lua_tolstring returns NULL for anything that is neither a string nor a
266 // coercible number; the reference raises in that case.
267 if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
268 return Err(state.where_error(1, b"'tostring' must return a string to 'print'"));
269 }
270 let bytes = state
271 .to_lua_string_bytes(-1)
272 .expect("string/number coerces to bytes");
273 if i > 1 {
274 state.write_output(b"\t")?;
275 }
276 state.write_output(&bytes)?;
277 state.pop_n(1);
278 }
279 state.write_output(b"\n")?;
280 Ok(0)
281}
282
283// ── warn ──────────────────────────────────────────────────────────────────────
284
285/// Validates that every argument is a string, then forwards them as a
286/// multi-part warning message via the state's warning hook.
287///
288pub(crate) fn warn_fn(state: &mut LuaState) -> Result<usize, LuaError> {
289 let n = state.top();
290 state.check_arg_string(1)?;
291 for i in 2..=n {
292 state.check_arg_string(i)?;
293 }
294 for i in 1..n {
295 // Clone bytes before further mutation to avoid borrow conflict.
296 // PORTING.md §8: "No &LuaValue across a stack-mutating call."
297 let s: Vec<u8> = state
298 .to_lua_string_bytes(i)
299 .map(|b| b.to_vec())
300 .unwrap_or_default();
301 // continue = true (1) — more parts follow
302 state.warning(&s, true)?;
303 }
304 let s: Vec<u8> = state
305 .to_lua_string_bytes(n)
306 .map(|b| b.to_vec())
307 .unwrap_or_default();
308 state.warning(&s, false)?;
309 Ok(0)
310}
311
312// ── tonumber ──────────────────────────────────────────────────────────────────
313
314/// Converts a value to a number, optionally in a given numeric base (2–36).
315///
316pub(crate) fn tonumber_fn(state: &mut LuaState) -> Result<usize, LuaError> {
317 if matches!(state.type_at(2), LuaType::None | LuaType::Nil) {
318 if state.type_at(1) == LuaType::Number {
319 lua_vm::api::set_top(state, 1)?;
320 return Ok(1);
321 }
322 // lua_stringtonumber returns bytes consumed including the NUL terminator,
323 // so success iff consumed == string_length + 1.
324 if let Some(len) = state.to_lua_string_len(1) {
325 if let Some(consumed) = state.string_to_number(1) {
326 if consumed == len + 1 {
327 return Ok(1);
328 }
329 }
330 }
331 state.check_arg_any(1)?;
332 } else {
333 let base = state.check_arg_integer(2)?;
334 state.check_arg_type(1, LuaType::String)?;
335 // Clone before further state ops (PORTING.md §8).
336 let bytes: Vec<u8> = state
337 .to_lua_string_bytes(1)
338 .map(|b| b.to_vec())
339 .unwrap_or_default();
340 if !(2..=36).contains(&base) {
341 return Err(lua_vm::debug::arg_error_impl(
342 state,
343 2,
344 b"base out of range",
345 ));
346 }
347 if let Some((consumed, n)) = b_str2int(&bytes, base as u32) {
348 if consumed == bytes.len() {
349 state.push(LuaValue::Int(n));
350 return Ok(1);
351 }
352 }
353 }
354 state.push(LuaValue::Nil);
355 Ok(1)
356}
357
358// ── error ─────────────────────────────────────────────────────────────────────
359
360/// Raises the value at stack[1] as a Lua error, optionally prepending
361/// source-location information for string errors when `level > 0`.
362///
363pub(crate) fn error_fn(state: &mut LuaState) -> Result<usize, LuaError> {
364 let level = state.opt_arg_integer(2, 1)? as i32;
365 lua_vm::api::set_top(state, 1)?;
366 let ty = state.type_at(1);
367 // 5.1/5.2 prepend the `luaL_where` location to a string OR a number error
368 // value (their guard is `lua_isstring`, which is true for numbers since
369 // numbers coerce to strings); `lua_concat` then stringifies the number. 5.3
370 // tightened this to strict strings only (`ttisstring`), so a number error is
371 // re-raised unchanged. 5.4 is the unchangeable baseline; the number branch is
372 // gated to the legacy family.
373 let legacy_number_prefix = matches!(
374 state.global().lua_version,
375 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
376 ) && ty == LuaType::Number;
377 if (ty == LuaType::String || legacy_number_prefix) && level > 0 {
378 state.push_where(level)?;
379 state.push_copy(1)?;
380 state.concat(2)?;
381 }
382 Err(LuaError::from_value(state.pop()))
383}
384
385// ── getmetatable ──────────────────────────────────────────────────────────────
386
387/// Returns the metatable of the first argument, or the `__metatable` field of
388/// the metatable if that field exists (protecting the raw metatable).
389///
390pub(crate) fn getmetatable_fn(state: &mut LuaState) -> Result<usize, LuaError> {
391 state.check_arg_any(1)?;
392 if !state.get_metatable(1)? {
393 state.push(LuaValue::Nil);
394 return Ok(1);
395 }
396 // Returns LuaType::Nil if metatable has no __metatable; otherwise pushes it.
397 state.get_metafield(1, b"__metatable")?;
398 Ok(1)
399}
400
401// ── setmetatable ──────────────────────────────────────────────────────────────
402
403/// Sets the metatable of the table at argument 1 to the value at argument 2
404/// (nil clears it). Raises an error if the current metatable is protected via
405/// `__metatable`.
406///
407pub(crate) fn setmetatable_fn(state: &mut LuaState) -> Result<usize, LuaError> {
408 let t = state.type_at(2);
409 state.check_arg_type(1, LuaType::Table)?;
410 if !(t == LuaType::Nil || t == LuaType::Table) {
411 let got = state.value_at(2);
412 return Err(LuaError::type_arg_error(2, "nil or table", &got));
413 }
414 if state.get_metafield(1, b"__metatable")? != LuaType::Nil {
415 return Err(LuaError::runtime(format_args!(
416 "cannot change a protected metatable"
417 )));
418 }
419 lua_vm::api::set_top(state, 2)?;
420 state.set_metatable(1)?;
421 Ok(1)
422}
423
424// ── rawequal ──────────────────────────────────────────────────────────────────
425
426/// Raw equality check (no metamethods).
427///
428pub(crate) fn rawequal_fn(state: &mut LuaState) -> Result<usize, LuaError> {
429 state.check_arg_any(1)?;
430 state.check_arg_any(2)?;
431 let eq = state.raw_equal(1, 2)?;
432 state.push(LuaValue::Bool(eq));
433 Ok(1)
434}
435
436// ── rawlen ────────────────────────────────────────────────────────────────────
437
438/// Raw length (#) without metamethods; accepts tables and strings only.
439///
440/// The reject message names the function (`to 'rawlen'`) on every version that
441/// has `rawlen` (5.2+). The `, got <type>` suffix is version-gated: 5.2/5.3 use
442/// `luaL_argcheck(..., "table or string expected")` (no suffix); 5.4/5.5 use
443/// `luaL_argexpected(..., "table or string")`, which appends `, got <type>`
444/// from `luaL_typename` (so an `__name`'d table reports its `__name`).
445pub(crate) fn rawlen_fn(state: &mut LuaState) -> Result<usize, LuaError> {
446 let t = state.type_at(1);
447 if !(t == LuaType::Table || t == LuaType::String) {
448 let extramsg: Vec<u8> = if matches!(state.global().lua_version, lua_types::LuaVersion::V54 | lua_types::LuaVersion::V55) {
449 let got = state.value_at(1);
450 let got_name = state.full_type_name(&got)?;
451 let mut m = b"table or string expected, got ".to_vec();
452 m.extend_from_slice(&got_name);
453 m
454 } else {
455 b"table or string expected".to_vec()
456 };
457 return Err(lua_vm::debug::arg_error_impl(state, 1, &extramsg));
458 }
459 let len = state.raw_len(1);
460 state.push(LuaValue::Int(len));
461 Ok(1)
462}
463
464// ── rawget ────────────────────────────────────────────────────────────────────
465
466/// Raw table read (no metamethods).
467///
468pub(crate) fn rawget_fn(state: &mut LuaState) -> Result<usize, LuaError> {
469 state.check_arg_type(1, LuaType::Table)?;
470 state.check_arg_any(2)?;
471 lua_vm::api::set_top(state, 2)?;
472 state.raw_get(1)?;
473 Ok(1)
474}
475
476// ── rawset ────────────────────────────────────────────────────────────────────
477
478/// Raw table write (no metamethods).
479///
480pub(crate) fn rawset_fn(state: &mut LuaState) -> Result<usize, LuaError> {
481 state.check_arg_type(1, LuaType::Table)?;
482 state.check_arg_any(2)?;
483 state.check_arg_any(3)?;
484 lua_vm::api::set_top(state, 3)?;
485 state.raw_set(1)?;
486 Ok(1)
487}
488
489// ── collectgarbage ────────────────────────────────────────────────────────────
490
491/// Expose GC control to Lua scripts. The first argument selects the operation;
492/// subsequent arguments are operation-specific parameters.
493///
494/// A GC primitive that returns `-1` was called inside a finalizer and must
495/// `fail` (push `nil`). Each match arm either returns its result early or
496/// evaluates to the `valid` flag `false`, which falls through to the trailing
497/// pushfail — the structured-control-flow form of C's `checkvalres` break.
498pub(crate) fn collectgarbage_fn(state: &mut LuaState) -> Result<usize, LuaError> {
499 // Explicit collections bypass the checkpoint wrappers, so the dead
500 // stack slices must be cleared here before any collect dispatch
501 // (C parity: traversethread's atomic clear; see #140 / GC_ROOTS.md).
502 state.gc_clear_dead_stack_tails();
503 // The option set is version-gated. 5.4/5.3 expose `setpause`/`setstepmul`;
504 // 5.5 removed both and added `param` (lbaselib.c). The version that owns
505 // the running state decides which list/mapping applies.
506 let version = state.global().lua_version;
507 let is_v55 = version == lua_types::LuaVersion::V55;
508 // Lua 5.1's `collectgarbage` accepts only `collect/stop/restart/count/step/
509 // setpause/setstepmul`; the 5.2 `isrunning`/`generational`, the 5.4
510 // `incremental`, and the 5.5 `param` must be rejected with `invalid option`.
511 // Verified against lua5.1.5: `collectgarbage("isrunning")` errors. (5.2 DOES
512 // accept `isrunning`/`generational`, so it stays on OPTS_54.) See
513 // specs/followup/5.1-roster-syntax.md §1.
514 static OPTS_51: &[&[u8]] = &[
515 b"stop",
516 b"restart",
517 b"collect",
518 b"count",
519 b"step",
520 b"setpause",
521 b"setstepmul",
522 ];
523 static OPTS_NUM_51: &[GcOp] = &[
524 GcOp::Stop,
525 GcOp::Restart,
526 GcOp::Collect,
527 GcOp::Count,
528 GcOp::Step,
529 GcOp::SetPause,
530 GcOp::SetStepMul,
531 ];
532 // 5.2 accepts `generational`/`incremental` (both return the PREVIOUS GC mode
533 // as the integer 0 — there is no string mode name pre-5.4) and `isrunning`,
534 // but NOT 5.3's narrower roster. 5.3 removed `generational`/`incremental`
535 // entirely (they raise `invalid option`), keeping only the incremental knobs.
536 // Verified by probing lua5.2.4 / lua5.3.6 (`specs/followup` GC roster). The
537 // 5.2-only `setmajorinc` is a generational-GC param this reused incremental
538 // core does not carry, so it is left out of scope here.
539 static OPTS_52: &[&[u8]] = &[
540 b"stop",
541 b"restart",
542 b"collect",
543 b"count",
544 b"step",
545 b"setpause",
546 b"setstepmul",
547 b"isrunning",
548 b"generational",
549 b"incremental",
550 ];
551 static OPTS_NUM_52: &[GcOp] = &[
552 GcOp::Stop,
553 GcOp::Restart,
554 GcOp::Collect,
555 GcOp::Count,
556 GcOp::Step,
557 GcOp::SetPause,
558 GcOp::SetStepMul,
559 GcOp::IsRunning,
560 GcOp::Gen,
561 GcOp::Inc,
562 ];
563 static OPTS_53: &[&[u8]] = &[
564 b"stop",
565 b"restart",
566 b"collect",
567 b"count",
568 b"step",
569 b"setpause",
570 b"setstepmul",
571 b"isrunning",
572 ];
573 static OPTS_NUM_53: &[GcOp] = &[
574 GcOp::Stop,
575 GcOp::Restart,
576 GcOp::Collect,
577 GcOp::Count,
578 GcOp::Step,
579 GcOp::SetPause,
580 GcOp::SetStepMul,
581 GcOp::IsRunning,
582 ];
583 static OPTS_54: &[&[u8]] = &[
584 b"stop",
585 b"restart",
586 b"collect",
587 b"count",
588 b"step",
589 b"setpause",
590 b"setstepmul",
591 b"isrunning",
592 b"generational",
593 b"incremental",
594 ];
595 static OPTS_NUM_54: &[GcOp] = &[
596 GcOp::Stop,
597 GcOp::Restart,
598 GcOp::Collect,
599 GcOp::Count,
600 GcOp::Step,
601 GcOp::SetPause,
602 GcOp::SetStepMul,
603 GcOp::IsRunning,
604 GcOp::Gen,
605 GcOp::Inc,
606 ];
607 static OPTS_55: &[&[u8]] = &[
608 b"stop",
609 b"restart",
610 b"collect",
611 b"count",
612 b"step",
613 b"isrunning",
614 b"generational",
615 b"incremental",
616 b"param",
617 ];
618 static OPTS_NUM_55: &[GcOp] = &[
619 GcOp::Stop,
620 GcOp::Restart,
621 GcOp::Collect,
622 GcOp::Count,
623 GcOp::Step,
624 GcOp::IsRunning,
625 GcOp::Gen,
626 GcOp::Inc,
627 GcOp::Param,
628 ];
629 let (opts, opts_num): (&[&[u8]], &[GcOp]) = if is_v55 {
630 (OPTS_55, OPTS_NUM_55)
631 } else if matches!(version, lua_types::LuaVersion::V51) {
632 (OPTS_51, OPTS_NUM_51)
633 } else if matches!(version, lua_types::LuaVersion::V52) {
634 (OPTS_52, OPTS_NUM_52)
635 } else if matches!(version, lua_types::LuaVersion::V53) {
636 (OPTS_53, OPTS_NUM_53)
637 } else {
638 (OPTS_54, OPTS_NUM_54)
639 };
640 let idx = state.check_arg_option(1, Some(b"collect"), opts)?;
641 let op = opts_num[idx];
642
643 // Each arm either returns early on success, or evaluates to `false`
644 // (meaning checkvalres fired — fall through to pushfail).
645 let valid: bool = match op {
646 GcOp::Count => {
647 let k = state.gc_count()?;
648 let b = state.gc_count_b()?;
649 if k == -1 {
650 false
651 } else {
652 state.push(LuaValue::Float(k as f64 + b as f64 / 1024.0));
653 // 5.2 returns a SECOND result, the byte remainder `b` (0..1024)
654 // — `lua_pushinteger(L, lua_gc(L, LUA_GCCOUNTB, 0))`. 5.3 dropped
655 // it (`collectgarbage("count")` is one value there on), so the
656 // second result is gated to V52. Verified against lua5.2.4 /
657 // lua5.3.6.
658 if matches!(version, lua_types::LuaVersion::V52) {
659 state.push(LuaValue::Int(b as i64));
660 return Ok(2);
661 }
662 return Ok(1);
663 }
664 }
665 GcOp::Step => {
666 let step = state.opt_arg_integer(2, 0)? as i32;
667 let res = state.gc_step(step)?;
668 if res == -1 {
669 false
670 } else {
671 state.push(LuaValue::Bool(res != 0));
672 return Ok(1);
673 }
674 }
675 GcOp::SetPause | GcOp::SetStepMul => {
676 let p = state.opt_arg_integer(2, 0)? as i32;
677 let previous = state.gc_set_param(op as i32, p)?;
678 if previous == -1 {
679 false
680 } else {
681 state.push(LuaValue::Int(previous as i64));
682 return Ok(1);
683 }
684 }
685 GcOp::IsRunning => {
686 let res = state.gc_is_running()?;
687 state.push(LuaValue::Bool(res));
688 return Ok(1);
689 }
690 GcOp::Gen => {
691 let minormul = state.opt_arg_integer(2, 0)? as i32;
692 let majormul = state.opt_arg_integer(3, 0)? as i32;
693 let oldmode = state.gc_gen(minormul, majormul)?;
694 return push_gc_mode(state, version, oldmode);
695 }
696 GcOp::Inc => {
697 let pause = state.opt_arg_integer(2, 0)? as i32;
698 let stepmul = state.opt_arg_integer(3, 0)? as i32;
699 let stepsize = state.opt_arg_integer(4, 0)? as i32;
700 let oldmode = state.gc_inc(pause, stepmul, stepsize)?;
701 return push_gc_mode(state, version, oldmode);
702 }
703 GcOp::Param => {
704 // 5.5 collectgarbage("param", name [, value]): read or write a GC
705 // parameter, always returning the OLD integer value. arg2 selects
706 // the param; arg3 (default -1 = read-only) is the new value.
707 static PARAMS: &[&[u8]] = &[
708 b"minormul",
709 b"majorminor",
710 b"minormajor",
711 b"pause",
712 b"stepmul",
713 b"stepsize",
714 ];
715 let pidx = state.check_arg_option(2, None, PARAMS)?;
716 let value = state.opt_arg_integer(3, -1)?;
717 let old = state.gc_param(pidx, value)?;
718 state.push(LuaValue::Int(old));
719 return Ok(1);
720 }
721 _ => {
722 let res = state.gc_control_simple(op as i32)?;
723 if res == -1 {
724 false
725 } else {
726 state.push(LuaValue::Int(res as i64));
727 return Ok(1);
728 }
729 }
730 };
731 debug_assert!(
732 !valid,
733 "valid arms return early; reaching here means checkvalres fired"
734 );
735 state.push(LuaValue::Nil);
736 Ok(1)
737}
738
739// ── type ──────────────────────────────────────────────────────────────────────
740
741/// Returns the type name of its argument as a string.
742///
743pub(crate) fn type_fn(state: &mut LuaState) -> Result<usize, LuaError> {
744 let t = state.type_at(1);
745 if t == LuaType::None {
746 return Err(lua_vm::debug::arg_error_impl(state, 1, b"value expected"));
747 }
748 // Clone the bytes before the push to avoid borrow conflict with state.
749 let name: Vec<u8> = state.type_name(t).to_vec();
750 state.push_string(&name)?;
751 Ok(1)
752}
753
754// ── getfenv / setfenv (Lua 5.1 fenv globals) ──────────────────────────────────
755
756/// Truncate a numeric `getfenv`/`setfenv` level toward zero.
757///
758/// 5.1's `luaL_checkint` casts `lua_Number` to a C `int`, truncating toward
759/// zero, so `getfenv(1.9)` is level 1 and `getfenv(-0.5)` is level 0. Under the
760/// float-only V51 model every number arrives as a `Float`; the `Int` arm is a
761/// defensive no-op. A non-number never reaches this helper.
762fn fenv_level(v: &LuaValue) -> i64 {
763 match v {
764 LuaValue::Float(f) => f.trunc() as i64,
765 LuaValue::Int(i) => *i,
766 _ => 0,
767 }
768}
769
770/// Resolve the function value targeted by a `getfenv`/`setfenv` first argument.
771///
772/// Returns the `LuaValue::Function` whose environment is being read or written.
773/// `arg1` is interpreted exactly as Lua 5.1's `getfunc`/`setfunc`
774/// (lbaselib.c): a function value targets that function directly; a number is a
775/// stack *level* (floored toward zero), where level 1 is the function calling
776/// `getfenv`/`setfenv`. Level 0 is handled by the callers (it denotes the
777/// running thread's global table, not a function) and never reaches here.
778///
779/// Errors mirror lua5.1.5:
780/// - negative level → `level must be non-negative`
781/// - level past the stack → `invalid level`
782/// - neither number nor function → `number expected, got <type>`
783fn fenv_getfunc(state: &mut LuaState, level: i64) -> Result<LuaValue, LuaError> {
784 if level < 0 {
785 return Err(lua_vm::debug::arg_error_impl(
786 state,
787 1,
788 b"level must be non-negative",
789 ));
790 }
791 let mut ar = lua_vm::debug::LuaDebug::default();
792 if !lua_vm::debug::get_stack(state, level as i32, &mut ar) {
793 return Err(lua_vm::debug::arg_error_impl(state, 1, b"invalid level"));
794 }
795 let ci_idx = ar
796 .i_ci
797 .ok_or_else(|| lua_vm::debug::arg_error_impl(state, 1, b"invalid level"))?;
798 if state.global().lua_version == lua_types::LuaVersion::V51 && state.is_base_ci(ci_idx) {
799 return Err(LuaError::runtime(format_args!(
800 "no function environment for tail call at level {}",
801 level
802 )));
803 }
804 let func_slot = state.get_ci(ci_idx).func;
805 Ok(state.get_at(func_slot))
806}
807
808/// Index of a Lua closure's `_ENV` upvalue, by upvalue name.
809///
810/// The reused modern parser threads an upvalue literally named `_ENV` and
811/// resolves every free (global) name through it; under V51 that upvalue *is* the
812/// function environment. It is NOT always upvalue 0 — a nested closure that
813/// captures locals places those first, with `_ENV` at a later index — so it must
814/// be located by name, not position. A closure that references no free names has
815/// no `_ENV` upvalue and returns `None`.
816fn fenv_env_upval_index(
817 lcl: &lua_types::gc::GcRef<lua_types::closure::LuaLClosure>,
818) -> Option<usize> {
819 lcl.proto
820 .upvalues
821 .iter()
822 .position(|ud| ud.name.as_ref().map(|s| s.as_bytes()) == Some(b"_ENV"))
823}
824
825/// Read the environment of a resolved function value.
826///
827/// A Lua closure's environment is its `_ENV` upvalue. A Lua closure that
828/// references no globals has no `_ENV` upvalue; its environment lives in the
829/// `closure_envs` side map once `setfenv` has set one, otherwise it has never
830/// been given a distinct environment and resolves to the running thread's
831/// global table. A C/Rust function likewise reports the thread global table —
832/// the common 5.1 case and the documented `LUA_ENVIRONINDEX` gap
833/// (specs/followup/5.1-fenv.md §4).
834fn fenv_read(state: &LuaState, func: &LuaValue) -> LuaValue {
835 if let LuaValue::Function(LuaClosure::Lua(lcl)) = func {
836 if let Some(idx) = fenv_env_upval_index(lcl) {
837 return state.upvalue_get(lcl, idx);
838 }
839 if let Some(env) = state.global().closure_envs.get(&lcl.identity()) {
840 return env.clone();
841 }
842 }
843 let running = state.global().current_thread_id;
844 state.v51_thread_lgt(running)
845}
846
847/// Set the environment of a Lua closure that carries no `_ENV` upvalue.
848///
849/// Such a closure (the modern parser threads `_ENV` only onto closures that
850/// reference a free global name) has no upvalue slot to write, so 5.1's
851/// `setfenv` stores its environment in the `closure_envs` side map keyed by
852/// closure identity. A closure that *does* have an `_ENV` upvalue is handled by
853/// the upvalue-cell path and never reaches here.
854fn fenv_set_closure_env(
855 state: &mut LuaState,
856 lcl: &lua_types::gc::GcRef<lua_types::closure::LuaLClosure>,
857 new_env: LuaValue,
858) {
859 state
860 .global_mut()
861 .closure_envs
862 .insert(lcl.identity(), new_env);
863}
864
865/// `getfenv([f])` — Lua 5.1 only.
866///
867/// Returns the environment of the function `f` (a function value or a stack
868/// level), or the running function's environment when the argument is absent,
869/// `nil`, or `1`. 5.1's `getfunc` resolves the level via `luaL_optint(L, 1, 1)`,
870/// which defaults both an absent and an explicit `nil` argument to level 1.
871/// Level `0` returns the running thread's global table. See
872/// `specs/followup/5.1-fenv.md` §2.
873pub(crate) fn getfenv_fn(state: &mut LuaState) -> Result<usize, LuaError> {
874 let arg1 = state.value_at(1);
875 let func = match &arg1 {
876 LuaValue::Function(_) => arg1.clone(),
877 LuaValue::Nil => fenv_getfunc(state, 1)?,
878 LuaValue::Float(_) | LuaValue::Int(_) => {
879 let level = fenv_level(&arg1);
880 if level == 0 {
881 let running = state.global().current_thread_id;
882 let lgt = state.v51_thread_lgt(running);
883 state.push(lgt);
884 return Ok(1);
885 }
886 fenv_getfunc(state, level)?
887 }
888 other => {
889 let got = state.obj_type_name(other);
890 let msg = format!("number expected, got {}", String::from_utf8_lossy(&got));
891 return Err(lua_vm::debug::arg_error_impl(state, 1, msg.as_bytes()));
892 }
893 };
894 let env = fenv_read(state, &func);
895 state.push(env);
896 Ok(1)
897}
898
899/// `setfenv(f, table)` — Lua 5.1 only.
900///
901/// Sets the environment of the function `f` (a function value or a stack level)
902/// to `table`. `setfenv(0, t)` sets the running thread's global table. Returns
903/// the affected function (or the running thread for level 0). A C/Rust function
904/// (or any non-Lua object) cannot have its environment changed and raises,
905/// matching lua5.1.5. See `specs/followup/5.1-fenv.md` §2.
906pub(crate) fn setfenv_fn(state: &mut LuaState) -> Result<usize, LuaError> {
907 state.check_arg_type(2, LuaType::Table)?;
908 let new_env = state.value_at(2);
909
910 let arg1 = state.value_at(1);
911 let is_level_zero =
912 matches!(&arg1, LuaValue::Int(0)) || matches!(&arg1, LuaValue::Float(f) if *f == 0.0);
913 if is_level_zero {
914 // Level 0: replace the *running thread's* global table (5.1's
915 // per-thread `l_gt`) and return the running thread. Subsequently
916 // loaded top-level chunks take this env. From inside a coroutine this
917 // touches only that coroutine's `l_gt`, never the main thread's
918 // globals.
919 let running = state.global().current_thread_id;
920 state.v51_set_thread_lgt(running, new_env);
921 lua_vm::api::push_thread(state);
922 return Ok(1);
923 }
924
925 let func = match &arg1 {
926 LuaValue::Function(_) => arg1.clone(),
927 LuaValue::Float(_) | LuaValue::Int(_) => {
928 let level = fenv_level(&arg1);
929 fenv_getfunc(state, level)?
930 }
931 other => {
932 let got = state.obj_type_name(other);
933 let msg = format!("number expected, got {}", String::from_utf8_lossy(&got));
934 return Err(lua_vm::debug::arg_error_impl(state, 1, msg.as_bytes()));
935 }
936 };
937
938 match &func {
939 LuaValue::Function(LuaClosure::Lua(lcl)) => {
940 if let Some(idx) = fenv_env_upval_index(lcl) {
941 // Give the closure a PRIVATE environment: replace its `_ENV`
942 // upvalue *cell* with a fresh closed upvalue holding `new_env`.
943 // Mutating the existing cell's value (`upvalue_set`) would alter
944 // every closure sharing that upvalue (e.g. the main chunk's
945 // `_G`), which is wrong — `setfenv(f, e)` must not change the
946 // caller's globals. A new cell isolates `f`.
947 let uv = state.new_upval_closed(new_env);
948 lcl.set_upval(idx, uv);
949 state.gc().obj_barrier(lcl, &uv);
950 } else {
951 // A Lua closure that references no free global name has no
952 // `_ENV` upvalue, so there is no upvalue cell to write. 5.1
953 // still sets its environment; store it in the `closure_envs`
954 // side map keyed by closure identity, where `getfenv(f)` /
955 // `getfenv(level)` reads it back.
956 let lcl = *lcl;
957 fenv_set_closure_env(state, &lcl, new_env);
958 }
959 }
960 _ => {
961 // C/Rust functions cannot have their environment changed. 5.1
962 // raises this exact message (via luaL_error, so it carries the
963 // caller's source location) for any object whose env is fixed.
964 return Err(
965 state.where_error(1, b"'setfenv' cannot change environment of given object")
966 );
967 }
968 }
969 state.push(func);
970 Ok(1)
971}
972
973/// Set the environment of the Lua closure `level` frames up the running stack
974/// to `new_env`, the internal equivalent of `setfenv(level, new_env)`.
975///
976/// Used by `module` (5.1 `package` library), which sets its caller's
977/// environment to the module table. A non-Lua function (or a closure with no
978/// `_ENV` upvalue) is left unchanged, matching the inert-set behavior of
979/// `setfenv`. See specs/followup/5.1-fenv.md.
980#[cfg(feature = "package")]
981pub(crate) fn set_func_env_at_level(
982 state: &mut LuaState,
983 level: i64,
984 new_env: LuaValue,
985) -> Result<(), LuaError> {
986 let func = fenv_getfunc(state, level)?;
987 if let LuaValue::Function(LuaClosure::Lua(lcl)) = &func {
988 if let Some(idx) = fenv_env_upval_index(lcl) {
989 let uv = state.new_upval_closed(new_env);
990 lcl.set_upval(idx, uv);
991 state.gc().obj_barrier(lcl, &uv);
992 } else {
993 let lcl = *lcl;
994 fenv_set_closure_env(state, &lcl, new_env);
995 }
996 }
997 Ok(())
998}
999
1000/// `debug.getfenv(o)` — Lua 5.1 only.
1001///
1002/// Returns the environment of object `o` *directly* (`db_getfenv` =
1003/// `luaL_checkany; lua_getfenv`). Unlike the global `getfenv`, the argument is
1004/// the object itself, never a stack level: `debug.getfenv(1)` returns `nil`
1005/// because the number 1 has no environment. A function returns its `_ENV`
1006/// environment; a value with no environment returns `nil`. Absent argument
1007/// raises `value expected`.
1008///
1009/// Gap: 5.1 userdata/thread environments live in fields this reused modern core
1010/// does not expose, so those return `nil` here rather than their stored table.
1011/// The common function/non-function cases match lua5.1.5.
1012#[cfg(feature = "debug")]
1013pub(crate) fn debug_getfenv_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1014 if state.type_at(1) == LuaType::None {
1015 return Err(lua_vm::debug::arg_error_impl(state, 1, b"value expected"));
1016 }
1017 let obj = state.value_at(1);
1018 match &obj {
1019 LuaValue::Function(_) => {
1020 let env = fenv_read(state, &obj);
1021 state.push(env);
1022 }
1023 LuaValue::Thread(th) => {
1024 // A thread's environment is its per-thread global table (`l_gt`):
1025 // `debug.getfenv(co)` returns the global table that `co`'s freshly
1026 // loaded chunks and `getfenv(0)` see (closure.lua@5.1).
1027 let lgt = state.v51_thread_lgt(th.id);
1028 state.push(lgt);
1029 }
1030 _ => {
1031 state.push(LuaValue::Nil);
1032 }
1033 }
1034 Ok(1)
1035}
1036
1037/// `debug.setfenv(o, t)` — Lua 5.1 only.
1038///
1039/// Sets object `o`'s environment to table `t` and returns `o` (`db_setfenv` =
1040/// `luaL_checktype(2, TABLE); lua_setfenv`). For a Lua closure this installs a
1041/// fresh closed `_ENV` upvalue cell (the same private-environment isolation
1042/// `setfenv` uses). An object whose environment cannot be set raises
1043/// `'setfenv' cannot change environment of given object`.
1044#[cfg(feature = "debug")]
1045pub(crate) fn debug_setfenv_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1046 state.check_arg_type(2, LuaType::Table)?;
1047 let new_env = state.value_at(2);
1048 let obj = state.value_at(1);
1049 match &obj {
1050 LuaValue::Function(LuaClosure::Lua(lcl)) => {
1051 if let Some(idx) = fenv_env_upval_index(lcl) {
1052 let uv = state.new_upval_closed(new_env);
1053 lcl.set_upval(idx, uv);
1054 state.gc().obj_barrier(lcl, &uv);
1055 } else {
1056 let lcl = *lcl;
1057 fenv_set_closure_env(state, &lcl, new_env);
1058 }
1059 }
1060 LuaValue::Thread(th) => {
1061 // `debug.setfenv(co, t)` sets thread `co`'s per-thread global
1062 // table (`l_gt`), the env its freshly loaded chunks and
1063 // `getfenv(0)` resolve through (closure.lua@5.1).
1064 state.v51_set_thread_lgt(th.id, new_env);
1065 }
1066 LuaValue::Function(_) => {
1067 return Err(
1068 state.where_error(1, b"'setfenv' cannot change environment of given object")
1069 );
1070 }
1071 _ => {
1072 return Err(
1073 state.where_error(1, b"'setfenv' cannot change environment of given object")
1074 );
1075 }
1076 }
1077 state.push(obj);
1078 Ok(1)
1079}
1080
1081// ── next ──────────────────────────────────────────────────────────────────────
1082
1083/// Table traversal iterator: given a table and a key, pushes the next key-value
1084/// pair. Pushes nil and returns 1 when the traversal is exhausted.
1085///
1086pub(crate) fn next_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1087 state.check_arg_type(1, LuaType::Table)?;
1088 lua_vm::api::set_top(state, 2)?;
1089 if state.table_next(1)? {
1090 Ok(2)
1091 } else {
1092 state.push(LuaValue::Nil);
1093 Ok(1)
1094 }
1095}
1096
1097// ── pairs continuation (coroutine stub) ───────────────────────────────────────
1098
1099/// Continuation for `pairs` when the `__pairs` metamethod yields.
1100/// Re-invoked by `finishCcall` after the yielded `__pairs` resumes.
1101///
1102fn pairs_cont(state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
1103 if state.global().lua_version == lua_types::LuaVersion::V55 {
1104 Ok(4)
1105 } else {
1106 Ok(3)
1107 }
1108}
1109
1110// ── pairs ─────────────────────────────────────────────────────────────────────
1111
1112/// Returns the `next` function, the table, and nil (or invokes a `__pairs`
1113/// metamethod).
1114///
1115pub(crate) fn pairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1116 state.check_arg_any(1)?;
1117 // Lua 5.1 has no `__pairs` metamethod; `pairs(t)` always iterates the raw
1118 // table even when a `__pairs` is set (it is silently ignored). Lua 5.5
1119 // extends the result list with a fourth to-be-closed object.
1120 let consult_pairs_tm = !matches!(state.global().lua_version, lua_types::LuaVersion::V51);
1121 let nresults = if state.global().lua_version == lua_types::LuaVersion::V55 {
1122 4
1123 } else {
1124 3
1125 };
1126 if !consult_pairs_tm || state.get_metafield(1, b"__pairs")? == LuaType::Nil {
1127 state.push_c_function(next_fn)?;
1128 state.push_copy(1)?;
1129 state.push(LuaValue::Nil);
1130 if nresults == 4 {
1131 state.push(LuaValue::Nil);
1132 }
1133 } else {
1134 state.push_copy(1)?;
1135 state.call_k(1, nresults as i32, 0, Some(pairs_cont))?;
1136 }
1137 Ok(nresults)
1138}
1139
1140// ── ipairs auxiliary ──────────────────────────────────────────────────────────
1141
1142/// Iterator step function for `ipairs`: increments the counter and fetches
1143/// the next array element. Returns the index + value, or just the index when
1144/// the value is nil (signalling end-of-iteration).
1145///
1146/// The element fetch is a genuine cross-version split. Lua 5.1/5.2's
1147/// `ipairsaux` reads with `lua_rawgeti` — `__index` is NOT consulted, so an
1148/// empty array part stops immediately even when an `__index` would supply
1149/// values. Lua 5.3 switched to `lua_geti`, which honors `__index`.
1150///
1151/// The split is resolved ONCE in the cold `ipairs_fn` setup, which registers
1152/// the matching specialization (`ipairs_aux_raw` for 5.1/5.2, `ipairs_aux` for
1153/// 5.3+). This per-step loop body therefore carries NO version branch — the GC
1154/// `global()` borrow stays out of the hot iteration path (cf. the string
1155/// packet's `gmatch_aux` const-split). The `RAW` const folds at monomorphization.
1156fn ipairs_step<const RAW: bool>(state: &mut LuaState) -> Result<usize, LuaError> {
1157 let i = match lua_vm::api::positive_index_value(state, 2) {
1158 LuaValue::Int(i) => i,
1159 _ => state.check_arg_integer(2)?,
1160 };
1161 let i = (i as u64).wrapping_add(1u64) as i64;
1162 state.push(LuaValue::Int(i));
1163 let t = if RAW {
1164 // 5.1/5.2: `lua_rawgeti`. The first argument is guaranteed a table
1165 // (`ipairs` type-checks it on those versions), so the raw read is safe.
1166 lua_vm::api::raw_get_i(state, 1, i)
1167 } else {
1168 let table = lua_vm::api::positive_index_value(state, 1);
1169 state.table_get_i_value(&table, i)?
1170 };
1171 if t == LuaType::Nil {
1172 Ok(1)
1173 } else {
1174 Ok(2)
1175 }
1176}
1177
1178/// 5.3+ `ipairsaux`: honors `__index` via `lua_geti`.
1179fn ipairs_aux(state: &mut LuaState) -> Result<usize, LuaError> {
1180 ipairs_step::<false>(state)
1181}
1182
1183/// 5.1/5.2 `ipairsaux`: raw `lua_rawgeti`, no `__index`.
1184fn ipairs_aux_raw(state: &mut LuaState) -> Result<usize, LuaError> {
1185 ipairs_step::<true>(state)
1186}
1187
1188// ── ipairs ────────────────────────────────────────────────────────────────────
1189
1190/// Returns the `ipairsaux` iterator, the table, and 0 as the initial counter
1191/// (or invokes an `__ipairs` metamethod on the versions that honor it).
1192///
1193/// Three cross-version seams converge here, all in this cold setup path:
1194///
1195/// - **`__ipairs` metamethod.** The `LUA_COMPAT_IPAIRS` macro (default ON in
1196/// 5.2/5.3 via `LUA_COMPAT_5_2`) routes `ipairs` through `pairsmeta`, which
1197/// calls `t.__ipairs(t)` for the iterator triple when present. 5.1 predates
1198/// `__ipairs`; 5.4/5.5 removed the compat path. Honored only on 5.2/5.3.
1199/// - **Setup type check.** 5.1's `luaB_ipairs` (and 5.2's `pairsmeta` when no
1200/// `__ipairs` is found) does `luaL_checktype(1, TABLE)` — `ipairs(non_table)`
1201/// raises at the `ipairs` call. 5.3+ relaxed this to `luaL_checkany`, so a
1202/// non-table reaches the iterator and only errors (or stops) there.
1203/// - **Raw vs `__index` read** is handled in `ipairs_aux` (5.1/5.2 raw).
1204pub(crate) fn ipairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1205 let version = state.global().lua_version;
1206 let consult_ipairs_tm = matches!(
1207 version,
1208 lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
1209 );
1210 if consult_ipairs_tm && state.get_metafield(1, b"__ipairs")? != LuaType::Nil {
1211 state.push_copy(1)?;
1212 state.call(1, 3)?;
1213 return Ok(3);
1214 }
1215 let legacy = matches!(
1216 version,
1217 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1218 );
1219 if legacy {
1220 state.check_arg_type(1, LuaType::Table)?;
1221 state.push_c_function(ipairs_aux_raw)?;
1222 } else {
1223 state.check_arg_any(1)?;
1224 state.push_c_function(ipairs_aux)?;
1225 }
1226 state.push_copy(1)?;
1227 state.push(LuaValue::Int(0));
1228 Ok(3)
1229}
1230
1231// ── loadfile ──────────────────────────────────────────────────────────────────
1232
1233/// Loads a Lua chunk from a file.
1234///
1235pub(crate) fn loadfile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1236 let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
1237 let mode: Option<Vec<u8>> = if state.is_none_or_nil(2) {
1238 None
1239 } else {
1240 Some(check_load_mode(state, 2, b"bt")?)
1241 };
1242 let env = if state.type_at(3) != LuaType::None {
1243 3
1244 } else {
1245 0
1246 };
1247 let status_ok = state.load_file_ex(fname.as_deref(), mode.as_deref())?;
1248 load_aux(state, status_ok, env)
1249}
1250
1251// ── generic_reader ────────────────────────────────────────────────────────────
1252
1253/// Reader callback for `load` when the chunk source is a Lua function.
1254///
1255/// Calls the function at stack[1] repeatedly to obtain successive chunks; a
1256/// `nil` return ends the stream and anything that is neither a string nor a
1257/// coercible number (the C `lua_isstring` test) is rejected. The latest chunk
1258/// is anchored in `RESERVED_SLOT` so the GC cannot collect it while `lua_load`
1259/// consumes it. `state.load_with_reader` drives this as the reader.
1260fn generic_reader(state: &mut LuaState) -> Result<Option<Vec<u8>>, LuaError> {
1261 state.ensure_stack(2, b"too many nested functions")?;
1262 state.push_copy(1)?;
1263 state.call(0, 1)?;
1264 if state.type_at(-1) == LuaType::Nil {
1265 state.pop_n(1);
1266 return Ok(None);
1267 }
1268 if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
1269 return Err(LuaError::runtime(format_args!(
1270 "reader function must return a string"
1271 )));
1272 }
1273 state.replace(RESERVED_SLOT)?;
1274 let bytes = state.to_lua_string_bytes(RESERVED_SLOT).map(|b| b.to_vec());
1275 Ok(bytes)
1276}
1277
1278// ── load ──────────────────────────────────────────────────────────────────────
1279
1280/// Loads a Lua chunk from a string or a reader function.
1281///
1282pub(crate) fn load_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1283 // Lua 5.1's `load` takes a *reader function only* — string loading is
1284 // `loadstring`'s job. `load("...")` errors with `function expected, got
1285 // string`. The string-or-function overload is a 5.2 addition. Verified
1286 // against lua5.1.5; see specs/followup/5.1-roster-syntax.md §1.
1287 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1288 state.check_arg_type(1, LuaType::Function)?;
1289 }
1290 // Determine whether argument 1 is a string (load from buffer) or a
1291 // function (load from reader).
1292 let is_string = matches!(state.type_at(1), LuaType::String | LuaType::Number);
1293 let mode: Vec<u8> = check_load_mode(state, 3, b"bt")?;
1294 let env = if state.type_at(4) != LuaType::None {
1295 4
1296 } else {
1297 0
1298 };
1299 let status_ok = if is_string {
1300 let chunk: Vec<u8> = state.to_lua_string_bytes(1).unwrap_or_default();
1301 let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
1302 chunk.clone()
1303 } else {
1304 state.check_arg_string(2)?
1305 };
1306 state.load_buffer_ex(&chunk, &chunkname, &mode)?
1307 } else {
1308 let chunkname: Vec<u8> = state
1309 .opt_arg_string_bytes(2)
1310 .unwrap_or_else(|_| b"=(load)".to_vec());
1311 state.check_arg_type(1, LuaType::Function)?;
1312 lua_vm::api::set_top(state, RESERVED_SLOT)?;
1313 state.load_with_reader(generic_reader, &chunkname, &mode)?
1314 };
1315 load_aux(state, status_ok, env)
1316}
1317
1318/// `loadstring(s [, chunkname])` — Lua 5.1 only.
1319///
1320/// Loads a string as a Lua chunk. In 5.1 this is the string-loading counterpart
1321/// to `load` (which takes a reader function only). The second argument is the
1322/// chunk name. Verified against lua5.1.5; see
1323/// specs/followup/5.1-roster-syntax.md §1.
1324pub(crate) fn loadstring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1325 let chunk: Vec<u8> = state.check_arg_string(1)?;
1326 let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
1327 chunk.clone()
1328 } else {
1329 state.check_arg_string(2)?
1330 };
1331 let status_ok = state.load_buffer_ex(&chunk, &chunkname, b"bt")?;
1332 load_aux(state, status_ok, 0)
1333}
1334
1335/// `gcinfo()` — Lua 5.1 only. Returns the amount of memory in use by Lua, in
1336/// kilobytes. A deprecated holdover of `collectgarbage("count")` that returns
1337/// just the integer KB count. Verified against lua5.1.5: returns a number. See
1338/// specs/followup/5.1-roster-syntax.md §1.
1339pub(crate) fn gcinfo_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1340 let k = state.gc_count()?;
1341 state.push(LuaValue::Int(k as i64));
1342 Ok(1)
1343}
1344
1345/// `newproxy([boolean | proxy])` — Lua 5.1 only.
1346///
1347/// Creates a zero-size userdata (a "proxy"). With no argument or `false`, the
1348/// proxy has no metatable. With `true`, it gets a fresh empty metatable (so a
1349/// host can install `__gc`/`__len`, the userdata idiom these metamethods need
1350/// in 5.1). With another proxy, it shares that proxy's metatable. Mirrors
1351/// `luaB_newproxy` in 5.1 `lbaselib.c`; see specs/followup/5.1-roster-syntax.md
1352/// §1. The C version validates the proxy argument against a weak table of
1353/// metatables it created; this port instead accepts any userdata that carries a
1354/// metatable, which is observably equivalent for the proxy idiom.
1355pub(crate) fn newproxy_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1356 lua_vm::api::set_top(state, 1)?;
1357 // The new userdata is pushed at stack position 2.
1358 state.new_userdata_typed(b"", 0, 0)?;
1359 if !state.to_boolean(1) {
1360 return Ok(1); // no metatable
1361 }
1362 if matches!(state.type_at(1), LuaType::Boolean) {
1363 // `true`: create and attach a fresh empty metatable.
1364 let mt = state.new_table();
1365 state.push(LuaValue::Table(mt));
1366 state.set_metatable(2)?;
1367 } else {
1368 // A proxy argument: share its metatable. Validate it is a userdata that
1369 // carries one (the C version checks a weak table of valid metatables).
1370 let is_proxy = matches!(state.type_at(1), LuaType::UserData) && state.get_metatable(1)?;
1371 if !is_proxy {
1372 return Err(lua_vm::debug::arg_error_impl(
1373 state,
1374 1,
1375 b"boolean or proxy expected",
1376 ));
1377 }
1378 // get_metatable pushed arg1's metatable on top; attach it to the proxy.
1379 state.set_metatable(2)?;
1380 }
1381 Ok(1)
1382}
1383
1384// ── dofile ────────────────────────────────────────────────────────────────────
1385
1386/// Loads and runs a Lua file, forwarding all return values.
1387///
1388fn dofile_cont(state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
1389 Ok((state.top() as i32 - 1) as usize)
1390}
1391
1392pub(crate) fn dofile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1393 let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
1394 lua_vm::api::set_top(state, 1)?;
1395 if !state.load_file(fname.as_deref())? {
1396 return Err(LuaError::from_value(state.pop()));
1397 }
1398 state.call_k(0, LUA_MULTRET, 0, Some(dofile_cont))?;
1399 dofile_cont(state, 0, 0)
1400}
1401
1402// ── assert ────────────────────────────────────────────────────────────────────
1403
1404/// Raises an error if the first argument is falsy, otherwise passes all
1405/// arguments through as return values.
1406///
1407/// The message handling is a cross-version split. Lua 5.1/5.2 `luaB_assert`
1408/// raise via `luaL_error("%s", luaL_optstring(L, 2, "assertion failed!"))`:
1409/// the message must be string-coercible, so a present non-string/non-number
1410/// second argument raises `bad argument #2 to 'assert' (string expected,
1411/// got <type>)`, a number is stringified, and the result is location-prefixed.
1412/// Lua 5.3+ forward the raw second argument (any value) to `error`, so a table
1413/// message becomes the error object itself, unprefixed.
1414pub(crate) fn assert_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1415 if state.to_boolean(1) {
1416 return Ok(state.top() as usize);
1417 }
1418 if matches!(
1419 state.global().lua_version,
1420 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1421 ) {
1422 let msg = state.opt_arg_string(2, b"assertion failed!")?;
1423 return Err(state.where_error(1, &msg));
1424 }
1425 state.check_arg_any(1)?;
1426 state.remove(1)?;
1427 state.push_string(b"assertion failed!")?;
1428 lua_vm::api::set_top(state, 1)?;
1429 error_fn(state)
1430}
1431
1432// ── select ────────────────────────────────────────────────────────────────────
1433
1434/// Returns a slice of its arguments starting at the given index, or returns
1435/// the count of arguments when called with `"#"`.
1436///
1437pub(crate) fn select_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1438 let n = state.top() as i64;
1439 // Check for '#' first byte without holding a borrow across subsequent ops.
1440 let first_is_hash = state.type_at(1) == LuaType::String && {
1441 state
1442 .to_lua_string_bytes(1)
1443 .and_then(|b| b.first().copied())
1444 == Some(b'#')
1445 };
1446 if first_is_hash {
1447 state.push(LuaValue::Int(n - 1));
1448 return Ok(1);
1449 }
1450 let mut i = state.check_arg_integer(1)?;
1451 if i < 0 {
1452 i = n + i;
1453 } else if i > n {
1454 i = n;
1455 }
1456 if i < 1 {
1457 return Err(lua_vm::debug::arg_error_impl(
1458 state,
1459 1,
1460 b"index out of range",
1461 ));
1462 }
1463 // The values at stack positions [i+1 .. n] are already in place; the
1464 // runtime picks up the top (n - i) of them as results.
1465 Ok((n - i) as usize)
1466}
1467
1468// ── pcall ─────────────────────────────────────────────────────────────────────
1469
1470/// Protected call: returns true + results on success, or false + error on
1471/// failure.
1472///
1473pub(crate) fn pcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1474 state.check_arg_any(1)?;
1475 // Stack before: [f, a1, …, aN]
1476 // Stack after: [true, f, a1, …, aN]
1477 state.push(LuaValue::Bool(true));
1478 state.insert(1)?;
1479 // nargs = gettop - 2 (subtract the sentinel `true` and the function).
1480 let nargs = state.top() as i32 - 2;
1481 let yieldable = state.is_yieldable();
1482 let ok = match state.protected_call_k(nargs, LUA_MULTRET, 0, 0, Some(finish_pcall_k)) {
1483 Ok(()) => true,
1484 // `LuaError::Yield` must bubble up to `lua_resume` so the continuation
1485 // saved on this frame can be invoked on resume.
1486 Err(LuaError::Yield) => return Err(LuaError::Yield),
1487 // A sandbox budget trip is uncatchable: re-raise instead of catching so
1488 // untrusted code cannot defeat the budget with `while true do pcall(..) end`.
1489 Err(e) if state.sandbox_aborting() => return Err(e),
1490 Err(e) if yieldable => return Err(e),
1491 Err(e) => {
1492 state.push(e.into_value());
1493 false
1494 }
1495 };
1496 finish_pcall(state, ok, 0)
1497}
1498
1499/// Continuation matching `LuaKFunction`. Invoked by `finishCcall` on the
1500/// resume path after a yield through pcall (or after a `__close` ran during
1501/// pcall error recovery).
1502///
1503fn finish_pcall_k(state: &mut LuaState, status: i32, extra: isize) -> Result<usize, LuaError> {
1504 let ok = status == LuaStatus::Ok as i32 || status == LuaStatus::Yield as i32;
1505 finish_pcall(state, ok, extra as i32)
1506}
1507
1508// ── xpcall ────────────────────────────────────────────────────────────────────
1509
1510/// Protected call with a separate error-handler function.
1511///
1512pub(crate) fn xpcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1513 // Lua 5.1's `xpcall(f, h)` does NOT forward extra arguments to `f` — `f` is
1514 // always called with zero arguments. The extra-argument forwarding is a 5.2
1515 // addition. Verified against lua5.1.5: `xpcall(fn, h, 1,2,3)` calls `fn`
1516 // with `select("#",...) == 0`. Drop any args past the handler. See
1517 // specs/followup/5.1-roster-syntax.md §1.
1518 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) && state.top() > 2 {
1519 lua_vm::api::set_top(state, 2)?;
1520 }
1521 let n = state.top() as i32;
1522 state.check_arg_type(2, LuaType::Function)?;
1523 // Stack before rotate: [f, err, a1, …, aN, true, f]
1524 // Stack after rotate: [f, err, true, f, a1, …, aN]
1525 state.push(LuaValue::Bool(true));
1526 state.push_copy(1)?;
1527 state.rotate(3, 2)?;
1528 // errfunc is at stack index 2; extra=2 means finishpcall skips 2 values.
1529 let yieldable = state.is_yieldable();
1530 let ok = match state.protected_call_k(n - 2, LUA_MULTRET, 2, 2, Some(finish_pcall_k)) {
1531 Ok(()) => true,
1532 Err(LuaError::Yield) => return Err(LuaError::Yield),
1533 // Uncatchable sandbox abort: re-raise without running the message
1534 // handler, so an `xpcall` handler can neither swallow nor loop on it.
1535 Err(e) if state.sandbox_aborting() => return Err(e),
1536 Err(e) if yieldable => return Err(e),
1537 Err(e) => {
1538 state.push(e.into_value());
1539 false
1540 }
1541 };
1542 finish_pcall(state, ok, 2)
1543}
1544
1545// ── tostring ──────────────────────────────────────────────────────────────────
1546
1547/// Converts any value to its string representation.
1548///
1549/// `to_display_string` honors the `__tostring` metamethod (and, from 5.3, the
1550/// `__name` metafield via the VM's type-naming core), pushes the converted
1551/// string, and leaves it on top as this function's single result.
1552pub(crate) fn tostring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1553 state.check_arg_any(1)?;
1554 state.to_display_string(1)?;
1555 Ok(1)
1556}
1557
1558// ── Registration table ────────────────────────────────────────────────────────
1559
1560/// All base-library functions registered into the global table by `open`.
1561///
1562///
1563/// `_G` and `_VERSION` are not functions and so are absent here; `open()`
1564/// installs them (and the per-version roster deltas) explicitly.
1565pub(crate) const BASE_FUNCS: &[(&[u8], LuaLibFn)] = &[
1566 (b"assert", assert_fn),
1567 (b"collectgarbage", collectgarbage_fn),
1568 (b"dofile", dofile_fn),
1569 (b"error", error_fn),
1570 (b"getmetatable", getmetatable_fn),
1571 (b"ipairs", ipairs_fn),
1572 (b"loadfile", loadfile_fn),
1573 (b"load", load_fn),
1574 (b"next", next_fn),
1575 (b"pairs", pairs_fn),
1576 (b"pcall", pcall_fn),
1577 (b"print", print_fn),
1578 (b"warn", warn_fn),
1579 (b"rawequal", rawequal_fn),
1580 (b"rawlen", rawlen_fn),
1581 (b"rawget", rawget_fn),
1582 (b"rawset", rawset_fn),
1583 (b"select", select_fn),
1584 (b"setmetatable", setmetatable_fn),
1585 (b"tonumber", tonumber_fn),
1586 (b"tostring", tostring_fn),
1587 (b"type", type_fn),
1588 (b"xpcall", xpcall_fn),
1589];
1590
1591// ── Module opener ─────────────────────────────────────────────────────────────
1592
1593/// Open the base library: register all base functions into the global table,
1594/// then set `_G` (a self-reference) and `_VERSION`.
1595///
1596pub fn open(state: &mut LuaState) -> Result<usize, LuaError> {
1597 state.push_globals()?;
1598 state.set_funcs(BASE_FUNCS, 0)?;
1599 state.push_copy(-1)?;
1600 state.set_field(-2, LUA_GNAME)?;
1601 let version_str = state.global().lua_version.version_str();
1602 state.push_string(version_str.as_bytes())?;
1603 state.set_field(-2, b"_VERSION")?;
1604 // `warn` was introduced in Lua 5.4; it is absent on 5.1/5.2/5.3.
1605 if matches!(
1606 state.global().lua_version,
1607 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
1608 ) {
1609 state.push(LuaValue::Nil);
1610 state.set_field(-2, b"warn")?;
1611 }
1612 // Lua 5.1/5.2 carry two globals that were removed in 5.3: `unpack` (an alias
1613 // of `table.unpack`) and `loadstring` (an alias of `load`). Verified against
1614 // lua5.2.4: both are functions. The base table is on the stack top here.
1615 if matches!(
1616 state.global().lua_version,
1617 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1618 ) {
1619 state.push_c_function(crate::table_lib::unpack)?;
1620 state.set_field(-2, b"unpack")?;
1621 }
1622 // `loadstring` aliases `load` in 5.2 (whose `load` accepts a string), but in
1623 // 5.1 `load` is reader-only, so `loadstring` is a distinct string-loader.
1624 // Both are absent in 5.3+. See specs/followup/5.1-roster-syntax.md §1.
1625 if matches!(state.global().lua_version, lua_types::LuaVersion::V52) {
1626 state.push_c_function(load_fn)?;
1627 state.set_field(-2, b"loadstring")?;
1628 }
1629 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1630 state.push_c_function(loadstring_fn)?;
1631 state.set_field(-2, b"loadstring")?;
1632 // `gcinfo()` and `newproxy()` are 5.1 holdovers absent in 5.2+.
1633 state.push_c_function(gcinfo_fn)?;
1634 state.set_field(-2, b"gcinfo")?;
1635 state.push_c_function(newproxy_fn)?;
1636 state.set_field(-2, b"newproxy")?;
1637 // `rawlen` is a Lua 5.2 addition; it is absent in 5.1. Verified against
1638 // lua5.1.5: `type(rawlen)` == "nil". It lives in BASE_FUNCS (registered
1639 // for every version), so withhold it under V51.
1640 state.push(LuaValue::Nil);
1641 state.set_field(-2, b"rawlen")?;
1642 }
1643 // Lua 5.1's fenv-based globals model: `getfenv`/`setfenv` read and write a
1644 // function's environment (its `_ENV` upvalue under the reused modern core)
1645 // or the running thread's global table for level 0. Both were removed in
1646 // 5.2 (which switched to lexical `_ENV`), so they are V51-only. See
1647 // specs/followup/5.1-fenv.md.
1648 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1649 state.push_c_function(getfenv_fn)?;
1650 state.set_field(-2, b"getfenv")?;
1651 state.push_c_function(setfenv_fn)?;
1652 state.set_field(-2, b"setfenv")?;
1653 }
1654 Ok(1)
1655}