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