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