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