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