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