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