lua_stdlib/base.rs
1//! Base library — Lua's built-in functions (`print`, `type`, `pairs`, `pcall`, …).
2//!
3//! Translated from: `reference/lua-5.4.7/src/lbaselib.c` (549 lines, 32 functions)
4//! Target crate: `lua-stdlib`
5
6use crate::state_stub::{LuaState, LuaStateStubExt as _};
7use lua_types::{closure::LuaClosure, error::LuaError, value::LuaValue, LuaStatus, LuaType};
8
9// ── Module-level constants ────────────────────────────────────────────────────
10
11/// ASCII whitespace characters used by `b_str2int` for strspn-style skipping.
12const SPACECHARS: &[u8] = b" \x0c\n\r\t\x0b";
13
14/// Reserved stack slot used by `generic_reader` to anchor the current chunk
15/// string so it is not collected while `lua_load` is running.
16const RESERVED_SLOT: i32 = 5;
17
18/// Name of the global environment table stored as a global itself.
19const LUA_GNAME: &[u8] = b"_G";
20
21/// Sentinel indicating "all return values" for call/pcall helpers.
22const LUA_MULTRET: i32 = -1;
23
24// ── GC operation codes ────────────────────────────────────────────────────────
25
26/// Identifies a GC control operation passed to the `collectgarbage` built-in.
27/// Mirrors the `LUA_GC*` integer constants from `lua.h`.
28/// TODO(port): define as a proper type in lua-types once the GC API is finalised.
29#[repr(i32)]
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31enum GcOp {
32 Stop = 0,
33 Restart = 1,
34 Collect = 2,
35 Count = 3,
36 #[expect(
37 dead_code,
38 reason = "ported stdlib helper; not yet wired into the runtime"
39 )]
40 CountB = 4,
41 Step = 5,
42 SetPause = 6,
43 SetStepMul = 7,
44 IsRunning = 9,
45 Gen = 10,
46 Inc = 11,
47 Param = 12,
48}
49
50// ── LuaState forward declaration ─────────────────────────────────────────────
51
52// LuaState is provided by crate::state_stub.
53
54// ── Type alias for standard Lua-callable functions ────────────────────────────
55
56/// Rust equivalent of `lua_CFunction`: a bare function that receives the
57/// interpreter state and returns a count of pushed results.
58pub(crate) type LuaLibFn = fn(&mut LuaState) -> Result<usize, LuaError>;
59
60// ── Helper: push_mode ─────────────────────────────────────────────────────────
61
62/// Push the GC mode string ("incremental" or "generational") onto the stack,
63/// or push `nil` (fail) when `oldmode == -1` (invalid call inside a finalizer).
64///
65fn push_mode(state: &mut LuaState, oldmode: i32) -> Result<usize, LuaError> {
66 if oldmode == -1 {
67 state.push(LuaValue::Nil);
68 } else {
69 let s: &[u8] = if oldmode == GcOp::Inc as i32 {
70 b"incremental"
71 } else {
72 b"generational"
73 };
74 state.push_string(s)?;
75 }
76 Ok(1)
77}
78
79// ── Helper: finish_pcall ──────────────────────────────────────────────────────
80
81/// Shared result-adjustment logic for `pcall` and `xpcall`.
82///
83/// On success: returns the count of values already on the stack minus `extra`
84/// skipped sentinel values. On failure: replaces whatever is on the stack
85/// with `[false, error_message]` and returns 2.
86///
87fn finish_pcall(state: &mut LuaState, ok: bool, extra: i32) -> Result<usize, LuaError> {
88 if !ok {
89 state.push(LuaValue::Bool(false));
90 state.push_copy(-2)?;
91 return Ok(2);
92 }
93 Ok((state.top() as i32 - extra) as usize)
94}
95
96// ── Helper: b_str2int ─────────────────────────────────────────────────────────
97
98/// Parse an integer in an arbitrary base from the byte slice `s`.
99///
100/// Returns `Some((consumed, value))` on success, where `consumed` is the number
101/// of bytes from the start of `s` that were processed (leading and trailing
102/// ASCII whitespace included). Returns `None` when the slice contains no valid
103/// numeral in `base`.
104///
105/// The caller checks `consumed == s.len()` to verify the whole string was used.
106///
107fn b_str2int(s: &[u8], base: u32) -> Option<(usize, i64)> {
108 let mut pos = 0usize;
109 while pos < s.len() && SPACECHARS.contains(&s[pos]) {
110 pos += 1;
111 }
112 let neg = if pos < s.len() && s[pos] == b'-' {
113 pos += 1;
114 true
115 } else {
116 if pos < s.len() && s[pos] == b'+' {
117 pos += 1;
118 }
119 false
120 };
121 if pos >= s.len() || !s[pos].is_ascii_alphanumeric() {
122 return None;
123 }
124 let mut n: u64 = 0u64;
125 loop {
126 let byte = s[pos];
127 let digit = if byte.is_ascii_digit() {
128 (byte - b'0') as u32
129 } else {
130 (byte.to_ascii_uppercase() - b'A') as u32 + 10
131 };
132 if digit >= base {
133 return None;
134 }
135 n = n.wrapping_mul(base as u64).wrapping_add(digit as u64);
136 pos += 1;
137 if pos >= s.len() || !s[pos].is_ascii_alphanumeric() {
138 break;
139 }
140 }
141 while pos < s.len() && SPACECHARS.contains(&s[pos]) {
142 pos += 1;
143 }
144 let value: i64 = if neg {
145 0u64.wrapping_sub(n) as i64
146 } else {
147 n as i64
148 };
149 Some((pos, value))
150}
151
152// ── Helper: load_aux ──────────────────────────────────────────────────────────
153
154/// Shared post-load logic for `load` and `loadfile`.
155///
156/// On success (status_ok == true): optionally installs an environment upvalue,
157/// then returns 1 (the chunk function is on the stack).
158/// On failure: pushes nil then moves it before the error message, returns 2.
159///
160fn load_aux(state: &mut LuaState, status_ok: bool, envidx: i32) -> Result<usize, LuaError> {
161 if status_ok {
162 if envidx != 0 {
163 state.push_copy(envidx)?;
164 if state.set_upvalue(-2, 1)?.is_none() {
165 state.pop_n(1);
166 }
167 }
168 Ok(1)
169 } else {
170 state.push(LuaValue::Nil);
171 state.insert(-2)?;
172 Ok(2)
173 }
174}
175
176fn check_load_mode(state: &mut LuaState, idx: i32, default: &[u8]) -> Result<Vec<u8>, LuaError> {
177 let mode = state.opt_arg_string(idx, default)?;
178 if matches!(state.global().lua_version, lua_types::LuaVersion::V55) && mode.contains(&b'B') {
179 return Err(lua_vm::debug::arg_error_impl(state, idx, b"invalid mode"));
180 }
181 Ok(mode)
182}
183
184// ── print ─────────────────────────────────────────────────────────────────────
185
186/// Converts each argument to a string, separates them with tabs, writes them to
187/// standard output, and finishes with a newline.
188///
189/// The conversion mechanism is a genuine cross-version split:
190///
191/// - Lua 5.1/5.2/5.3 `luaB_print` fetch the **global** `tostring` and *call* it
192/// on each argument. Redefining global `tostring` therefore changes `print`,
193/// a `nil` global makes `print` raise `attempt to call a nil value`, and a
194/// result that is neither a string nor a coercible number raises
195/// `'tostring' must return a string to 'print'`.
196/// - Lua 5.4/5.5 `luaB_print` use `luaL_tolstring` directly: it honors the
197/// `__tostring` / `__name` metafields but ignores the global `tostring`.
198///
199pub(crate) fn print_fn(state: &mut LuaState) -> Result<usize, LuaError> {
200 let calls_global_tostring = matches!(
201 state.global().lua_version,
202 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
203 );
204 if calls_global_tostring {
205 return print_via_global_tostring(state);
206 }
207 let n = state.top();
208 for i in 1..=n {
209 // luaL_tolstring converts via tostring() metamethod, pushes result,
210 // returns a pointer. In Rust we get a GcRef and use its bytes.
211 let display_ref = state.to_display_string(i)?;
212 if i > 1 {
213 state.write_output(b"\t")?;
214 }
215 let bytes = display_ref.clone();
216 state.write_output(&bytes)?;
217 state.pop_n(1);
218 }
219 state.write_output(b"\n")?;
220 Ok(0)
221}
222
223/// Faithful port of the Lua 5.1/5.2/5.3 `luaB_print`: fetch the global
224/// `tostring` once, then call it on each argument.
225///
226fn print_via_global_tostring(state: &mut LuaState) -> Result<usize, LuaError> {
227 let n = state.top();
228 lua_vm::api::get_global(state, b"tostring")?;
229 for i in 1..=n {
230 state.push_copy(-1)?;
231 state.push_copy(i)?;
232 state.call(1, 1)?;
233 // lua_tolstring returns NULL for anything that is neither a string nor a
234 // coercible number; the reference raises in that case.
235 if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
236 return Err(state.where_error(1, b"'tostring' must return a string to 'print'"));
237 }
238 let bytes = state
239 .to_lua_string_bytes(-1)
240 .expect("string/number coerces to bytes");
241 if i > 1 {
242 state.write_output(b"\t")?;
243 }
244 state.write_output(&bytes)?;
245 state.pop_n(1);
246 }
247 state.write_output(b"\n")?;
248 Ok(0)
249}
250
251// ── warn ──────────────────────────────────────────────────────────────────────
252
253/// Validates that every argument is a string, then forwards them as a
254/// multi-part warning message via the state's warning hook.
255///
256pub(crate) fn warn_fn(state: &mut LuaState) -> Result<usize, LuaError> {
257 let n = state.top();
258 state.check_arg_string(1)?;
259 for i in 2..=n {
260 state.check_arg_string(i)?;
261 }
262 for i in 1..n {
263 // Clone bytes before further mutation to avoid borrow conflict.
264 // PORTING.md §8: "No &LuaValue across a stack-mutating call."
265 let s: Vec<u8> = state
266 .to_lua_string_bytes(i)
267 .map(|b| b.to_vec())
268 .unwrap_or_default();
269 // continue = true (1) — more parts follow
270 state.warning(&s, true)?;
271 }
272 let s: Vec<u8> = state
273 .to_lua_string_bytes(n)
274 .map(|b| b.to_vec())
275 .unwrap_or_default();
276 state.warning(&s, false)?;
277 Ok(0)
278}
279
280// ── tonumber ──────────────────────────────────────────────────────────────────
281
282/// Converts a value to a number, optionally in a given numeric base (2–36).
283///
284pub(crate) fn tonumber_fn(state: &mut LuaState) -> Result<usize, LuaError> {
285 if matches!(state.type_at(2), LuaType::None | LuaType::Nil) {
286 if state.type_at(1) == LuaType::Number {
287 lua_vm::api::set_top(state, 1)?;
288 return Ok(1);
289 }
290 // lua_stringtonumber returns bytes consumed including the NUL terminator,
291 // so success iff consumed == string_length + 1.
292 if let Some(len) = state.to_lua_string_len(1) {
293 if let Some(consumed) = state.string_to_number(1) {
294 if consumed == len + 1 {
295 return Ok(1);
296 }
297 }
298 }
299 state.check_arg_any(1)?;
300 } else {
301 let base = state.check_arg_integer(2)?;
302 state.check_arg_type(1, LuaType::String)?;
303 // Clone before further state ops (PORTING.md §8).
304 let bytes: Vec<u8> = state
305 .to_lua_string_bytes(1)
306 .map(|b| b.to_vec())
307 .unwrap_or_default();
308 if !(2..=36).contains(&base) {
309 return Err(lua_vm::debug::arg_error_impl(
310 state,
311 2,
312 b"base out of range",
313 ));
314 }
315 if let Some((consumed, n)) = b_str2int(&bytes, base as u32) {
316 if consumed == bytes.len() {
317 state.push(LuaValue::Int(n));
318 return Ok(1);
319 }
320 }
321 }
322 state.push(LuaValue::Nil);
323 Ok(1)
324}
325
326// ── error ─────────────────────────────────────────────────────────────────────
327
328/// Raises the value at stack[1] as a Lua error, optionally prepending
329/// source-location information for string errors when `level > 0`.
330///
331pub(crate) fn error_fn(state: &mut LuaState) -> Result<usize, LuaError> {
332 let level = state.opt_arg_integer(2, 1)? as i32;
333 lua_vm::api::set_top(state, 1)?;
334 if state.type_at(1) == LuaType::String && level > 0 {
335 state.push_where(level)?;
336 state.push_copy(1)?;
337 state.concat(2)?;
338 }
339 Err(LuaError::from_value(state.pop()))
340}
341
342// ── getmetatable ──────────────────────────────────────────────────────────────
343
344/// Returns the metatable of the first argument, or the `__metatable` field of
345/// the metatable if that field exists (protecting the raw metatable).
346///
347pub(crate) fn getmetatable_fn(state: &mut LuaState) -> Result<usize, LuaError> {
348 state.check_arg_any(1)?;
349 if !state.get_metatable(1)? {
350 state.push(LuaValue::Nil);
351 return Ok(1);
352 }
353 // Returns LuaType::Nil if metatable has no __metatable; otherwise pushes it.
354 state.get_metafield(1, b"__metatable")?;
355 Ok(1)
356}
357
358// ── setmetatable ──────────────────────────────────────────────────────────────
359
360/// Sets the metatable of the table at argument 1 to the value at argument 2
361/// (nil clears it). Raises an error if the current metatable is protected via
362/// `__metatable`.
363///
364pub(crate) fn setmetatable_fn(state: &mut LuaState) -> Result<usize, LuaError> {
365 let t = state.type_at(2);
366 state.check_arg_type(1, LuaType::Table)?;
367 if !(t == LuaType::Nil || t == LuaType::Table) {
368 let got = state.value_at(2);
369 return Err(LuaError::type_arg_error(2, "nil or table", &got));
370 }
371 if state.get_metafield(1, b"__metatable")? != LuaType::Nil {
372 return Err(LuaError::runtime(format_args!(
373 "cannot change a protected metatable"
374 )));
375 }
376 lua_vm::api::set_top(state, 2)?;
377 state.set_metatable(1)?;
378 Ok(1)
379}
380
381// ── rawequal ──────────────────────────────────────────────────────────────────
382
383/// Raw equality check (no metamethods).
384///
385pub(crate) fn rawequal_fn(state: &mut LuaState) -> Result<usize, LuaError> {
386 state.check_arg_any(1)?;
387 state.check_arg_any(2)?;
388 let eq = state.raw_equal(1, 2)?;
389 state.push(LuaValue::Bool(eq));
390 Ok(1)
391}
392
393// ── rawlen ────────────────────────────────────────────────────────────────────
394
395/// Raw length (#) without metamethods; accepts tables and strings only.
396///
397pub(crate) fn rawlen_fn(state: &mut LuaState) -> Result<usize, LuaError> {
398 let t = state.type_at(1);
399 if !(t == LuaType::Table || t == LuaType::String) {
400 let got = state.value_at(1);
401 return Err(LuaError::type_arg_error(1, "table or string", &got));
402 }
403 let len = state.raw_len(1);
404 state.push(LuaValue::Int(len));
405 Ok(1)
406}
407
408// ── rawget ────────────────────────────────────────────────────────────────────
409
410/// Raw table read (no metamethods).
411///
412pub(crate) fn rawget_fn(state: &mut LuaState) -> Result<usize, LuaError> {
413 state.check_arg_type(1, LuaType::Table)?;
414 state.check_arg_any(2)?;
415 lua_vm::api::set_top(state, 2)?;
416 state.raw_get(1)?;
417 Ok(1)
418}
419
420// ── rawset ────────────────────────────────────────────────────────────────────
421
422/// Raw table write (no metamethods).
423///
424pub(crate) fn rawset_fn(state: &mut LuaState) -> Result<usize, LuaError> {
425 state.check_arg_type(1, LuaType::Table)?;
426 state.check_arg_any(2)?;
427 state.check_arg_any(3)?;
428 lua_vm::api::set_top(state, 3)?;
429 state.raw_set(1)?;
430 Ok(1)
431}
432
433// ── collectgarbage ────────────────────────────────────────────────────────────
434
435/// Expose GC control to Lua scripts. The first argument selects the operation;
436/// subsequent arguments are operation-specific parameters.
437///
438///
439/// PORT NOTE: C's `checkvalres(x)` macro breaks out of the `switch` to the
440/// trailing `luaL_pushfail` when `x == -1` (called inside a finalizer).
441/// In Rust we model this with an explicit early-return to the pushfail path
442/// using a boolean flag, avoiding labeled blocks.
443pub(crate) fn collectgarbage_fn(state: &mut LuaState) -> Result<usize, LuaError> {
444 // Explicit collections bypass the checkpoint wrappers, so the dead
445 // stack slices must be cleared here before any collect dispatch
446 // (C parity: traversethread's atomic clear; see #140 / GC_ROOTS.md).
447 state.gc_clear_dead_stack_tails();
448 // The option set is version-gated. 5.4/5.3 expose `setpause`/`setstepmul`;
449 // 5.5 removed both and added `param` (lbaselib.c). The version that owns
450 // the running state decides which list/mapping applies.
451 let version = state.global().lua_version;
452 let is_v55 = version == lua_types::LuaVersion::V55;
453 // Lua 5.1's `collectgarbage` accepts only `collect/stop/restart/count/step/
454 // setpause/setstepmul`; the 5.2 `isrunning`/`generational`, the 5.4
455 // `incremental`, and the 5.5 `param` must be rejected with `invalid option`.
456 // Verified against lua5.1.5: `collectgarbage("isrunning")` errors. (5.2 DOES
457 // accept `isrunning`/`generational`, so it stays on OPTS_54.) See
458 // specs/followup/5.1-roster-syntax.md §1.
459 static OPTS_51: &[&[u8]] = &[
460 b"stop",
461 b"restart",
462 b"collect",
463 b"count",
464 b"step",
465 b"setpause",
466 b"setstepmul",
467 ];
468 static OPTS_NUM_51: &[GcOp] = &[
469 GcOp::Stop,
470 GcOp::Restart,
471 GcOp::Collect,
472 GcOp::Count,
473 GcOp::Step,
474 GcOp::SetPause,
475 GcOp::SetStepMul,
476 ];
477 static OPTS_54: &[&[u8]] = &[
478 b"stop",
479 b"restart",
480 b"collect",
481 b"count",
482 b"step",
483 b"setpause",
484 b"setstepmul",
485 b"isrunning",
486 b"generational",
487 b"incremental",
488 ];
489 static OPTS_NUM_54: &[GcOp] = &[
490 GcOp::Stop,
491 GcOp::Restart,
492 GcOp::Collect,
493 GcOp::Count,
494 GcOp::Step,
495 GcOp::SetPause,
496 GcOp::SetStepMul,
497 GcOp::IsRunning,
498 GcOp::Gen,
499 GcOp::Inc,
500 ];
501 static OPTS_55: &[&[u8]] = &[
502 b"stop",
503 b"restart",
504 b"collect",
505 b"count",
506 b"step",
507 b"isrunning",
508 b"generational",
509 b"incremental",
510 b"param",
511 ];
512 static OPTS_NUM_55: &[GcOp] = &[
513 GcOp::Stop,
514 GcOp::Restart,
515 GcOp::Collect,
516 GcOp::Count,
517 GcOp::Step,
518 GcOp::IsRunning,
519 GcOp::Gen,
520 GcOp::Inc,
521 GcOp::Param,
522 ];
523 let (opts, opts_num): (&[&[u8]], &[GcOp]) = if is_v55 {
524 (OPTS_55, OPTS_NUM_55)
525 } else if matches!(version, lua_types::LuaVersion::V51) {
526 (OPTS_51, OPTS_NUM_51)
527 } else {
528 (OPTS_54, OPTS_NUM_54)
529 };
530 let idx = state.check_arg_option(1, Some(b"collect"), opts)?;
531 let op = opts_num[idx];
532
533 // Each arm either returns early on success, or evaluates to `false`
534 // (meaning checkvalres fired — fall through to pushfail).
535 let valid: bool = match op {
536 GcOp::Count => {
537 // TODO(port): gc_count / gc_count_b are stubs in Phase A.
538 let k = state.gc_count()?;
539 let b = state.gc_count_b()?;
540 if k == -1 {
541 false
542 } else {
543 state.push(LuaValue::Float(k as f64 + b as f64 / 1024.0));
544 return Ok(1);
545 }
546 }
547 GcOp::Step => {
548 let step = state.opt_arg_integer(2, 0)? as i32;
549 // TODO(port): gc_step is a stub in Phase A.
550 let res = state.gc_step(step)?;
551 if res == -1 {
552 false
553 } else {
554 state.push(LuaValue::Bool(res != 0));
555 return Ok(1);
556 }
557 }
558 GcOp::SetPause | GcOp::SetStepMul => {
559 let p = state.opt_arg_integer(2, 0)? as i32;
560 // TODO(port): gc_set_param is a stub in Phase A.
561 let previous = state.gc_set_param(op as i32, p)?;
562 if previous == -1 {
563 false
564 } else {
565 state.push(LuaValue::Int(previous as i64));
566 return Ok(1);
567 }
568 }
569 GcOp::IsRunning => {
570 let res = state.gc_is_running()?;
571 state.push(LuaValue::Bool(res));
572 return Ok(1);
573 }
574 GcOp::Gen => {
575 let minormul = state.opt_arg_integer(2, 0)? as i32;
576 let majormul = state.opt_arg_integer(3, 0)? as i32;
577 // TODO(port): gc_gen is a stub in Phase A.
578 let oldmode = state.gc_gen(minormul, majormul)?;
579 return push_mode(state, oldmode);
580 }
581 GcOp::Inc => {
582 let pause = state.opt_arg_integer(2, 0)? as i32;
583 let stepmul = state.opt_arg_integer(3, 0)? as i32;
584 let stepsize = state.opt_arg_integer(4, 0)? as i32;
585 // TODO(port): gc_inc is a stub in Phase A.
586 let oldmode = state.gc_inc(pause, stepmul, stepsize)?;
587 return push_mode(state, oldmode);
588 }
589 GcOp::Param => {
590 // 5.5 collectgarbage("param", name [, value]): read or write a GC
591 // parameter, always returning the OLD integer value. arg2 selects
592 // the param; arg3 (default -1 = read-only) is the new value.
593 static PARAMS: &[&[u8]] = &[
594 b"minormul",
595 b"majorminor",
596 b"minormajor",
597 b"pause",
598 b"stepmul",
599 b"stepsize",
600 ];
601 let pidx = state.check_arg_option(2, None, PARAMS)?;
602 let value = state.opt_arg_integer(3, -1)?;
603 let old = state.gc_param(pidx, value)?;
604 state.push(LuaValue::Int(old));
605 return Ok(1);
606 }
607 _ => {
608 // TODO(port): gc_control_simple is a stub in Phase A.
609 let res = state.gc_control_simple(op as i32)?;
610 if res == -1 {
611 false
612 } else {
613 state.push(LuaValue::Int(res as i64));
614 return Ok(1);
615 }
616 }
617 };
618 debug_assert!(
619 !valid,
620 "valid arms return early; reaching here means checkvalres fired"
621 );
622 state.push(LuaValue::Nil);
623 Ok(1)
624}
625
626// ── type ──────────────────────────────────────────────────────────────────────
627
628/// Returns the type name of its argument as a string.
629///
630pub(crate) fn type_fn(state: &mut LuaState) -> Result<usize, LuaError> {
631 let t = state.type_at(1);
632 if t == LuaType::None {
633 return Err(lua_vm::debug::arg_error_impl(state, 1, b"value expected"));
634 }
635 // Clone the bytes before the push to avoid borrow conflict with state.
636 let name: Vec<u8> = state.type_name(t).to_vec();
637 state.push_string(&name)?;
638 Ok(1)
639}
640
641// ── getfenv / setfenv (Lua 5.1 fenv globals) ──────────────────────────────────
642
643/// Truncate a numeric `getfenv`/`setfenv` level toward zero.
644///
645/// 5.1's `luaL_checkint` casts `lua_Number` to a C `int`, truncating toward
646/// zero, so `getfenv(1.9)` is level 1 and `getfenv(-0.5)` is level 0. Under the
647/// float-only V51 model every number arrives as a `Float`; the `Int` arm is a
648/// defensive no-op. A non-number never reaches this helper.
649fn fenv_level(v: &LuaValue) -> i64 {
650 match v {
651 LuaValue::Float(f) => f.trunc() as i64,
652 LuaValue::Int(i) => *i,
653 _ => 0,
654 }
655}
656
657/// Resolve the function value targeted by a `getfenv`/`setfenv` first argument.
658///
659/// Returns the `LuaValue::Function` whose environment is being read or written.
660/// `arg1` is interpreted exactly as Lua 5.1's `getfunc`/`setfunc`
661/// (lbaselib.c): a function value targets that function directly; a number is a
662/// stack *level* (floored toward zero), where level 1 is the function calling
663/// `getfenv`/`setfenv`. Level 0 is handled by the callers (it denotes the
664/// running thread's global table, not a function) and never reaches here.
665///
666/// Errors mirror lua5.1.5:
667/// - negative level → `level must be non-negative`
668/// - level past the stack → `invalid level`
669/// - neither number nor function → `number expected, got <type>`
670fn fenv_getfunc(state: &mut LuaState, level: i64) -> Result<LuaValue, LuaError> {
671 if level < 0 {
672 return Err(lua_vm::debug::arg_error_impl(
673 state,
674 1,
675 b"level must be non-negative",
676 ));
677 }
678 let mut ar = lua_vm::debug::LuaDebug::default();
679 if !lua_vm::debug::get_stack(state, level as i32, &mut ar) {
680 return Err(lua_vm::debug::arg_error_impl(state, 1, b"invalid level"));
681 }
682 let ci_idx = ar
683 .i_ci
684 .ok_or_else(|| lua_vm::debug::arg_error_impl(state, 1, b"invalid level"))?;
685 let func_slot = state.get_ci(ci_idx).func;
686 Ok(state.get_at(func_slot))
687}
688
689/// Index of a Lua closure's `_ENV` upvalue, by upvalue name.
690///
691/// The reused modern parser threads an upvalue literally named `_ENV` and
692/// resolves every free (global) name through it; under V51 that upvalue *is* the
693/// function environment. It is NOT always upvalue 0 — a nested closure that
694/// captures locals places those first, with `_ENV` at a later index — so it must
695/// be located by name, not position. A closure that references no free names has
696/// no `_ENV` upvalue and returns `None`.
697fn fenv_env_upval_index(
698 lcl: &lua_types::gc::GcRef<lua_types::closure::LuaLClosure>,
699) -> Option<usize> {
700 lcl.proto
701 .upvalues
702 .iter()
703 .position(|ud| ud.name.as_ref().map(|s| s.as_bytes()) == Some(b"_ENV"))
704}
705
706/// Read the environment of a resolved function value.
707///
708/// A Lua closure's environment is its `_ENV` upvalue. A C/Rust function (or a
709/// Lua closure that references no globals, hence has no `_ENV` upvalue) is given
710/// the thread global table as its environment — matching the common 5.1 case
711/// and the documented `LUA_ENVIRONINDEX` gap (specs/followup/5.1-fenv.md §4).
712fn fenv_read(state: &LuaState, func: &LuaValue) -> LuaValue {
713 if let LuaValue::Function(LuaClosure::Lua(lcl)) = func {
714 if let Some(idx) = fenv_env_upval_index(lcl) {
715 return state.upvalue_get(lcl, idx);
716 }
717 }
718 state.global().globals.clone()
719}
720
721/// `getfenv([f])` — Lua 5.1 only.
722///
723/// Returns the environment of the function `f` (a function value or a stack
724/// level), or the running function's environment when the argument is absent or
725/// `1`. Level `0` returns the running thread's global table. See
726/// `specs/followup/5.1-fenv.md` §2.
727pub(crate) fn getfenv_fn(state: &mut LuaState) -> Result<usize, LuaError> {
728 let arg1 = state.value_at(1);
729 let func = match &arg1 {
730 LuaValue::Function(_) => arg1.clone(),
731 LuaValue::Nil if state.type_at(1) == LuaType::None => {
732 // No argument => level 1 (the running function).
733 fenv_getfunc(state, 1)?
734 }
735 LuaValue::Float(_) | LuaValue::Int(_) => {
736 let level = fenv_level(&arg1);
737 if level == 0 {
738 let g = state.global().globals.clone();
739 state.push(g);
740 return Ok(1);
741 }
742 fenv_getfunc(state, level)?
743 }
744 other => {
745 let got = state.obj_type_name(other);
746 let msg = format!("number expected, got {}", String::from_utf8_lossy(&got));
747 return Err(lua_vm::debug::arg_error_impl(state, 1, msg.as_bytes()));
748 }
749 };
750 let env = fenv_read(state, &func);
751 state.push(env);
752 Ok(1)
753}
754
755/// `setfenv(f, table)` — Lua 5.1 only.
756///
757/// Sets the environment of the function `f` (a function value or a stack level)
758/// to `table`. `setfenv(0, t)` sets the running thread's global table. Returns
759/// the affected function (or the running thread for level 0). A C/Rust function
760/// (or any non-Lua object) cannot have its environment changed and raises,
761/// matching lua5.1.5. See `specs/followup/5.1-fenv.md` §2.
762pub(crate) fn setfenv_fn(state: &mut LuaState) -> Result<usize, LuaError> {
763 state.check_arg_type(2, LuaType::Table)?;
764 let new_env = state.value_at(2);
765
766 let arg1 = state.value_at(1);
767 let is_level_zero =
768 matches!(&arg1, LuaValue::Int(0)) || matches!(&arg1, LuaValue::Float(f) if *f == 0.0);
769 if is_level_zero {
770 // Level 0: replace the running thread's global table and return the
771 // running thread. Subsequently-loaded top-level chunks take this env.
772 state.global_mut().globals = new_env;
773 lua_vm::api::push_thread(state);
774 return Ok(1);
775 }
776
777 let func = match &arg1 {
778 LuaValue::Function(_) => arg1.clone(),
779 LuaValue::Float(_) | LuaValue::Int(_) => {
780 let level = fenv_level(&arg1);
781 fenv_getfunc(state, level)?
782 }
783 other => {
784 let got = state.obj_type_name(other);
785 let msg = format!("number expected, got {}", String::from_utf8_lossy(&got));
786 return Err(lua_vm::debug::arg_error_impl(state, 1, msg.as_bytes()));
787 }
788 };
789
790 match &func {
791 LuaValue::Function(LuaClosure::Lua(lcl)) => {
792 if let Some(idx) = fenv_env_upval_index(lcl) {
793 // Give the closure a PRIVATE environment: replace its `_ENV`
794 // upvalue *cell* with a fresh closed upvalue holding `new_env`.
795 // Mutating the existing cell's value (`upvalue_set`) would alter
796 // every closure sharing that upvalue (e.g. the main chunk's
797 // `_G`), which is wrong — `setfenv(f, e)` must not change the
798 // caller's globals. A new cell isolates `f`.
799 let uv = state.new_upval_closed(new_env);
800 lcl.set_upval(idx, uv);
801 state.gc().obj_barrier(lcl, &uv);
802 }
803 // A Lua closure that references no globals has no `_ENV` upvalue and
804 // nothing reads globals through it, so the set is inert; 5.1 still
805 // accepts it and returns the function. (Gap: a subsequent
806 // `getfenv` on such a closure returns the thread globals rather than
807 // the set table — see specs/followup/5.1-fenv.md §4.)
808 }
809 _ => {
810 // C/Rust functions cannot have their environment changed. 5.1
811 // raises this exact message (via luaL_error, so it carries the
812 // caller's source location) for any object whose env is fixed.
813 return Err(
814 state.where_error(1, b"'setfenv' cannot change environment of given object")
815 );
816 }
817 }
818 state.push(func);
819 Ok(1)
820}
821
822/// Set the environment of the Lua closure `level` frames up the running stack
823/// to `new_env`, the internal equivalent of `setfenv(level, new_env)`.
824///
825/// Used by `module` (5.1 `package` library), which sets its caller's
826/// environment to the module table. A non-Lua function (or a closure with no
827/// `_ENV` upvalue) is left unchanged, matching the inert-set behavior of
828/// `setfenv`. See specs/followup/5.1-fenv.md.
829pub(crate) fn set_func_env_at_level(
830 state: &mut LuaState,
831 level: i64,
832 new_env: LuaValue,
833) -> Result<(), LuaError> {
834 let func = fenv_getfunc(state, level)?;
835 if let LuaValue::Function(LuaClosure::Lua(lcl)) = &func {
836 if let Some(idx) = fenv_env_upval_index(lcl) {
837 let uv = state.new_upval_closed(new_env);
838 lcl.set_upval(idx, uv);
839 state.gc().obj_barrier(lcl, &uv);
840 }
841 }
842 Ok(())
843}
844
845// ── next ──────────────────────────────────────────────────────────────────────
846
847/// Table traversal iterator: given a table and a key, pushes the next key-value
848/// pair. Pushes nil and returns 1 when the traversal is exhausted.
849///
850pub(crate) fn next_fn(state: &mut LuaState) -> Result<usize, LuaError> {
851 state.check_arg_type(1, LuaType::Table)?;
852 lua_vm::api::set_top(state, 2)?;
853 if state.table_next(1)? {
854 Ok(2)
855 } else {
856 state.push(LuaValue::Nil);
857 Ok(1)
858 }
859}
860
861// ── pairs continuation (coroutine stub) ───────────────────────────────────────
862
863/// Continuation for `pairs` when the `__pairs` metamethod yields.
864/// Re-invoked by `finishCcall` after the yielded `__pairs` resumes.
865///
866fn pairs_cont(state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
867 if state.global().lua_version == lua_types::LuaVersion::V55 {
868 Ok(4)
869 } else {
870 Ok(3)
871 }
872}
873
874// ── pairs ─────────────────────────────────────────────────────────────────────
875
876/// Returns the `next` function, the table, and nil (or invokes a `__pairs`
877/// metamethod).
878///
879pub(crate) fn pairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
880 state.check_arg_any(1)?;
881 // Lua 5.1 has no `__pairs` metamethod; `pairs(t)` always iterates the raw
882 // table even when a `__pairs` is set (it is silently ignored). Lua 5.5
883 // extends the result list with a fourth to-be-closed object.
884 let consult_pairs_tm = !matches!(state.global().lua_version, lua_types::LuaVersion::V51);
885 let nresults = if state.global().lua_version == lua_types::LuaVersion::V55 {
886 4
887 } else {
888 3
889 };
890 if !consult_pairs_tm || state.get_metafield(1, b"__pairs")? == LuaType::Nil {
891 state.push_c_function(next_fn)?;
892 state.push_copy(1)?;
893 state.push(LuaValue::Nil);
894 if nresults == 4 {
895 state.push(LuaValue::Nil);
896 }
897 } else {
898 state.push_copy(1)?;
899 state.call_k(1, nresults as i32, 0, Some(pairs_cont))?;
900 }
901 Ok(nresults)
902}
903
904// ── ipairs auxiliary ──────────────────────────────────────────────────────────
905
906/// Iterator step function for `ipairs`: increments the counter and fetches
907/// the next array element. Returns the index + value, or just the index when
908/// the value is nil (signalling end-of-iteration).
909///
910fn ipairs_aux(state: &mut LuaState) -> Result<usize, LuaError> {
911 let i = match lua_vm::api::positive_index_value(state, 2) {
912 LuaValue::Int(i) => i,
913 _ => state.check_arg_integer(2)?,
914 };
915 // luaL_intop(+, a, b) → wrapping integer addition (PORTING.md §9 / macros.tsv `intop`)
916 let i = (i as u64).wrapping_add(1u64) as i64;
917 state.push(LuaValue::Int(i));
918 let table = lua_vm::api::positive_index_value(state, 1);
919 let t = state.table_get_i_value(&table, i)?;
920 if t == LuaType::Nil {
921 Ok(1)
922 } else {
923 Ok(2)
924 }
925}
926
927// ── ipairs ────────────────────────────────────────────────────────────────────
928
929/// Returns the `ipairsaux` iterator, the table, and 0 as the initial counter.
930///
931pub(crate) fn ipairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
932 state.check_arg_any(1)?;
933 state.push_c_function(ipairs_aux)?;
934 state.push_copy(1)?;
935 state.push(LuaValue::Int(0));
936 Ok(3)
937}
938
939// ── loadfile ──────────────────────────────────────────────────────────────────
940
941/// Loads a Lua chunk from a file.
942///
943pub(crate) fn loadfile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
944 let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
945 let mode: Option<Vec<u8>> = if state.is_none_or_nil(2) {
946 None
947 } else {
948 Some(check_load_mode(state, 2, b"bt")?)
949 };
950 let env = if state.type_at(3) != LuaType::None {
951 3
952 } else {
953 0
954 };
955 let status_ok = state.load_file_ex(fname.as_deref(), mode.as_deref())?;
956 load_aux(state, status_ok, env)
957}
958
959// ── generic_reader ────────────────────────────────────────────────────────────
960
961/// Reader callback for `luaB_load` when the chunk source is a Lua function.
962/// Calls the function at stack[1] repeatedly to obtain successive chunks.
963///
964///
965/// PORT NOTE: In C this is a `lua_Reader` function pointer passed to
966/// `lua_load`. In Rust, readers are closures — but `generic_reader` itself
967/// needs `&mut LuaState`, which conflicts with `state.load_with_reader`'s
968/// own borrow. The current translation materialises the reader as a free
969/// function for documentation purposes; Phase B must resolve the design
970/// (e.g., a separate reader-context type, or a split between "advance reader"
971/// and "run Lua call" phases).
972/// TODO(port): generic_reader — self-referential &mut borrow when used as lua_load callback.
973fn generic_reader(state: &mut LuaState) -> Result<Option<Vec<u8>>, LuaError> {
974 state.ensure_stack(2, b"too many nested functions")?;
975 state.push_copy(1)?;
976 state.call(0, 1)?;
977 if state.type_at(-1) == LuaType::Nil {
978 state.pop_n(1);
979 return Ok(None);
980 }
981 // luaL_error(L, "reader function must return a string");
982 // lua_isstring in C is true for strings AND coercible numbers.
983 if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
984 return Err(LuaError::runtime(format_args!(
985 "reader function must return a string"
986 )));
987 }
988 state.replace(RESERVED_SLOT)?;
989 let bytes = state.to_lua_string_bytes(RESERVED_SLOT).map(|b| b.to_vec());
990 Ok(bytes)
991}
992
993// ── load ──────────────────────────────────────────────────────────────────────
994
995/// Loads a Lua chunk from a string or a reader function.
996///
997pub(crate) fn load_fn(state: &mut LuaState) -> Result<usize, LuaError> {
998 // Lua 5.1's `load` takes a *reader function only* — string loading is
999 // `loadstring`'s job. `load("...")` errors with `function expected, got
1000 // string`. The string-or-function overload is a 5.2 addition. Verified
1001 // against lua5.1.5; see specs/followup/5.1-roster-syntax.md §1.
1002 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1003 state.check_arg_type(1, LuaType::Function)?;
1004 }
1005 // Determine whether argument 1 is a string (load from buffer) or a
1006 // function (load from reader).
1007 let is_string = matches!(state.type_at(1), LuaType::String | LuaType::Number);
1008 let mode: Vec<u8> = check_load_mode(state, 3, b"bt")?;
1009 let env = if state.type_at(4) != LuaType::None {
1010 4
1011 } else {
1012 0
1013 };
1014 let status_ok = if is_string {
1015 let chunk: Vec<u8> = state.to_lua_string_bytes(1).unwrap_or_default();
1016 let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
1017 chunk.clone()
1018 } else {
1019 state.check_arg_string(2)?
1020 };
1021 state.load_buffer_ex(&chunk, &chunkname, &mode)?
1022 } else {
1023 let chunkname: Vec<u8> = state
1024 .opt_arg_string_bytes(2)
1025 .unwrap_or_else(|_| b"=(load)".to_vec());
1026 state.check_arg_type(1, LuaType::Function)?;
1027 lua_vm::api::set_top(state, RESERVED_SLOT)?;
1028 // TODO(port): generic_reader cannot be passed directly due to self-referential
1029 // &mut borrow — see generic_reader's PORT NOTE. Phase B resolves this.
1030 state.load_with_reader(generic_reader, &chunkname, &mode)?
1031 };
1032 load_aux(state, status_ok, env)
1033}
1034
1035/// `loadstring(s [, chunkname])` — Lua 5.1 only.
1036///
1037/// Loads a string as a Lua chunk. In 5.1 this is the string-loading counterpart
1038/// to `load` (which takes a reader function only). The second argument is the
1039/// chunk name. Verified against lua5.1.5; see
1040/// specs/followup/5.1-roster-syntax.md §1.
1041pub(crate) fn loadstring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1042 let chunk: Vec<u8> = state.check_arg_string(1)?;
1043 let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
1044 chunk.clone()
1045 } else {
1046 state.check_arg_string(2)?
1047 };
1048 let status_ok = state.load_buffer_ex(&chunk, &chunkname, b"bt")?;
1049 load_aux(state, status_ok, 0)
1050}
1051
1052/// `gcinfo()` — Lua 5.1 only. Returns the amount of memory in use by Lua, in
1053/// kilobytes. A deprecated holdover of `collectgarbage("count")` that returns
1054/// just the integer KB count. Verified against lua5.1.5: returns a number. See
1055/// specs/followup/5.1-roster-syntax.md §1.
1056pub(crate) fn gcinfo_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1057 let k = state.gc_count()?;
1058 state.push(LuaValue::Int(k as i64));
1059 Ok(1)
1060}
1061
1062/// `newproxy([boolean | proxy])` — Lua 5.1 only.
1063///
1064/// Creates a zero-size userdata (a "proxy"). With no argument or `false`, the
1065/// proxy has no metatable. With `true`, it gets a fresh empty metatable (so a
1066/// host can install `__gc`/`__len`, the userdata idiom these metamethods need
1067/// in 5.1). With another proxy, it shares that proxy's metatable. Mirrors
1068/// `luaB_newproxy` in 5.1 `lbaselib.c`; see specs/followup/5.1-roster-syntax.md
1069/// §1. The C version validates the proxy argument against a weak table of
1070/// metatables it created; this port instead accepts any userdata that carries a
1071/// metatable, which is observably equivalent for the proxy idiom.
1072pub(crate) fn newproxy_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1073 lua_vm::api::set_top(state, 1)?;
1074 // The new userdata is pushed at stack position 2.
1075 state.new_userdata_typed(b"", 0, 0)?;
1076 if !state.to_boolean(1) {
1077 return Ok(1); // no metatable
1078 }
1079 if matches!(state.type_at(1), LuaType::Boolean) {
1080 // `true`: create and attach a fresh empty metatable.
1081 let mt = state.new_table();
1082 state.push(LuaValue::Table(mt));
1083 state.set_metatable(2)?;
1084 } else {
1085 // A proxy argument: share its metatable. Validate it is a userdata that
1086 // carries one (the C version checks a weak table of valid metatables).
1087 let is_proxy = matches!(state.type_at(1), LuaType::UserData) && state.get_metatable(1)?;
1088 if !is_proxy {
1089 return Err(lua_vm::debug::arg_error_impl(
1090 state,
1091 1,
1092 b"boolean or proxy expected",
1093 ));
1094 }
1095 // get_metatable pushed arg1's metatable on top; attach it to the proxy.
1096 state.set_metatable(2)?;
1097 }
1098 Ok(1)
1099}
1100
1101// ── dofile ────────────────────────────────────────────────────────────────────
1102
1103/// Loads and runs a Lua file, forwarding all return values.
1104///
1105fn dofile_cont(state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
1106 Ok((state.top() as i32 - 1) as usize)
1107}
1108
1109pub(crate) fn dofile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1110 let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
1111 lua_vm::api::set_top(state, 1)?;
1112 if !state.load_file(fname.as_deref())? {
1113 return Err(LuaError::from_value(state.pop()));
1114 }
1115 state.call_k(0, LUA_MULTRET, 0, Some(dofile_cont))?;
1116 dofile_cont(state, 0, 0)
1117}
1118
1119// ── assert ────────────────────────────────────────────────────────────────────
1120
1121/// Raises an error if the first argument is falsy, otherwise passes all
1122/// arguments through as return values.
1123///
1124pub(crate) fn assert_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1125 if state.to_boolean(1) {
1126 return Ok(state.top() as usize);
1127 }
1128 state.check_arg_any(1)?;
1129 state.remove(1)?;
1130 state.push_string(b"assertion failed!")?;
1131 lua_vm::api::set_top(state, 1)?;
1132 error_fn(state)
1133}
1134
1135// ── select ────────────────────────────────────────────────────────────────────
1136
1137/// Returns a slice of its arguments starting at the given index, or returns
1138/// the count of arguments when called with `"#"`.
1139///
1140pub(crate) fn select_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1141 let n = state.top() as i64;
1142 // Check for '#' first byte without holding a borrow across subsequent ops.
1143 let first_is_hash = state.type_at(1) == LuaType::String && {
1144 state
1145 .to_lua_string_bytes(1)
1146 .and_then(|b| b.first().copied())
1147 == Some(b'#')
1148 };
1149 if first_is_hash {
1150 state.push(LuaValue::Int(n - 1));
1151 return Ok(1);
1152 }
1153 let mut i = state.check_arg_integer(1)?;
1154 if i < 0 {
1155 i = n + i;
1156 } else if i > n {
1157 i = n;
1158 }
1159 if i < 1 {
1160 return Err(lua_vm::debug::arg_error_impl(
1161 state,
1162 1,
1163 b"index out of range",
1164 ));
1165 }
1166 // The values at stack positions [i+1 .. n] are already in place; the
1167 // runtime picks up the top (n - i) of them as results.
1168 Ok((n - i) as usize)
1169}
1170
1171// ── pcall ─────────────────────────────────────────────────────────────────────
1172
1173/// Protected call: returns true + results on success, or false + error on
1174/// failure.
1175///
1176pub(crate) fn pcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1177 state.check_arg_any(1)?;
1178 // Stack before: [f, a1, …, aN]
1179 // Stack after: [true, f, a1, …, aN]
1180 state.push(LuaValue::Bool(true));
1181 state.insert(1)?;
1182 // nargs = gettop - 2 (subtract the sentinel `true` and the function).
1183 let nargs = state.top() as i32 - 2;
1184 let yieldable = state.is_yieldable();
1185 let ok = match state.protected_call_k(nargs, LUA_MULTRET, 0, 0, Some(finish_pcall_k)) {
1186 Ok(()) => true,
1187 // `LuaError::Yield` must bubble up to `lua_resume` so the continuation
1188 // saved on this frame can be invoked on resume.
1189 Err(LuaError::Yield) => return Err(LuaError::Yield),
1190 // A sandbox budget trip is uncatchable: re-raise instead of catching so
1191 // untrusted code cannot defeat the budget with `while true do pcall(..) end`.
1192 Err(e) if state.sandbox_aborting() => return Err(e),
1193 Err(e) if yieldable => return Err(e),
1194 Err(e) => {
1195 state.push(e.into_value());
1196 false
1197 }
1198 };
1199 finish_pcall(state, ok, 0)
1200}
1201
1202/// Continuation matching `LuaKFunction`. Invoked by `finishCcall` on the
1203/// resume path after a yield through pcall (or after a `__close` ran during
1204/// pcall error recovery).
1205///
1206fn finish_pcall_k(state: &mut LuaState, status: i32, extra: isize) -> Result<usize, LuaError> {
1207 let ok = status == LuaStatus::Ok as i32 || status == LuaStatus::Yield as i32;
1208 finish_pcall(state, ok, extra as i32)
1209}
1210
1211// ── xpcall ────────────────────────────────────────────────────────────────────
1212
1213/// Protected call with a separate error-handler function.
1214///
1215pub(crate) fn xpcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1216 // Lua 5.1's `xpcall(f, h)` does NOT forward extra arguments to `f` — `f` is
1217 // always called with zero arguments. The extra-argument forwarding is a 5.2
1218 // addition. Verified against lua5.1.5: `xpcall(fn, h, 1,2,3)` calls `fn`
1219 // with `select("#",...) == 0`. Drop any args past the handler. See
1220 // specs/followup/5.1-roster-syntax.md §1.
1221 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) && state.top() > 2 {
1222 lua_vm::api::set_top(state, 2)?;
1223 }
1224 let n = state.top() as i32;
1225 state.check_arg_type(2, LuaType::Function)?;
1226 // Stack before rotate: [f, err, a1, …, aN, true, f]
1227 // Stack after rotate: [f, err, true, f, a1, …, aN]
1228 state.push(LuaValue::Bool(true));
1229 state.push_copy(1)?;
1230 state.rotate(3, 2)?;
1231 // errfunc is at stack index 2; extra=2 means finishpcall skips 2 values.
1232 let yieldable = state.is_yieldable();
1233 let ok = match state.protected_call_k(n - 2, LUA_MULTRET, 2, 2, Some(finish_pcall_k)) {
1234 Ok(()) => true,
1235 Err(LuaError::Yield) => return Err(LuaError::Yield),
1236 // Uncatchable sandbox abort: re-raise without running the message
1237 // handler, so an `xpcall` handler can neither swallow nor loop on it.
1238 Err(e) if state.sandbox_aborting() => return Err(e),
1239 Err(e) if yieldable => return Err(e),
1240 Err(e) => {
1241 state.push(e.into_value());
1242 false
1243 }
1244 };
1245 finish_pcall(state, ok, 2)
1246}
1247
1248// ── tostring ──────────────────────────────────────────────────────────────────
1249
1250/// Converts any value to its string representation (calls `__tostring` if
1251/// present).
1252///
1253pub(crate) fn tostring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1254 state.check_arg_any(1)?;
1255 // to_display_string pushes the converted string and returns a handle to it.
1256 // TODO(port): to_display_string method needs implementing on LuaState.
1257 state.to_display_string(1)?;
1258 Ok(1)
1259}
1260
1261// ── Registration table ────────────────────────────────────────────────────────
1262
1263/// All base-library functions registered into the global table by `open`.
1264///
1265///
1266/// PORT NOTE: The C table includes placeholder entries
1267/// `{LUA_GNAME, NULL}` and `{"_VERSION", NULL}` that `luaopen_base` fills in
1268/// separately. Those are omitted here; `open()` sets them explicitly.
1269pub(crate) const BASE_FUNCS: &[(&[u8], LuaLibFn)] = &[
1270 (b"assert", assert_fn),
1271 (b"collectgarbage", collectgarbage_fn),
1272 (b"dofile", dofile_fn),
1273 (b"error", error_fn),
1274 (b"getmetatable", getmetatable_fn),
1275 (b"ipairs", ipairs_fn),
1276 (b"loadfile", loadfile_fn),
1277 (b"load", load_fn),
1278 (b"next", next_fn),
1279 (b"pairs", pairs_fn),
1280 (b"pcall", pcall_fn),
1281 (b"print", print_fn),
1282 (b"warn", warn_fn),
1283 (b"rawequal", rawequal_fn),
1284 (b"rawlen", rawlen_fn),
1285 (b"rawget", rawget_fn),
1286 (b"rawset", rawset_fn),
1287 (b"select", select_fn),
1288 (b"setmetatable", setmetatable_fn),
1289 (b"tonumber", tonumber_fn),
1290 (b"tostring", tostring_fn),
1291 (b"type", type_fn),
1292 (b"xpcall", xpcall_fn),
1293];
1294
1295// ── Module opener ─────────────────────────────────────────────────────────────
1296
1297/// Open the base library: register all base functions into the global table,
1298/// then set `_G` (a self-reference) and `_VERSION`.
1299///
1300pub fn open(state: &mut LuaState) -> Result<usize, LuaError> {
1301 state.push_globals()?;
1302 state.set_funcs(BASE_FUNCS, 0)?;
1303 state.push_copy(-1)?;
1304 state.set_field(-2, LUA_GNAME)?;
1305 let version_str = state.global().lua_version.version_str();
1306 state.push_string(version_str.as_bytes())?;
1307 state.set_field(-2, b"_VERSION")?;
1308 // `warn` was introduced in Lua 5.4; it is absent on 5.1/5.2/5.3.
1309 if matches!(
1310 state.global().lua_version,
1311 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
1312 ) {
1313 state.push(LuaValue::Nil);
1314 state.set_field(-2, b"warn")?;
1315 }
1316 // Lua 5.1/5.2 carry two globals that were removed in 5.3: `unpack` (an alias
1317 // of `table.unpack`) and `loadstring` (an alias of `load`). Verified against
1318 // lua5.2.4: both are functions. The base table is on the stack top here.
1319 if matches!(
1320 state.global().lua_version,
1321 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1322 ) {
1323 state.push_c_function(crate::table_lib::unpack)?;
1324 state.set_field(-2, b"unpack")?;
1325 }
1326 // `loadstring` aliases `load` in 5.2 (whose `load` accepts a string), but in
1327 // 5.1 `load` is reader-only, so `loadstring` is a distinct string-loader.
1328 // Both are absent in 5.3+. See specs/followup/5.1-roster-syntax.md §1.
1329 if matches!(state.global().lua_version, lua_types::LuaVersion::V52) {
1330 state.push_c_function(load_fn)?;
1331 state.set_field(-2, b"loadstring")?;
1332 }
1333 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1334 state.push_c_function(loadstring_fn)?;
1335 state.set_field(-2, b"loadstring")?;
1336 // `gcinfo()` and `newproxy()` are 5.1 holdovers absent in 5.2+.
1337 state.push_c_function(gcinfo_fn)?;
1338 state.set_field(-2, b"gcinfo")?;
1339 state.push_c_function(newproxy_fn)?;
1340 state.set_field(-2, b"newproxy")?;
1341 // `rawlen` is a Lua 5.2 addition; it is absent in 5.1. Verified against
1342 // lua5.1.5: `type(rawlen)` == "nil". It lives in BASE_FUNCS (registered
1343 // for every version), so withhold it under V51.
1344 state.push(LuaValue::Nil);
1345 state.set_field(-2, b"rawlen")?;
1346 }
1347 // Lua 5.1's fenv-based globals model: `getfenv`/`setfenv` read and write a
1348 // function's environment (its `_ENV` upvalue under the reused modern core)
1349 // or the running thread's global table for level 0. Both were removed in
1350 // 5.2 (which switched to lexical `_ENV`), so they are V51-only. See
1351 // specs/followup/5.1-fenv.md.
1352 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1353 state.push_c_function(getfenv_fn)?;
1354 state.set_field(-2, b"getfenv")?;
1355 state.push_c_function(setfenv_fn)?;
1356 state.set_field(-2, b"setfenv")?;
1357 }
1358 Ok(1)
1359}
1360
1361// ──────────────────────────────────────────────────────────────────────────────
1362// PORT STATUS
1363// source: src/lbaselib.c (549 lines, 32 functions)
1364// target_crate: lua-stdlib
1365// confidence: medium
1366// todos: 21
1367// port_notes: 5
1368// unsafe_blocks: 0
1369// notes: All 32 C functions translated. Main uncertainties are (1)
1370// LuaState method signatures (top/type_at/push/… — resolved
1371// in Phase B when lua-vm is compiled), (2) generic_reader's
1372// self-referential &mut borrow needs architectural resolution,
1373// (3) GC API stubs (gc_count, gc_step, …) need Phase D
1374// implementations, (4) I/O host capabilities now route through
1375// state/global hooks, but stdin/env/time/temp remain incomplete,
1376// (5) pcallk / callk continuations are
1377// stubbed pending coroutine support in Phase E. The fake
1378// `struct LuaState;` placeholder here avoids duplicate-definition
1379// errors while keeping the file self-contained; Phase B removes it.
1380// ──────────────────────────────────────────────────────────────────────────────