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