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 state.gc().obj_barrier(lcl, &uv);
740 }
741 // A Lua closure that references no globals has no `_ENV` upvalue and
742 // nothing reads globals through it, so the set is inert; 5.1 still
743 // accepts it and returns the function. (Gap: a subsequent
744 // `getfenv` on such a closure returns the thread globals rather than
745 // the set table — see specs/followup/5.1-fenv.md §4.)
746 }
747 _ => {
748 // C/Rust functions cannot have their environment changed. 5.1
749 // raises this exact message (via luaL_error, so it carries the
750 // caller's source location) for any object whose env is fixed.
751 return Err(state.where_error(1, b"'setfenv' cannot change environment of given object"));
752 }
753 }
754 state.push(func);
755 Ok(1)
756}
757
758/// Set the environment of the Lua closure `level` frames up the running stack
759/// to `new_env`, the internal equivalent of `setfenv(level, new_env)`.
760///
761/// Used by `module` (5.1 `package` library), which sets its caller's
762/// environment to the module table. A non-Lua function (or a closure with no
763/// `_ENV` upvalue) is left unchanged, matching the inert-set behavior of
764/// `setfenv`. See specs/followup/5.1-fenv.md.
765pub(crate) fn set_func_env_at_level(
766 state: &mut LuaState,
767 level: i64,
768 new_env: LuaValue,
769) -> Result<(), LuaError> {
770 let func = fenv_getfunc(state, level)?;
771 if let LuaValue::Function(LuaClosure::Lua(lcl)) = &func {
772 if let Some(idx) = fenv_env_upval_index(lcl) {
773 let uv = state.new_upval_closed(new_env);
774 lcl.set_upval(idx, uv);
775 state.gc().obj_barrier(lcl, &uv);
776 }
777 }
778 Ok(())
779}
780
781// ── next ──────────────────────────────────────────────────────────────────────
782
783/// Table traversal iterator: given a table and a key, pushes the next key-value
784/// pair. Pushes nil and returns 1 when the traversal is exhausted.
785///
786pub(crate) fn next_fn(state: &mut LuaState) -> Result<usize, LuaError> {
787 state.check_arg_type(1, LuaType::Table)?;
788 lua_vm::api::set_top(state, 2)?;
789 if state.table_next(1)? {
790 Ok(2)
791 } else {
792 state.push(LuaValue::Nil);
793 Ok(1)
794 }
795}
796
797// ── pairs continuation (coroutine stub) ───────────────────────────────────────
798
799/// Continuation for `pairs` when the `__pairs` metamethod yields.
800/// Re-invoked by `finishCcall` after the yielded `__pairs` resumes.
801///
802fn pairs_cont(_state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
803 Ok(3)
804}
805
806// ── pairs ─────────────────────────────────────────────────────────────────────
807
808/// Returns the `next` function, the table, and nil (or invokes a `__pairs`
809/// metamethod).
810///
811pub(crate) fn pairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
812 state.check_arg_any(1)?;
813 // Lua 5.1 has no `__pairs` metamethod; `pairs(t)` always iterates the raw
814 // table even when a `__pairs` is set (it is silently ignored). `__pairs`
815 // was added in 5.2 and removed again in 5.4, so only consult it off V51.
816 let consult_pairs_tm = !matches!(state.global().lua_version, lua_types::LuaVersion::V51);
817 if !consult_pairs_tm || state.get_metafield(1, b"__pairs")? == LuaType::Nil {
818 state.push_c_function(next_fn)?;
819 state.push_copy(1)?;
820 state.push(LuaValue::Nil);
821 } else {
822 state.push_copy(1)?;
823 state.call_k(1, 3, 0, Some(pairs_cont))?;
824 }
825 Ok(3)
826}
827
828// ── ipairs auxiliary ──────────────────────────────────────────────────────────
829
830/// Iterator step function for `ipairs`: increments the counter and fetches
831/// the next array element. Returns the index + value, or just the index when
832/// the value is nil (signalling end-of-iteration).
833///
834fn ipairs_aux(state: &mut LuaState) -> Result<usize, LuaError> {
835 let i = state.check_arg_integer(2)?;
836 // luaL_intop(+, a, b) → wrapping integer addition (PORTING.md §9 / macros.tsv `intop`)
837 let i = (i as u64).wrapping_add(1u64) as i64;
838 state.push(LuaValue::Int(i));
839 let t = state.get_i(1, i)?;
840 if t == LuaType::Nil {
841 Ok(1)
842 } else {
843 Ok(2)
844 }
845}
846
847// ── ipairs ────────────────────────────────────────────────────────────────────
848
849/// Returns the `ipairsaux` iterator, the table, and 0 as the initial counter.
850///
851pub(crate) fn ipairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
852 state.check_arg_any(1)?;
853 state.push_c_function(ipairs_aux)?;
854 state.push_copy(1)?;
855 state.push(LuaValue::Int(0));
856 Ok(3)
857}
858
859// ── loadfile ──────────────────────────────────────────────────────────────────
860
861/// Loads a Lua chunk from a file.
862///
863pub(crate) fn loadfile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
864 let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
865 let mode: Option<Vec<u8>> = state.opt_arg_lstring(2, None)?;
866 let env = if state.type_at(3) != LuaType::None { 3 } else { 0 };
867 let status_ok = state.load_file_ex(fname.as_deref(), mode.as_deref())?;
868 load_aux(state, status_ok, env)
869}
870
871// ── generic_reader ────────────────────────────────────────────────────────────
872
873/// Reader callback for `luaB_load` when the chunk source is a Lua function.
874/// Calls the function at stack[1] repeatedly to obtain successive chunks.
875///
876///
877/// PORT NOTE: In C this is a `lua_Reader` function pointer passed to
878/// `lua_load`. In Rust, readers are closures — but `generic_reader` itself
879/// needs `&mut LuaState`, which conflicts with `state.load_with_reader`'s
880/// own borrow. The current translation materialises the reader as a free
881/// function for documentation purposes; Phase B must resolve the design
882/// (e.g., a separate reader-context type, or a split between "advance reader"
883/// and "run Lua call" phases).
884/// TODO(port): generic_reader — self-referential &mut borrow when used as lua_load callback.
885fn generic_reader(state: &mut LuaState) -> Result<Option<Vec<u8>>, LuaError> {
886 state.ensure_stack(2, b"too many nested functions")?;
887 state.push_copy(1)?;
888 state.call(0, 1)?;
889 if state.type_at(-1) == LuaType::Nil {
890 state.pop_n(1);
891 return Ok(None);
892 }
893 // luaL_error(L, "reader function must return a string");
894 // lua_isstring in C is true for strings AND coercible numbers.
895 if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
896 return Err(LuaError::runtime(format_args!(
897 "reader function must return a string"
898 )));
899 }
900 state.replace(RESERVED_SLOT)?;
901 let bytes = state
902 .to_lua_string_bytes(RESERVED_SLOT)
903 .map(|b| b.to_vec());
904 Ok(bytes)
905}
906
907// ── load ──────────────────────────────────────────────────────────────────────
908
909/// Loads a Lua chunk from a string or a reader function.
910///
911pub(crate) fn load_fn(state: &mut LuaState) -> Result<usize, LuaError> {
912 // Lua 5.1's `load` takes a *reader function only* — string loading is
913 // `loadstring`'s job. `load("...")` errors with `function expected, got
914 // string`. The string-or-function overload is a 5.2 addition. Verified
915 // against lua5.1.5; see specs/followup/5.1-roster-syntax.md §1.
916 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
917 state.check_arg_type(1, LuaType::Function)?;
918 }
919 // Determine whether argument 1 is a string (load from buffer) or a
920 // function (load from reader).
921 let is_string = matches!(state.type_at(1), LuaType::String | LuaType::Number);
922 let mode: Vec<u8> = state.opt_arg_string(3, b"bt")?;
923 let env = if state.type_at(4) != LuaType::None { 4 } else { 0 };
924 let status_ok = if is_string {
925 let chunk: Vec<u8> = state.to_lua_string_bytes(1).unwrap_or_default();
926 let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
927 chunk.clone()
928 } else {
929 state.check_arg_string(2)?
930 };
931 state.load_buffer_ex(&chunk, &chunkname, &mode)?
932 } else {
933 let chunkname: Vec<u8> = state
934 .opt_arg_string_bytes(2)
935 .unwrap_or_else(|_| b"=(load)".to_vec());
936 state.check_arg_type(1, LuaType::Function)?;
937 lua_vm::api::set_top(state, RESERVED_SLOT)?;
938 // TODO(port): generic_reader cannot be passed directly due to self-referential
939 // &mut borrow — see generic_reader's PORT NOTE. Phase B resolves this.
940 state.load_with_reader(generic_reader, &chunkname, &mode)?
941 };
942 load_aux(state, status_ok, env)
943}
944
945/// `loadstring(s [, chunkname])` — Lua 5.1 only.
946///
947/// Loads a string as a Lua chunk. In 5.1 this is the string-loading counterpart
948/// to `load` (which takes a reader function only). The second argument is the
949/// chunk name. Verified against lua5.1.5; see
950/// specs/followup/5.1-roster-syntax.md §1.
951pub(crate) fn loadstring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
952 let chunk: Vec<u8> = state.check_arg_string(1)?;
953 let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
954 chunk.clone()
955 } else {
956 state.check_arg_string(2)?
957 };
958 let status_ok = state.load_buffer_ex(&chunk, &chunkname, b"bt")?;
959 load_aux(state, status_ok, 0)
960}
961
962/// `gcinfo()` — Lua 5.1 only. Returns the amount of memory in use by Lua, in
963/// kilobytes. A deprecated holdover of `collectgarbage("count")` that returns
964/// just the integer KB count. Verified against lua5.1.5: returns a number. See
965/// specs/followup/5.1-roster-syntax.md §1.
966pub(crate) fn gcinfo_fn(state: &mut LuaState) -> Result<usize, LuaError> {
967 let k = state.gc_count()?;
968 state.push(LuaValue::Int(k as i64));
969 Ok(1)
970}
971
972/// `newproxy([boolean | proxy])` — Lua 5.1 only.
973///
974/// Creates a zero-size userdata (a "proxy"). With no argument or `false`, the
975/// proxy has no metatable. With `true`, it gets a fresh empty metatable (so a
976/// host can install `__gc`/`__len`, the userdata idiom these metamethods need
977/// in 5.1). With another proxy, it shares that proxy's metatable. Mirrors
978/// `luaB_newproxy` in 5.1 `lbaselib.c`; see specs/followup/5.1-roster-syntax.md
979/// §1. The C version validates the proxy argument against a weak table of
980/// metatables it created; this port instead accepts any userdata that carries a
981/// metatable, which is observably equivalent for the proxy idiom.
982pub(crate) fn newproxy_fn(state: &mut LuaState) -> Result<usize, LuaError> {
983 lua_vm::api::set_top(state, 1)?;
984 // The new userdata is pushed at stack position 2.
985 state.new_userdata_typed(b"", 0, 0)?;
986 if !state.to_boolean(1) {
987 return Ok(1); // no metatable
988 }
989 if matches!(state.type_at(1), LuaType::Boolean) {
990 // `true`: create and attach a fresh empty metatable.
991 let mt = state.new_table();
992 state.push(LuaValue::Table(mt));
993 state.set_metatable(2)?;
994 } else {
995 // A proxy argument: share its metatable. Validate it is a userdata that
996 // carries one (the C version checks a weak table of valid metatables).
997 let is_proxy =
998 matches!(state.type_at(1), LuaType::UserData) && state.get_metatable(1)?;
999 if !is_proxy {
1000 return Err(lua_vm::debug::arg_error_impl(state, 1, b"boolean or proxy expected"));
1001 }
1002 // get_metatable pushed arg1's metatable on top; attach it to the proxy.
1003 state.set_metatable(2)?;
1004 }
1005 Ok(1)
1006}
1007
1008// ── dofile ────────────────────────────────────────────────────────────────────
1009
1010/// Loads and runs a Lua file, forwarding all return values.
1011///
1012fn dofile_cont(state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
1013 Ok((state.top() as i32 - 1) as usize)
1014}
1015
1016pub(crate) fn dofile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1017 let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
1018 lua_vm::api::set_top(state, 1)?;
1019 if !state.load_file(fname.as_deref())? {
1020 return Err(LuaError::from_value(state.pop()));
1021 }
1022 state.call_k(0, LUA_MULTRET, 0, Some(dofile_cont))?;
1023 dofile_cont(state, 0, 0)
1024}
1025
1026// ── assert ────────────────────────────────────────────────────────────────────
1027
1028/// Raises an error if the first argument is falsy, otherwise passes all
1029/// arguments through as return values.
1030///
1031pub(crate) fn assert_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1032 if state.to_boolean(1) {
1033 return Ok(state.top() as usize);
1034 }
1035 state.check_arg_any(1)?;
1036 state.remove(1)?;
1037 state.push_string(b"assertion failed!")?;
1038 lua_vm::api::set_top(state, 1)?;
1039 error_fn(state)
1040}
1041
1042// ── select ────────────────────────────────────────────────────────────────────
1043
1044/// Returns a slice of its arguments starting at the given index, or returns
1045/// the count of arguments when called with `"#"`.
1046///
1047pub(crate) fn select_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1048 let n = state.top() as i64;
1049 // Check for '#' first byte without holding a borrow across subsequent ops.
1050 let first_is_hash = state.type_at(1) == LuaType::String && {
1051 state
1052 .to_lua_string_bytes(1)
1053 .and_then(|b| b.first().copied())
1054 == Some(b'#')
1055 };
1056 if first_is_hash {
1057 state.push(LuaValue::Int(n - 1));
1058 return Ok(1);
1059 }
1060 let mut i = state.check_arg_integer(1)?;
1061 if i < 0 {
1062 i = n + i;
1063 } else if i > n {
1064 i = n;
1065 }
1066 if i < 1 {
1067 return Err(lua_vm::debug::arg_error_impl(state, 1, b"index out of range"));
1068 }
1069 // The values at stack positions [i+1 .. n] are already in place; the
1070 // runtime picks up the top (n - i) of them as results.
1071 Ok((n - i) as usize)
1072}
1073
1074// ── pcall ─────────────────────────────────────────────────────────────────────
1075
1076/// Protected call: returns true + results on success, or false + error on
1077/// failure.
1078///
1079pub(crate) fn pcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1080 state.check_arg_any(1)?;
1081 // Stack before: [f, a1, …, aN]
1082 // Stack after: [true, f, a1, …, aN]
1083 state.push(LuaValue::Bool(true));
1084 state.insert(1)?;
1085 // nargs = gettop - 2 (subtract the sentinel `true` and the function).
1086 let nargs = state.top() as i32 - 2;
1087 let yieldable = state.is_yieldable();
1088 let ok = match state.protected_call_k(nargs, LUA_MULTRET, 0, 0, Some(finish_pcall_k)) {
1089 Ok(()) => true,
1090 // `LuaError::Yield` must bubble up to `lua_resume` so the continuation
1091 // saved on this frame can be invoked on resume.
1092 Err(LuaError::Yield) => return Err(LuaError::Yield),
1093 // A sandbox budget trip is uncatchable: re-raise instead of catching so
1094 // untrusted code cannot defeat the budget with `while true do pcall(..) end`.
1095 Err(e) if state.sandbox_aborting() => return Err(e),
1096 Err(e) if yieldable => return Err(e),
1097 Err(e) => {
1098 state.push(e.into_value());
1099 false
1100 }
1101 };
1102 finish_pcall(state, ok, 0)
1103}
1104
1105/// Continuation matching `LuaKFunction`. Invoked by `finishCcall` on the
1106/// resume path after a yield through pcall (or after a `__close` ran during
1107/// pcall error recovery).
1108///
1109fn finish_pcall_k(state: &mut LuaState, status: i32, extra: isize) -> Result<usize, LuaError> {
1110 let ok = status == LuaStatus::Ok as i32 || status == LuaStatus::Yield as i32;
1111 finish_pcall(state, ok, extra as i32)
1112}
1113
1114// ── xpcall ────────────────────────────────────────────────────────────────────
1115
1116/// Protected call with a separate error-handler function.
1117///
1118pub(crate) fn xpcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1119 // Lua 5.1's `xpcall(f, h)` does NOT forward extra arguments to `f` — `f` is
1120 // always called with zero arguments. The extra-argument forwarding is a 5.2
1121 // addition. Verified against lua5.1.5: `xpcall(fn, h, 1,2,3)` calls `fn`
1122 // with `select("#",...) == 0`. Drop any args past the handler. See
1123 // specs/followup/5.1-roster-syntax.md §1.
1124 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) && state.top() > 2 {
1125 lua_vm::api::set_top(state, 2)?;
1126 }
1127 let n = state.top() as i32;
1128 state.check_arg_type(2, LuaType::Function)?;
1129 // Stack before rotate: [f, err, a1, …, aN, true, f]
1130 // Stack after rotate: [f, err, true, f, a1, …, aN]
1131 state.push(LuaValue::Bool(true));
1132 state.push_copy(1)?;
1133 state.rotate(3, 2)?;
1134 // errfunc is at stack index 2; extra=2 means finishpcall skips 2 values.
1135 let yieldable = state.is_yieldable();
1136 let ok = match state.protected_call_k(n - 2, LUA_MULTRET, 2, 2, Some(finish_pcall_k)) {
1137 Ok(()) => true,
1138 Err(LuaError::Yield) => return Err(LuaError::Yield),
1139 // Uncatchable sandbox abort: re-raise without running the message
1140 // handler, so an `xpcall` handler can neither swallow nor loop on it.
1141 Err(e) if state.sandbox_aborting() => return Err(e),
1142 Err(e) if yieldable => return Err(e),
1143 Err(e) => {
1144 state.push(e.into_value());
1145 false
1146 }
1147 };
1148 finish_pcall(state, ok, 2)
1149}
1150
1151// ── tostring ──────────────────────────────────────────────────────────────────
1152
1153/// Converts any value to its string representation (calls `__tostring` if
1154/// present).
1155///
1156pub(crate) fn tostring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
1157 state.check_arg_any(1)?;
1158 // to_display_string pushes the converted string and returns a handle to it.
1159 // TODO(port): to_display_string method needs implementing on LuaState.
1160 state.to_display_string(1)?;
1161 Ok(1)
1162}
1163
1164// ── Registration table ────────────────────────────────────────────────────────
1165
1166/// All base-library functions registered into the global table by `open`.
1167///
1168///
1169/// PORT NOTE: The C table includes placeholder entries
1170/// `{LUA_GNAME, NULL}` and `{"_VERSION", NULL}` that `luaopen_base` fills in
1171/// separately. Those are omitted here; `open()` sets them explicitly.
1172pub(crate) const BASE_FUNCS: &[(&[u8], LuaLibFn)] = &[
1173 (b"assert", assert_fn),
1174 (b"collectgarbage", collectgarbage_fn),
1175 (b"dofile", dofile_fn),
1176 (b"error", error_fn),
1177 (b"getmetatable", getmetatable_fn),
1178 (b"ipairs", ipairs_fn),
1179 (b"loadfile", loadfile_fn),
1180 (b"load", load_fn),
1181 (b"next", next_fn),
1182 (b"pairs", pairs_fn),
1183 (b"pcall", pcall_fn),
1184 (b"print", print_fn),
1185 (b"warn", warn_fn),
1186 (b"rawequal", rawequal_fn),
1187 (b"rawlen", rawlen_fn),
1188 (b"rawget", rawget_fn),
1189 (b"rawset", rawset_fn),
1190 (b"select", select_fn),
1191 (b"setmetatable", setmetatable_fn),
1192 (b"tonumber", tonumber_fn),
1193 (b"tostring", tostring_fn),
1194 (b"type", type_fn),
1195 (b"xpcall", xpcall_fn),
1196];
1197
1198// ── Module opener ─────────────────────────────────────────────────────────────
1199
1200/// Open the base library: register all base functions into the global table,
1201/// then set `_G` (a self-reference) and `_VERSION`.
1202///
1203pub fn open(state: &mut LuaState) -> Result<usize, LuaError> {
1204 state.push_globals()?;
1205 state.set_funcs(BASE_FUNCS, 0)?;
1206 state.push_copy(-1)?;
1207 state.set_field(-2, LUA_GNAME)?;
1208 let version_str = state.global().lua_version.version_str();
1209 state.push_string(version_str.as_bytes())?;
1210 state.set_field(-2, b"_VERSION")?;
1211 // `warn` was introduced in Lua 5.4; it is absent on 5.1/5.2/5.3.
1212 if matches!(
1213 state.global().lua_version,
1214 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52 | lua_types::LuaVersion::V53
1215 ) {
1216 state.push(LuaValue::Nil);
1217 state.set_field(-2, b"warn")?;
1218 }
1219 // Lua 5.1/5.2 carry two globals that were removed in 5.3: `unpack` (an alias
1220 // of `table.unpack`) and `loadstring` (an alias of `load`). Verified against
1221 // lua5.2.4: both are functions. The base table is on the stack top here.
1222 if matches!(
1223 state.global().lua_version,
1224 lua_types::LuaVersion::V51 | lua_types::LuaVersion::V52
1225 ) {
1226 state.push_c_function(crate::table_lib::unpack)?;
1227 state.set_field(-2, b"unpack")?;
1228 }
1229 // `loadstring` aliases `load` in 5.2 (whose `load` accepts a string), but in
1230 // 5.1 `load` is reader-only, so `loadstring` is a distinct string-loader.
1231 // Both are absent in 5.3+. See specs/followup/5.1-roster-syntax.md §1.
1232 if matches!(state.global().lua_version, lua_types::LuaVersion::V52) {
1233 state.push_c_function(load_fn)?;
1234 state.set_field(-2, b"loadstring")?;
1235 }
1236 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1237 state.push_c_function(loadstring_fn)?;
1238 state.set_field(-2, b"loadstring")?;
1239 // `gcinfo()` and `newproxy()` are 5.1 holdovers absent in 5.2+.
1240 state.push_c_function(gcinfo_fn)?;
1241 state.set_field(-2, b"gcinfo")?;
1242 state.push_c_function(newproxy_fn)?;
1243 state.set_field(-2, b"newproxy")?;
1244 // `rawlen` is a Lua 5.2 addition; it is absent in 5.1. Verified against
1245 // lua5.1.5: `type(rawlen)` == "nil". It lives in BASE_FUNCS (registered
1246 // for every version), so withhold it under V51.
1247 state.push(LuaValue::Nil);
1248 state.set_field(-2, b"rawlen")?;
1249 }
1250 // Lua 5.1's fenv-based globals model: `getfenv`/`setfenv` read and write a
1251 // function's environment (its `_ENV` upvalue under the reused modern core)
1252 // or the running thread's global table for level 0. Both were removed in
1253 // 5.2 (which switched to lexical `_ENV`), so they are V51-only. See
1254 // specs/followup/5.1-fenv.md.
1255 if matches!(state.global().lua_version, lua_types::LuaVersion::V51) {
1256 state.push_c_function(getfenv_fn)?;
1257 state.set_field(-2, b"getfenv")?;
1258 state.push_c_function(setfenv_fn)?;
1259 state.set_field(-2, b"setfenv")?;
1260 }
1261 Ok(1)
1262}
1263
1264// ──────────────────────────────────────────────────────────────────────────────
1265// PORT STATUS
1266// source: src/lbaselib.c (549 lines, 32 functions)
1267// target_crate: lua-stdlib
1268// confidence: medium
1269// todos: 21
1270// port_notes: 5
1271// unsafe_blocks: 0
1272// notes: All 32 C functions translated. Main uncertainties are (1)
1273// LuaState method signatures (top/type_at/push/… — resolved
1274// in Phase B when lua-vm is compiled), (2) generic_reader's
1275// self-referential &mut borrow needs architectural resolution,
1276// (3) GC API stubs (gc_count, gc_step, …) need Phase D
1277// implementations, (4) I/O host capabilities now route through
1278// state/global hooks, but stdin/env/time/temp remain incomplete,
1279// (5) pcallk / callk continuations are
1280// stubbed pending coroutine support in Phase E. The fake
1281// `struct LuaState;` placeholder here avoids duplicate-definition
1282// errors while keeping the file self-contained; Phase B removes it.
1283// ──────────────────────────────────────────────────────────────────────────────