Skip to main content

lua_vm/
api.rs

1//
2// PORT NOTE: This is the Rust-native translation of lapi.c.
3// The C-API surface (lua_State *, int stack-index protocol) is replaced by
4// methods on LuaState.  `lua_lock` / `lua_unlock` are dropped (no-op in the
5// single-threaded default build).  `api_incr_top` is dropped; `state.push()`
6// already increments.  Stack pointers (StkId) become StackIdx (u32).
7
8#![allow(dead_code)]
9
10use std::convert::Infallible;
11#[allow(unused_imports)] use crate::prelude::*;
12
13use crate::state::{LuaState, LuaCFunction, LuaCallable, StackIdx,
14    LuaValueExt, LuaTypeExt, StackIdxExt,
15    LuaTableRefExt, LuaUserDataRefExt};
16use lua_types::{
17    LuaValue, LuaType, LuaError, LuaString, LuaUserData, LuaClosure,
18    GcRef, LuaStatus,
19};
20use lua_types::value::LuaTable;
21
22pub const LUA_IDENT: &[u8] =
23    b"$LuaVersion: Lua 5.4.7  Copyright (C) 1994-2024 Lua.org, PUC-Rio $\
24      $LuaAuthors: R. Ierusalimschy, L. H. Figueiredo, W. Celes $";
25
26const LUA_REGISTRYINDEX: i32 = -(1_000_000) - 1000;
27
28const LUA_MULTRET: i32 = -1;
29
30const LUA_RIDX_GLOBALS: i64 = 2;
31
32const MAX_UPVAL: u8 = 255;
33
34#[inline]
35fn is_pseudo(idx: i32) -> bool {
36    idx <= LUA_REGISTRYINDEX
37}
38
39#[inline]
40fn is_upvalue(idx: i32) -> bool {
41    idx < LUA_REGISTRYINDEX
42}
43
44// PORT NOTE: In C, the only "invalid" TValue is the global nilvalue singleton
45// pointer returned by index2value when the index is out of range. In Rust we
46// cannot do pointer-equality on a singleton, so validity is decided by whether
47// the index resolves to a real stack/upvalue slot — see `is_valid_index`.
48#[inline]
49fn is_valid_index(state: &LuaState, idx: i32) -> bool {
50    if idx == 0 {
51        return false;
52    }
53    let ci = state.current_call_info();
54    if idx > 0 {
55        let slot = ci.func + idx;
56        slot.0 < state.top_idx().0
57    } else if !is_pseudo(idx) {
58        (-idx) as u32 <= state.top_idx().0.saturating_sub(ci.func.0 + 1)
59    } else if idx == LUA_REGISTRYINDEX {
60        true
61    } else {
62        let upval_n = (LUA_REGISTRYINDEX - idx) as usize;
63        let func_val = state.get_at(ci.func);
64        if let LuaValue::Function(LuaClosure::C(ref ccl)) = func_val {
65            upval_n >= 1 && upval_n <= ccl.upvalues.len()
66        } else {
67            false
68        }
69    }
70}
71
72// ── index helpers ─────────────────────────────────────────────────────────────
73
74// PORT NOTE: In Rust we cannot return a pointer; we return a cloned LuaValue.
75// Writers use a companion index_to_stack_idx() for actual stack slots.
76fn index_to_value(state: &LuaState, idx: i32) -> LuaValue {
77    let ci = state.current_call_info();
78    if idx > 0 {
79        let func_idx = ci.func;
80        let slot = func_idx + idx;
81        debug_assert!(
82            idx as u32 <= ci.top.saturating_sub(func_idx + 1),
83            "unacceptable index"
84        );
85        if slot.0 >= state.top_idx().0 {
86            LuaValue::Nil
87        } else {
88            state.get_at(slot)
89        }
90    } else if !is_pseudo(idx) {
91        // negative index
92        debug_assert!(
93            idx != 0,
94            "invalid index"
95        );
96        let top = state.top_idx();
97        let slot = (top.0 as i32 + idx) as u32;
98        state.get_at(slot)
99    } else if idx == LUA_REGISTRYINDEX {
100        state.registry_value()
101    } else {
102        // upvalues: idx = LUA_REGISTRYINDEX - idx  (idx < LUA_REGISTRYINDEX)
103        let upval_n = (LUA_REGISTRYINDEX - idx) as usize;
104        debug_assert!(upval_n <= MAX_UPVAL as usize + 1, "upvalue index too large");
105        let func_val = state.get_at(ci.func);
106        if let LuaValue::Function(LuaClosure::C(ref ccl)) = func_val {
107            // C closure upvalue
108            if upval_n >= 1 && upval_n <= ccl.upvalues.len() {
109                ccl.upvalues[upval_n - 1].clone()
110            } else {
111                LuaValue::Nil
112            }
113        } else {
114            LuaValue::Nil
115        }
116    }
117}
118
119// Returns a StackIdx for a valid (non-pseudo) actual stack slot.
120#[inline]
121fn index_to_stack_idx(state: &LuaState, idx: i32) -> StackIdx {
122    let ci = state.current_call_info();
123    if idx > 0 {
124        let slot = ci.func + idx;
125        debug_assert!(slot.0 < state.top_idx().0, "invalid index");
126        slot
127    } else {
128        debug_assert!(idx != 0 && !is_pseudo(idx), "invalid index");
129        StackIdx((state.top_idx().0 as i32 + idx) as u32)
130    }
131}
132
133// ── stack manipulation ────────────────────────────────────────────────────────
134
135pub fn check_stack(state: &mut LuaState, n: i32) -> bool {
136    debug_assert!(n >= 0, "negative 'n'");
137    let available = state.stack_available();
138    let res = if available > n as usize {
139        true
140    } else {
141        crate::do_::grow_stack(state, n, false).unwrap_or(false)
142    };
143    if res {
144        let needed_top = state.top_idx() + n as i32;
145        let ci_idx = state.current_ci_idx();
146        if state.get_ci(ci_idx).top.0 < needed_top.0 {
147            let live_top = state.top_idx();
148            state.get_ci_mut(ci_idx).top = needed_top;
149            state.clear_stack_range(live_top, needed_top);
150        }
151    }
152    res
153}
154
155/// Move the top `n` values from `from`'s stack onto `to`'s stack.
156///
157/// Both threads must share the same `GlobalState` (i.e. one is a
158/// coroutine the other created via `coroutine.create`). Calling with
159/// `from` == `to` is a no-op. Equivalent to:
160///
161/// ```text
162/// args = from.stack[top-n..top].clone();
163/// from.set_top(top - n);
164/// for v in args { to.push(v); }
165/// ```
166///
167///
168/// Phase E-3: implemented for the same-`GlobalState` case (the only one
169/// `lua-stdlib` uses today). `lua-vm` callers should prefer this helper
170/// over hand-rolling the snapshot/push dance.
171pub fn xmove(from: &mut LuaState, to: &mut LuaState, n: i32) {
172    if n <= 0 {
173        return;
174    }
175    if std::ptr::eq(from as *const LuaState, to as *const LuaState) {
176        return;
177    }
178    let abs_top = from.top_idx().0 as i32;
179    debug_assert!(abs_top >= n, "lua_xmove: from stack underflow");
180    let first_abs = abs_top - n;
181    let mut buf: Vec<lua_types::LuaValue> = Vec::with_capacity(n as usize);
182    for i in 0..n {
183        let idx = StackIdx((first_abs + i) as u32);
184        buf.push(from.get_at(idx));
185    }
186    from.set_top(StackIdx(first_abs as u32));
187    for v in buf {
188        to.push(v);
189    }
190}
191
192pub fn at_panic(
193    state: &mut LuaState,
194    panicf: Option<fn(&mut LuaState) -> Result<usize, LuaError>>,
195) -> Option<fn(&mut LuaState) -> Result<usize, LuaError>> {
196    let old = state.global_mut().panic;
197    state.global_mut().panic = panicf;
198    old
199}
200
201pub fn version(_state: &LuaState) -> f64 {
202    504.0
203}
204
205pub fn abs_index(state: &LuaState, idx: i32) -> i32 {
206    //          : cast_int(L->top.p - L->ci->func.p) + idx;
207    if idx > 0 || is_pseudo(idx) {
208        idx
209    } else {
210        let ci = state.current_call_info();
211        (state.top_idx().0 as i32 - ci.func.0 as i32) + idx
212    }
213}
214
215pub fn get_top(state: &LuaState) -> i32 {
216    let ci = state.current_call_info();
217    (state.top_idx().0 as i32) - (ci.func.0 as i32 + 1)
218}
219
220pub fn set_top(state: &mut LuaState, idx: i32) -> Result<(), LuaError> {
221    let func = state.current_call_info().func;
222    let ci_top = state.current_call_info().top;
223    if idx >= 0 {
224        debug_assert!(
225            idx as u32 <= ci_top.saturating_sub(func + 1),
226            "new top too large"
227        );
228        let new_top = func + 1 + idx as i32;
229        let old_top = state.top_idx();
230        if new_top.0 > old_top.0 {
231            for i in old_top.0..new_top.0 {
232                state.set_at(i, LuaValue::Nil);
233            }
234        }
235        // TODO(port): to-be-closed variable closing on stack shrink;
236        // luaF_close not yet translated. Skipping close logic for Phase A.
237        state.set_top_idx(new_top);
238    } else {
239        debug_assert!(
240            -(idx + 1) <= (state.top_idx().0 as i32 - (func.0 as i32 + 1)),
241            "invalid new top"
242        );
243        let new_top = (state.top_idx().0 as i32 + idx + 1) as u32;
244        // TODO(port): to-be-closed variable closing on stack shrink (same as above)
245        state.set_top_idx(new_top);
246    }
247    Ok(())
248}
249
250pub fn close_slot(state: &mut LuaState, idx: i32) -> Result<(), LuaError> {
251    let level = index_to_stack_idx(state, idx);
252    // TODO(port): tbc-list check and luaF_close not yet translated.
253    state.set_at(level, LuaValue::Nil);
254    Ok(())
255}
256
257#[inline]
258fn reverse_segment(state: &mut LuaState, from: StackIdx, to: StackIdx) {
259    let mut lo = from.0;
260    let mut hi = to.0;
261    while lo < hi {
262        let temp = state.get_at(StackIdx(lo));
263        let hi_val = state.get_at(StackIdx(hi));
264        state.set_at(StackIdx(lo), hi_val);
265        state.set_at(StackIdx(hi), temp);
266        lo += 1;
267        hi -= 1;
268    }
269}
270
271pub fn rotate(state: &mut LuaState, idx: i32, n: i32) {
272    let t = state.top_idx() - 1;
273    let p = index_to_stack_idx(state, idx);
274    debug_assert!((n.unsigned_abs() as i32) <= ((t.0 as i32) - (p.0 as i32) + 1), "invalid 'n'");
275    let m = if n >= 0 {
276        t - n
277    } else {
278        StackIdx((p.0 as i32 - n - 1) as u32)
279    };
280    reverse_segment(state, p, m);
281    reverse_segment(state, m + 1, t);
282    reverse_segment(state, p, t);
283}
284
285pub fn copy(state: &mut LuaState, fromidx: i32, toidx: i32) {
286    let fr = index_to_value(state, fromidx);
287    if is_upvalue(toidx) {
288        // Writing to a function upvalue pseudo-index
289        let upval_n = (LUA_REGISTRYINDEX - toidx) as usize;
290        let func_val = state.get_at(state.current_call_info().func);
291        if let LuaValue::Function(LuaClosure::C(ref ccl)) = func_val {
292            // TODO(port): CClosure upvalue write requires interior mutability on GcRef<CClosure>
293            // state.gc().barrier(ccl, &fr);
294            let _ = (upval_n, ccl);
295        }
296        // TODO(port): implement upvalue write for copy() to C closure upvalues
297    } else if toidx == LUA_REGISTRYINDEX {
298        // TODO(port): write to registry — needs GlobalState::set_registry(fr)
299    } else {
300        let to_slot = index_to_stack_idx(state, toidx);
301        state.set_at(to_slot, fr);
302    }
303}
304
305pub fn push_value(state: &mut LuaState, idx: i32) {
306    let v = index_to_value(state, idx);
307    state.push(v);
308}
309
310/// Inherent `push_copy` so the `LuaStateStubExt::push_copy` default
311/// `todo!()` no longer fires. Phase-A `state.push_copy(idx)` call-sites
312/// (base.rs, etc.) duplicate the value at `idx` onto the top of the stack —
313/// the same semantics as `lua_pushvalue`.
314impl LuaState {
315    pub fn push_copy(&mut self, idx: i32) -> Result<(), LuaError> {
316        push_value(self, idx);
317        Ok(())
318    }
319
320    pub fn push_value_at(&mut self, idx: i32) -> Result<(), LuaError> {
321        push_value(self, idx);
322        Ok(())
323    }
324
325    pub fn insert(&mut self, idx: i32) -> Result<(), LuaError> {
326        rotate(self, idx, 1);
327        Ok(())
328    }
329
330    /// Inherent `length_at` mirroring `luaL_len` from `lauxlib.c`: push the
331    /// value's length onto the stack (honouring `__len`), pop it as an
332    /// integer, and error if the result is not an integer. Defined on
333    /// `LuaState` so it overrides the `LuaStateStubExt::length_at` trait
334    /// default `todo!()`.
335    pub fn length_at(&mut self, idx: i32) -> Result<i64, LuaError> {
336        len(self, idx)?;
337        let l = match to_integer_x(self, -1) {
338            Some(n) => n,
339            None => {
340                return Err(LuaError::runtime(format_args!(
341                    "object length is not an integer"
342                )));
343            }
344        };
345        self.pop_n(1);
346        Ok(l)
347    }
348
349    /// Write `msg` bytes verbatim to standard output. Mirrors the C macro
350    /// `lua_writestring(s, l) = fwrite(s, 1, l, stdout)` from `lauxlib.h`,
351    /// used by `print` and friends. A failed write is propagated as a
352    /// `LuaError::runtime`; this matches C-Lua's behaviour where an I/O
353    /// error during `lua_writestring` would surface through the host's
354    /// error handling.
355    pub fn write_output(&mut self, msg: &[u8]) -> Result<(), LuaError> {
356        if let Some(write_fn) = self.global().stdout_hook {
357            write_fn(msg).map_err(|e| LuaError::runtime(format_args!("{}", e)))?;
358            return Ok(());
359        }
360
361        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
362        {
363            let _ = msg;
364            Err(LuaError::runtime(format_args!(
365                "stdout not available in this host"
366            )))
367        }
368
369        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
370        {
371            use std::io::Write;
372            let stdout = std::io::stdout();
373            let mut handle = stdout.lock();
374            handle
375                .write_all(msg)
376                .map_err(|e| LuaError::runtime(format_args!("{}", e)))?;
377            handle
378                .flush()
379                .map_err(|e| LuaError::runtime(format_args!("{}", e)))?;
380            Ok(())
381        }
382    }
383
384    /// Convert the value at `idx` to a display string, push the result onto
385    /// the stack, and return a copy of its bytes. Mirrors `luaL_tolstring`
386    /// from `lauxlib.c`. The default Lua formatting is used for primitives
387    /// (`"true"`/`"false"`/`"nil"`, `%I` integers, `%.14g` floats); for other
388    /// reference types the result is `"<typename>: 0x<hex pointer>"`.
389    ///
390    /// If the value has a `__tostring` metamethod, it is invoked first and its
391    /// (string) result is used in place of the default formatting (matching
392    pub fn to_display_string(&mut self, idx: i32) -> Result<Vec<u8>, LuaError> {
393        let abs = abs_index(self, idx);
394        let v = index_to_value(self, abs);
395        let mt: Option<GcRef<LuaTable>> = match &v {
396            LuaValue::Table(t) => t.metatable(),
397            LuaValue::UserData(u) => u.metatable(),
398            _ => self.global().mt[v.base_type() as usize].clone(),
399        };
400        if let Some(mt_ref) = mt {
401            let key = self.intern_str(b"__tostring")?;
402            let f = mt_ref.get_short_str(&key);
403            if !matches!(f, LuaValue::Nil) {
404                let func_idx = self.top_idx();
405                self.push(f);
406                self.push(v.clone());
407                if self.current_ci().is_lua_code() {
408                    self.do_call(func_idx, 1)?;
409                } else {
410                    self.do_call_no_yield(func_idx, 1)?;
411                }
412                let top = self.top_idx();
413                let result = self.get_at(StackIdx(top.0 - 1));
414                if let LuaValue::Str(s) = result {
415                    return Ok(s.as_bytes().to_vec());
416                }
417                return Err(LuaError::runtime(format_args!(
418                    "'__tostring' must return a string"
419                )));
420            }
421        }
422        let bytes: Vec<u8> = match &v {
423            LuaValue::Str(s) => {
424                let out = s.as_bytes().to_vec();
425                self.push(LuaValue::Str(s.clone()));
426                out
427            }
428            LuaValue::Int(_) | LuaValue::Float(_) => {
429                let s = crate::object::num_to_string(self, &v)?;
430                let out = s.as_bytes().to_vec();
431                self.push(LuaValue::Str(s));
432                out
433            }
434            LuaValue::Bool(b) => {
435                let lit: &[u8] = if *b { b"true" } else { b"false" };
436                let s = self.intern_str(lit)?;
437                self.push(LuaValue::Str(s));
438                lit.to_vec()
439            }
440            LuaValue::Nil => {
441                let s = self.intern_str(b"nil")?;
442                self.push(LuaValue::Str(s));
443                b"nil".to_vec()
444            }
445            _ => {
446                let kind = crate::tagmethods::obj_type_name(self, &v)?;
447                let ptr = to_pointer(self, abs).unwrap_or(0);
448                let mut buf = kind;
449                buf.extend_from_slice(b": 0x");
450                buf.extend_from_slice(format!("{:x}", ptr).as_bytes());
451                let s = self.intern_str(&buf)?;
452                self.push(LuaValue::Str(s));
453                buf
454            }
455        };
456        Ok(bytes)
457    }
458
459    /// (stack top minus the slot just after the frame's `func`).
460    ///
461    /// Receiver is `&mut self` to match the `LuaStateStubExt::top` trait
462    /// signature exactly; with a different receiver shape (`&self`), Rust's
463    /// method-resolution picks the trait default and the program panics on
464    /// `todo!("phase-b-reconcile: top")`.
465    pub fn top(&mut self) -> i32 {
466        get_top(self)
467    }
468
469    /// `LuaStateStubExt::get_top` trait method. Inherent method shadows the
470    /// trait default so the `todo!("phase-b-reconcile: get_top")` shim never
471    /// fires.
472    pub fn get_top(&mut self) -> i32 {
473        get_top(self)
474    }
475
476    /// stack index `idx`, or `LuaType::None` if `idx` falls outside the
477    /// active call frame. Inherent method shadows the
478    /// `LuaStateStubExt::type_at` trait default so the `todo!()` shim
479    /// never fires.
480    pub fn type_at(&mut self, idx: i32) -> LuaType {
481        lua_type_at(self, idx)
482    }
483
484    /// #N (value expected)` error if the slot at `arg` is `LUA_TNONE`
485    /// (i.e. beyond the active call frame's top). Otherwise a no-op.
486    ///
487    /// Inherent method on LuaState shadows the `LuaStateStubExt::check_arg_any`
488    /// trait default so the `todo!()` shim never fires.
489    pub fn check_arg_any(&mut self, arg: i32) -> Result<(), LuaError> {
490        if lua_type_at(self, arg) == LuaType::None {
491            return Err(LuaError::arg_error(arg, "value expected"));
492        }
493        Ok(())
494    }
495
496    /// at `arg` to a string via `lua_tolstring` (which coerces numbers to
497    /// their string form) and returns the bytes. Raises
498    /// `bad argument #N (string expected, got <type>)` if the value is not a
499    /// string and not number-coercible.
500    ///
501    /// Inherent method on LuaState shadows the `LuaStateStubExt::check_arg_string`
502    /// trait default so the `todo!()` shim never fires. Uses the free `to_lua_string`
503    /// helper here rather than `auxlib::check_lstring`, which routes through
504    /// `state.to_lua_string` / `state.type_name` — both still trait stubs.
505    pub fn check_arg_string(&mut self, arg: i32) -> Result<Vec<u8>, LuaError> {
506        match to_lua_string(self, arg)? {
507            Some(s) => Ok(s.as_bytes().to_vec()),
508            None => {
509                let got = index_to_value(self, arg);
510                let got_name = if lua_type_at(self, arg) == LuaType::None {
511                    b"no value".to_vec()
512                } else {
513                    crate::tagmethods::obj_type_name(self, &got)?
514                };
515                let extramsg = format!(
516                    "string expected, got {}",
517                    String::from_utf8_lossy(&got_name)
518                );
519                Err(crate::debug::arg_error_impl(self, arg, extramsg.as_bytes()))
520            }
521        }
522    }
523
524    /// `arg` to a `lua_Integer` (i64) via `lua_tointegerx` (which accepts
525    /// ints, floats with exact integer value, and string-form integers).
526    /// Raises `bad argument #N (number has no integer representation)` if
527    /// the value is a number but not representable as an integer, or
528    /// `bad argument #N (number expected, got <type>)` otherwise.
529    ///
530    /// Inherent method on LuaState shadows the `LuaStateStubExt::check_arg_integer`
531    /// trait default so the `todo!()` shim never fires. Uses the free
532    /// `to_integer_x` / `is_number` helpers in this file rather than
533    /// `auxlib::check_integer`, which routes through `state.to_integer_x`
534    /// and `state.type_name` — both still trait stubs.
535    pub fn check_arg_integer(&mut self, arg: i32) -> Result<i64, LuaError> {
536        match to_integer_x(self, arg) {
537            Some(d) => Ok(d),
538            None => {
539                if is_number(self, arg) {
540                    Err(LuaError::arg_error(
541                        arg,
542                        "number has no integer representation",
543                    ))
544                } else {
545                    let got = index_to_value(self, arg);
546                    let got_name = if lua_type_at(self, arg) == LuaType::None {
547                        b"no value".to_vec()
548                    } else {
549                        crate::tagmethods::obj_type_name(self, &got)?
550                    };
551                    let extramsg = format!(
552                        "number expected, got {}",
553                        String::from_utf8_lossy(&got_name)
554                    );
555                    Err(crate::debug::arg_error_impl(self, arg, extramsg.as_bytes()))
556                }
557            }
558        }
559    }
560
561    /// `arg` to an `f64` via `lua_tonumberx` (which accepts ints, floats,
562    /// and number-shaped strings) and raises `bad argument #N (number
563    /// expected, got <type>)` if the value is not number-coercible.
564    ///
565    /// Inherent method on LuaState shadows the `LuaStateStubExt::check_number`
566    /// trait default so the `todo!()` shim never fires. Uses the free
567    /// `to_number_x` helper here rather than `auxlib::check_number`, which
568    /// routes through `state.to_number_x` and `state.type_name` — both still
569    /// trait stubs.
570    pub fn check_number(&mut self, arg: i32) -> Result<f64, LuaError> {
571        match to_number_x(self, arg) {
572            Some(d) => Ok(d),
573            None => {
574                let got = index_to_value(self, arg);
575                let got_name = if lua_type_at(self, arg) == LuaType::None {
576                    b"no value".to_vec()
577                } else {
578                    crate::tagmethods::obj_type_name(self, &got)?
579                };
580                let extramsg = format!(
581                    "number expected, got {}",
582                    String::from_utf8_lossy(&got_name)
583                );
584                Err(crate::debug::arg_error_impl(self, arg, extramsg.as_bytes()))
585            }
586        }
587    }
588
589    /// `arg` is absent (`LUA_TNONE`) or `nil`, return `def`; otherwise
590    /// convert it to an integer (with the same string-to-number coercion
591    /// `lua_tointegerx` applies) and raise on failure.
592    ///
593    /// Inherent method on LuaState shadows the `LuaStateStubExt::opt_arg_integer`
594    /// trait default so the `todo!()` shim never fires. Implemented with the
595    /// free-function helpers in this file rather than `auxlib::opt_integer`
596    /// because the latter routes through `state.is_none_or_nil` and
597    /// `state.to_integer_x`, which are themselves stubbed.
598    pub fn opt_arg_integer(&mut self, arg: i32, def: i64) -> Result<i64, LuaError> {
599        match lua_type_at(self, arg) {
600            LuaType::None | LuaType::Nil => Ok(def),
601            _ => match to_integer_x(self, arg) {
602                Some(d) => Ok(d),
603                None => {
604                    if is_number(self, arg) {
605                        Err(LuaError::arg_error(
606                            arg,
607                            "number has no integer representation",
608                        ))
609                    } else {
610                        let got = index_to_value(self, arg);
611                        Err(LuaError::type_arg_error(arg, "number", &got))
612                    }
613                }
614            },
615        }
616    }
617
618    /// `lua_pcallk` with no continuation. Defers to the existing `pcall_k`
619    /// free function, which routes through `protected_call_raw` and
620    /// surfaces any runtime / syntax error as `Err(LuaError::Runtime|Syntax)`.
621    ///
622    /// Inherent method on LuaState shadows the `LuaStateStubExt::protected_call`
623    /// trait default so the `todo!()` shim never fires.
624    pub fn protected_call(&mut self, nargs: i32, nresults: i32, msgh: i32) -> Result<(), LuaError> {
625        pcall_k(self, nargs, nresults, msgh, 0, None).map(|_| ())
626    }
627
628    /// protected call. When `k` is set and the thread is yieldable, an
629    /// inner yield propagates as `LuaError::Yield` and the continuation
630    /// fires on resume via `finishCcall` → `finishpcallk`.
631    pub fn protected_call_k(
632        &mut self,
633        nargs: i32,
634        nresults: i32,
635        msgh: i32,
636        ctx: isize,
637        k: Option<crate::state::LuaKFunction>,
638    ) -> Result<(), LuaError> {
639        pcall_k(self, nargs, nresults, msgh, ctx, k).map(|_| ())
640    }
641
642    pub fn push_string(&mut self, s: &[u8]) -> Result<(), LuaError> {
643        push_lstring(self, s)?;
644        Ok(())
645    }
646
647    pub fn push_c_closure(
648        &mut self,
649        f: fn(&mut LuaState) -> Result<usize, LuaError>,
650        n: i32,
651    ) -> Result<(), LuaError> {
652        push_cclosure(self, f, n)
653    }
654
655    pub fn raw_seti(&mut self, idx: i32, n: i64) -> Result<(), LuaError> {
656        raw_set_i(self, idx, n)
657    }
658
659    pub fn table_set_i(&mut self, idx: i32, n: i64) -> Result<(), LuaError> {
660        set_i(self, idx, n)
661    }
662
663    /// Get `t[n]` where `t` is a pre-resolved `LuaValue`, bypassing stack-index
664    /// resolution. Use this in tight loops that operate on the same table
665    /// repeatedly to avoid the `index_to_value` call per iteration.
666    pub fn table_get_i_value(&mut self, t: &LuaValue, n: i64) -> Result<LuaType, LuaError> {
667        get_i_value(self, t, n)
668    }
669
670    /// Set `t[n] = stack_top` (then pop) where `t` is a pre-resolved `LuaValue`,
671    /// bypassing stack-index resolution. Use this in tight loops that operate on
672    /// the same table repeatedly to avoid the `index_to_value` call per iteration.
673    pub fn table_set_i_value(&mut self, t: &LuaValue, n: i64) -> Result<(), LuaError> {
674        set_i_value(self, t, n)
675    }
676
677    pub fn create_table(&mut self, narr: i32, nrec: i32) -> Result<(), LuaError> {
678        create_table(self, narr, nrec)
679    }
680
681    /// Pop the value on top of the stack and store it in the registry under
682    /// the string `key`.
683    ///
684    pub fn registry_set(&mut self, key: &[u8]) -> Result<(), LuaError> {
685        set_field(self, LUA_REGISTRYINDEX, key)
686    }
687
688    /// Create a new metatable in the registry under key `tname`. Leaves the
689    /// new metatable on top of the stack and returns `true` when newly
690    /// created. If `registry[tname]` already exists, leaves it on top of the
691    /// stack and returns `false`.
692    ///
693    pub fn new_metatable(&mut self, tname: &[u8]) -> Result<bool, LuaError> {
694        if get_field(self, LUA_REGISTRYINDEX, tname)? != LuaType::Nil {
695            return Ok(false);
696        }
697        self.pop_n(1);
698        create_table(self, 0, 2)?;
699        push_lstring(self, tname)?;
700        set_field(self, -2, b"__name")?;
701        push_value(self, -1);
702        set_field(self, LUA_REGISTRYINDEX, tname)?;
703        Ok(true)
704    }
705
706    /// Create a new library table sized for `funcs` and register each entry as
707    /// a closure field on it. Leaves the table on the top of the stack.
708    ///
709    ///   `luaL_checkversion(L), luaL_newlibtable(L,l), luaL_setfuncs(L,l,0)`.
710    /// `luaL_checkversion` is a no-op here (no ABI-version mismatch is
711    /// possible inside the Rust port).
712    pub fn new_lib(
713        &mut self,
714        funcs: &[(&[u8], LuaCFunction)],
715    ) -> Result<(), LuaError> {
716        create_table(self, 0, funcs.len() as i32)?;
717        for (name, f) in funcs {
718            push_cclosure(self, *f, 0)?;
719            set_field(self, -2, name)?;
720        }
721        Ok(())
722    }
723
724    /// Create and populate a library table for `funcs`, leaving it on top of
725    /// the stack. The `_name` argument is informational and matches the
726    /// `luaL_register`-style call sites in the Phase-A stdlib; the actual
727    /// global binding for the library happens later via `luaL_requiref`.
728    ///
729    pub fn register_lib(
730        &mut self,
731        _name: &[u8],
732        funcs: &[(&[u8], LuaCFunction)],
733    ) -> Result<(), LuaError> {
734        self.new_lib(funcs)
735    }
736
737    /// Create a new empty table presized to hold every entry in `funcs`, and
738    /// leave it on top of the stack. No registration is performed — callers
739    /// typically follow up with `set_funcs` / `set_funcs_with_upvalues` to
740    /// populate the table.
741    ///
742    ///   `lua_createtable(L, 0, sizeof(l)/sizeof((l)[0]) - 1)`. The C macro's
743    /// `- 1` discounts the sentinel `{NULL, NULL}` entry; the Rust slice has
744    /// no sentinel, so we use `funcs.len()` directly.
745    pub fn new_lib_table(
746        &mut self,
747        funcs: &[(&[u8], LuaCFunction)],
748    ) -> Result<(), LuaError> {
749        create_table(self, 0, funcs.len() as i32)
750    }
751
752    /// Register each entry in `funcs` as a C closure on the table at index
753    /// `-(nup + 2)`, sharing the `nup` values currently on top of the stack
754    /// as upvalues. The upvalues are popped at the end.
755    ///
756    pub fn set_funcs_with_upvalues(
757        &mut self,
758        funcs: &[(&[u8], LuaCFunction)],
759        nup: i32,
760    ) -> Result<(), LuaError> {
761        check_stack(self, nup);
762        for (name, f) in funcs {
763            for _ in 0..nup {
764                push_value(self, -nup);
765            }
766            push_cclosure(self, *f, nup)?;
767            set_field(self, -(nup + 2), name)?;
768        }
769        self.pop_n(nup as usize);
770        Ok(())
771    }
772
773    pub fn set_metatable(&mut self, objindex: i32) -> Result<(), LuaError> {
774        set_metatable(self, objindex)?;
775        Ok(())
776    }
777
778    /// Fetch the metatable registered under `name` in the registry and assign
779    /// it as the metatable of the value currently on top of the stack. The
780    /// fetched metatable is popped after assignment, leaving the original top
781    /// value in place.
782    ///
783    pub fn set_metatable_by_name(&mut self, name: &[u8]) -> Result<(), LuaError> {
784        get_field(self, LUA_REGISTRYINDEX, name)?;
785        set_metatable(self, -2)?;
786        Ok(())
787    }
788
789    /// Ensure `registry[name]` is a table; push it onto the stack.
790    /// Returns `true` if the table already existed, `false` if newly created.
791    ///
792    pub fn get_subtable_registry(&mut self, name: &[u8]) -> Result<bool, LuaError> {
793        if get_field(self, LUA_REGISTRYINDEX, name)? == LuaType::Table {
794            return Ok(true);
795        }
796        self.pop_n(1);
797        let idx = abs_index(self, LUA_REGISTRYINDEX);
798        let new_tbl = self.new_table();
799        self.push(LuaValue::Table(new_tbl));
800        push_value(self, -1);
801        set_field(self, idx, name)?;
802        Ok(false)
803    }
804
805    /// Allocate a fresh full-userdata block of `size` bytes with `nuvalue`
806    /// nil-initialised user-value slots, push it on the stack, and return a
807    /// `GcRef` to it. The `_name` parameter is advisory — callers typically
808    /// follow up with `set_metatable_by_name(name)` to attach the registered
809    /// metatable.
810    ///
811    /// C-correspondent: `lua_newuserdatauv(L, size, nuvalue)` (no name
812    /// parameter on the C side; the Rust signature carries it for callers'
813    /// convenience).
814    pub fn new_userdata_typed(
815        &mut self,
816        _name: &[u8],
817        size: usize,
818        nuvalue: i32,
819    ) -> Result<GcRef<LuaUserData>, LuaError> {
820        debug_assert!(nuvalue >= 0 && nuvalue < u16::MAX as i32, "invalid value");
821        // TODO(D-1c-bridge): state.new_userdata is still todo!(); keep direct alloc
822        let u = GcRef::new(LuaUserData {
823            data: vec![0u8; size].into_boxed_slice(),
824            uv: vec![LuaValue::Nil; nuvalue as usize],
825            metatable: std::cell::RefCell::new(None),
826            host_value: std::cell::RefCell::new(None),
827        });
828        self.push(LuaValue::UserData(u.clone()));
829        self.gc().check_step();
830        Ok(u)
831    }
832}
833
834// ── access functions (stack → Rust) ──────────────────────────────────────────
835
836pub fn lua_type_at(state: &LuaState, idx: i32) -> LuaType {
837    if !is_valid_index(state, idx) {
838        return LuaType::None;
839    }
840    index_to_value(state, idx).base_type()
841}
842
843pub fn type_name(_state: &LuaState, t: LuaType) -> &'static [u8] {
844    t.type_name()
845}
846
847pub fn is_cfunction(state: &LuaState, idx: i32) -> bool {
848    let o = index_to_value(state, idx);
849    matches!(o, LuaValue::Function(LuaClosure::LightC(_)) | LuaValue::Function(LuaClosure::C(_)))
850}
851
852pub fn is_integer(state: &LuaState, idx: i32) -> bool {
853    let o = index_to_value(state, idx);
854    matches!(o, LuaValue::Int(_))
855}
856
857pub fn is_number(state: &LuaState, idx: i32) -> bool {
858    let o = index_to_value(state, idx);
859    o.to_number_with_strconv().is_some()
860}
861
862pub fn is_string(state: &LuaState, idx: i32) -> bool {
863    let o = index_to_value(state, idx);
864    matches!(o, LuaValue::Str(_) | LuaValue::Int(_) | LuaValue::Float(_))
865}
866
867pub fn is_userdata(state: &LuaState, idx: i32) -> bool {
868    let o = index_to_value(state, idx);
869    matches!(o, LuaValue::UserData(_) | LuaValue::LightUserData(_))
870}
871
872pub fn raw_equal(state: &LuaState, index1: i32, index2: i32) -> bool {
873    if !is_valid_index(state, index1) || !is_valid_index(state, index2) {
874        return false;
875    }
876    let o1 = index_to_value(state, index1);
877    let o2 = index_to_value(state, index2);
878    state.equal_obj(None, &o1, &o2)
879}
880
881// PORT NOTE: LUA_OPUNM / LUA_OPBNOT are unary; all others are binary.
882pub fn arith(state: &mut LuaState, op: i32) -> Result<(), LuaError> {
883    // TODO(port): LUA_OPUNM and LUA_OPBNOT constant values not yet defined in
884    // Rust; using raw i32 comparison for now.
885    const LUA_OPUNM: i32 = 12;
886    const LUA_OPBNOT: i32 = 14;
887    if op == LUA_OPUNM || op == LUA_OPBNOT {
888        // unary — duplicate top as fake second operand
889        let top_val = state.get_at(state.top_idx() - 1);
890        state.push(top_val);
891    }
892    let top = state.top_idx();
893    let a = state.get_at(top - 2);
894    let b = state.get_at(top - 1);
895    let result = state.arith_op(op, &a, &b)?;
896    state.set_at(top - 2, result);
897    state.pop();
898    Ok(())
899}
900
901pub fn compare(state: &mut LuaState, index1: i32, index2: i32, op: i32) -> Result<bool, LuaError> {
902    let valid = is_valid_index(state, index1) && is_valid_index(state, index2);
903    let o1 = index_to_value(state, index1);
904    let o2 = index_to_value(state, index2);
905    if valid {
906        match op {
907            0 => Ok(state.equal_obj_with_tm(&o1, &o2)?),
908            1 => state.less_than(&o1, &o2),
909            2 => state.less_equal(&o1, &o2),
910            _ => {
911                debug_assert!(false, "invalid option");
912                Ok(false)
913            }
914        }
915    } else {
916        Ok(false)
917    }
918}
919
920pub fn string_to_number(state: &mut LuaState, s: &[u8]) -> usize {
921    // TODO(port): luaO_str2num not yet translated; push result if successful.
922    match state.str_to_num(s) {
923        Some((val, consumed)) => {
924            state.push(val);
925            consumed
926        }
927        None => 0,
928    }
929}
930
931pub fn to_number_x(state: &LuaState, idx: i32) -> Option<f64> {
932    let o = index_to_value(state, idx);
933    o.to_number_with_strconv()
934}
935
936pub fn to_integer_x(state: &LuaState, idx: i32) -> Option<i64> {
937    let o = index_to_value(state, idx);
938    o.to_integer_with_strconv()
939}
940
941pub fn to_boolean(state: &LuaState, idx: i32) -> bool {
942    let o = index_to_value(state, idx);
943    !matches!(o, LuaValue::Nil | LuaValue::Bool(false))
944}
945
946// PORT NOTE: returns Option<GcRef<LuaString>> instead of raw C pointer+len.
947pub fn to_lua_string(
948    state: &mut LuaState,
949    idx: i32,
950) -> Result<Option<GcRef<LuaString>>, LuaError> {
951    let o = index_to_value(state, idx);
952    if let LuaValue::Str(s) = &o {
953        return Ok(Some(s.clone()));
954    }
955    if !matches!(o, LuaValue::Int(_) | LuaValue::Float(_)) {
956        return Ok(None);
957    }
958    state.obj_to_string(idx)?;
959    state.gc().check_step();
960    let updated = index_to_value(state, idx);
961    if let LuaValue::Str(s) = updated {
962        Ok(Some(s))
963    } else {
964        Ok(None)
965    }
966}
967
968pub fn raw_len(state: &LuaState, idx: i32) -> u64 {
969    let o = index_to_value(state, idx);
970    match &o {
971        LuaValue::Str(s) => s.len() as u64,
972        LuaValue::UserData(u) => u.len() as u64,
973        LuaValue::Table(t) => state.table_getn(t) as u64,
974        _ => 0,
975    }
976}
977
978pub fn to_cfunction(
979    state: &LuaState,
980    idx: i32,
981) -> Option<fn(&mut LuaState) -> Result<usize, LuaError>> {
982    let o = index_to_value(state, idx);
983    match o {
984        // TODO(phase-b): lua-types `LuaClosure::LightC` carries a placeholder
985        // `fn() -> i32` until it can reference `LuaState`. The real cast
986        // happens once lua-types absorbs the LuaState-aware signature.
987        LuaValue::Function(LuaClosure::LightC(_f)) => None,
988        LuaValue::Function(LuaClosure::C(_ccl)) => None,
989        _ => None,
990    }
991}
992
993#[inline]
994fn to_userdata_ptr(o: &LuaValue) -> Option<*mut core::ffi::c_void> {
995    match o {
996        LuaValue::UserData(u) => {
997            // TODO(port): getudatamem returns a pointer to the raw byte payload of Udata.
998            // In Rust, LuaUserData carries a Box<[u8]>; we'd need to return a raw ptr.
999            // This is only safe inside lua-gc; stubbing with None for Phase A.
1000            let _ = u;
1001            None
1002        }
1003        LuaValue::LightUserData(p) => Some(*p),
1004        _ => None,
1005    }
1006}
1007
1008pub fn to_userdata(state: &LuaState, idx: i32) -> Option<*mut core::ffi::c_void> {
1009    let o = index_to_value(state, idx);
1010    to_userdata_ptr(&o)
1011}
1012
1013pub fn to_thread(state: &LuaState, idx: i32) -> Option<GcRef<lua_types::value::LuaThread>> {
1014    // TODO(phase-b): lua-vm's rich LuaState is not the same type as
1015    // lua_types::value::LuaThread; the latter is a placeholder. Resolve in
1016    // Phase B by unifying thread types.
1017    let o = index_to_value(state, idx);
1018    if let LuaValue::Thread(t) = o {
1019        Some(t)
1020    } else {
1021        None
1022    }
1023}
1024
1025// PORT NOTE: returns a usize (opaque identity) rather than a raw void*.
1026// Raw pointers are only allowed in lua-gc / lua-coro.
1027pub fn to_pointer(state: &LuaState, idx: i32) -> Option<usize> {
1028    let o = index_to_value(state, idx);
1029    // TODO(port): returning a raw pointer here is not safe outside lua-gc.
1030    // Returning the GC identity as a usize for opaque pointer identity purposes.
1031    match &o {
1032        LuaValue::Function(LuaClosure::LightC(f)) => Some(*f as usize),
1033        LuaValue::LightUserData(p) => Some(*p as usize),
1034        LuaValue::Str(s) => Some(GcRef::identity(s)),
1035        LuaValue::Table(t) => Some(GcRef::identity(t)),
1036        LuaValue::Function(LuaClosure::Lua(f)) => Some(GcRef::identity(f)),
1037        LuaValue::Function(LuaClosure::C(f)) => Some(GcRef::identity(f)),
1038        LuaValue::UserData(u) => Some(GcRef::identity(u)),
1039        LuaValue::Thread(t) => Some(GcRef::identity(t)),
1040        _ => None,
1041    }
1042}
1043
1044// ── push functions (Rust → stack) ────────────────────────────────────────────
1045
1046pub fn push_nil(state: &mut LuaState) {
1047    state.push(LuaValue::Nil);
1048}
1049
1050pub fn push_number(state: &mut LuaState, n: f64) {
1051    state.push(LuaValue::Float(n));
1052}
1053
1054pub fn push_integer(state: &mut LuaState, n: i64) {
1055    state.push(LuaValue::Int(n));
1056}
1057
1058// PORT NOTE: returns the interned LuaString instead of a raw C pointer.
1059pub fn push_lstring(state: &mut LuaState, s: &[u8]) -> Result<GcRef<LuaString>, LuaError> {
1060    let ts = state.intern_str(s)?;
1061    state.push(LuaValue::Str(ts.clone()));
1062    state.gc().check_step();
1063    Ok(ts)
1064}
1065
1066pub fn push_string(state: &mut LuaState, s: Option<&[u8]>) -> Result<Option<GcRef<LuaString>>, LuaError> {
1067    match s {
1068        None => {
1069            state.push(LuaValue::Nil);
1070            state.gc().check_step();
1071            Ok(None)
1072        }
1073        Some(bytes) => {
1074            let ts = state.intern_str(bytes)?;
1075            state.push(LuaValue::Str(ts.clone()));
1076            state.gc().check_step();
1077            Ok(Some(ts))
1078        }
1079    }
1080}
1081
1082// PORT NOTE: va_list is not representable in safe Rust; callers pass a pre-formatted &[u8].
1083// TODO(port): lua_pushvfstring uses C varargs (va_list); no direct Rust equivalent.
1084// The Rust API uses state.push_fstring(format_args!(...)) instead.
1085pub fn push_vfstring(state: &mut LuaState, formatted: &[u8]) -> Result<GcRef<LuaString>, LuaError> {
1086    let ts = state.intern_str(formatted)?;
1087    state.push(LuaValue::Str(ts.clone()));
1088    state.gc().check_step();
1089    Ok(ts)
1090}
1091
1092// PORT NOTE: C varargs not used; callers use format_args! and push_fstring.
1093pub fn push_fstring(state: &mut LuaState, formatted: &[u8]) -> Result<GcRef<LuaString>, LuaError> {
1094    push_vfstring(state, formatted)
1095}
1096
1097pub fn push_cclosure(
1098    state: &mut LuaState,
1099    f: fn(&mut LuaState) -> Result<usize, LuaError>,
1100    n: i32,
1101) -> Result<(), LuaError> {
1102    //    if (n == 0) { setfvalue(s2v(L->top.p), fn); api_incr_top(L); }
1103    //    else { api_checknelems(L, n); api_check(L, n <= MAXUPVAL, ...);
1104    //           cl = luaF_newCclosure(L, n); cl->f = fn;
1105    //           L->top.p -= n;
1106    //           while (n--) setobj2n(L, &cl->upvalue[n], s2v(L->top.p + n));
1107    //           setclCvalue(L, s2v(L->top.p), cl); api_incr_top(L);
1108    //           luaC_checkGC(L); }
1109    //    lua_unlock(L);
1110    //
1111    // PORT NOTE: `LuaClosure::LightC` and `LuaCClosure` carry a `LuaCFnPtr`
1112    // (a `usize` index into `GlobalState.c_functions`) rather than the raw
1113    // function pointer, because lua-types cannot reference `LuaState`. We
1114    // register `f` in the per-state registry and store the resulting index.
1115    let idx: lua_types::closure::LuaCFnPtr = {
1116        let mut g = state.global_mut();
1117        if n == 0 {
1118            match g.c_functions.iter().position(|existing| {
1119                existing
1120                    .as_bare()
1121                    .is_some_and(|existing| std::ptr::fn_addr_eq(existing, f))
1122            }) {
1123                Some(i) => i,
1124                None => {
1125                    let i = g.c_functions.len();
1126                    g.c_functions.push(LuaCallable::bare(f));
1127                    i
1128                }
1129            }
1130        } else {
1131            let i = g.c_functions.len();
1132            g.c_functions.push(LuaCallable::bare(f));
1133            i
1134        }
1135    };
1136    if n == 0 {
1137        state.push(LuaValue::Function(LuaClosure::LightC(idx)));
1138    } else {
1139        debug_assert!(n > 0 && (n as u32) <= MAX_UPVAL as u32, "upvalue index too large");
1140        let n_usize = n as usize;
1141        let top = state.top_idx();
1142        debug_assert!((top.0 as usize) >= n_usize, "not enough elements on stack");
1143        let base = top.0 as usize - n_usize;
1144        let mut upvalues: Vec<LuaValue> = Vec::with_capacity(n_usize);
1145        for i in 0..n_usize {
1146            upvalues.push(state.get_at(crate::state::StackIdx((base + i) as u32)));
1147        }
1148        state.pop_n(n_usize);
1149        // TODO(D-1c-bridge): state.new_c_closure is still todo!(); keep direct alloc
1150        let cl = LuaClosure::C(GcRef::new(lua_types::closure::LuaCClosure {
1151            func: idx,
1152            upvalues,
1153        }));
1154        state.push(LuaValue::Function(cl));
1155        state.gc().check_step();
1156    }
1157    Ok(())
1158}
1159
1160pub fn push_boolean(state: &mut LuaState, b: bool) {
1161    state.push(LuaValue::Bool(b));
1162}
1163
1164pub fn push_light_userdata(state: &mut LuaState, p: *mut core::ffi::c_void) {
1165    state.push(LuaValue::LightUserData(p));
1166}
1167
1168// Returns true if pushed thread is the main thread.
1169pub fn push_thread(state: &mut LuaState) -> bool {
1170    let (value, is_main) = {
1171        let g = state.global();
1172        let id = g.current_thread_id;
1173        let v = g
1174            .thread_value_for(id)
1175            .expect("current_thread_id must always resolve to a registered thread");
1176        (v, id == g.main_thread_id)
1177    };
1178    state.push(LuaValue::Thread(value));
1179    is_main
1180}
1181
1182// ── get functions (Lua → stack) ───────────────────────────────────────────────
1183
1184fn aux_get_str(state: &mut LuaState, t: LuaValue, k: &[u8]) -> Result<LuaType, LuaError> {
1185    let str_val = {
1186        let ts = state.intern_str(k)?;
1187        LuaValue::Str(ts)
1188    };
1189    // TODO(port): luaV_fastget / luaV_finishget not yet translated; using
1190    // a simplified table_get that may miss metamethod chains.
1191    let result = state.table_get_with_tm(&t, &str_val)?;
1192    state.push(result);
1193    let top = state.top_idx();
1194    Ok(state.get_at(top - 1).base_type())
1195}
1196
1197fn get_global_table(state: &LuaState) -> LuaValue {
1198    // PORT NOTE (phase-b-reconcile): The lua-types LuaTable placeholder has
1199    // no storage, so we cannot fetch the globals table from the registry's
1200    // array slot. init_registry now stashes globals in a direct
1201    // GlobalState field; read it from there until the LuaTable placeholder
1202    // reconciles with lua-vm::table::LuaTable.
1203    state.global().globals.clone()
1204}
1205
1206pub fn get_global(state: &mut LuaState, name: &[u8]) -> Result<LuaType, LuaError> {
1207    let g = get_global_table(state);
1208    aux_get_str(state, g, name)
1209}
1210
1211pub fn get_table(state: &mut LuaState, idx: i32) -> Result<LuaType, LuaError> {
1212    let t = index_to_value(state, idx);
1213    let top = state.top_idx();
1214    let key = state.get_at(top - 1);
1215    let result = state.table_get_with_tm(&t, &key)?;
1216    state.set_at(top - 1, result);
1217    let val = state.get_at(top - 1);
1218    Ok(val.base_type())
1219}
1220
1221pub fn get_field(state: &mut LuaState, idx: i32, k: &[u8]) -> Result<LuaType, LuaError> {
1222    let t = index_to_value(state, idx);
1223    aux_get_str(state, t, k)
1224}
1225
1226pub fn get_i(state: &mut LuaState, idx: i32, n: i64) -> Result<LuaType, LuaError> {
1227    let t = index_to_value(state, idx);
1228    let key = LuaValue::Int(n);
1229    let result = state.table_get_with_tm(&t, &key)?;
1230    state.push(result);
1231    let top = state.top_idx();
1232    Ok(state.get_at(top - 1).base_type())
1233}
1234
1235/// Variant of `get_i` that accepts a pre-resolved table value instead of a
1236/// stack index. Callers that invoke `get_i` repeatedly on the same table
1237/// (e.g. the shift loops in `table.remove` / `table.insert`) should resolve
1238/// the table once and use this function to avoid calling `index_to_value`
1239/// on every iteration.
1240pub fn get_i_value(state: &mut LuaState, t: &LuaValue, n: i64) -> Result<LuaType, LuaError> {
1241    let key = LuaValue::Int(n);
1242    let result = state.table_get_with_tm(t, &key)?;
1243    state.push(result);
1244    let top = state.top_idx();
1245    Ok(state.get_at(top - 1).base_type())
1246}
1247
1248fn finish_raw_get(state: &mut LuaState, val: Option<LuaValue>) -> LuaType {
1249    let v = val.unwrap_or(LuaValue::Nil);
1250    state.push(v);
1251    let top = state.top_idx();
1252    state.get_at(top - 1).base_type()
1253}
1254
1255fn get_table_value(state: &LuaState, idx: i32) -> Option<GcRef<LuaTable>> {
1256    let t = index_to_value(state, idx);
1257    debug_assert!(matches!(t, LuaValue::Table(_)), "table expected");
1258    if let LuaValue::Table(tbl) = t {
1259        Some(tbl)
1260    } else {
1261        None
1262    }
1263}
1264
1265pub fn raw_get(state: &mut LuaState, idx: i32) -> LuaType {
1266    let t = get_table_value(state, idx);
1267    let top = state.top_idx();
1268    let key = state.get_at(top - 1);
1269    let val = t.as_ref().map(|tbl| tbl.get(&key));
1270    state.set_top_idx(top - 1);
1271    finish_raw_get(state, val)
1272}
1273
1274pub fn raw_get_i(state: &mut LuaState, idx: i32, n: i64) -> LuaType {
1275    let t = get_table_value(state, idx);
1276    let val = t.as_ref().map(|tbl| tbl.get_int(n));
1277    finish_raw_get(state, val)
1278}
1279
1280pub fn raw_get_p(state: &mut LuaState, idx: i32, p: *const core::ffi::c_void) -> LuaType {
1281    let t = get_table_value(state, idx);
1282    let key = LuaValue::LightUserData(p as *mut core::ffi::c_void);
1283    let val = t.as_ref().map(|tbl| tbl.get(&key));
1284    finish_raw_get(state, val)
1285}
1286
1287pub fn create_table(state: &mut LuaState, narray: i32, nrec: i32) -> Result<(), LuaError> {
1288    let t = state.new_table();
1289    if narray > 0 || nrec > 0 {
1290        t.resize(state, narray as usize, nrec as usize)?;
1291    }
1292    state.push(LuaValue::Table(t));
1293    state.gc().check_step();
1294    Ok(())
1295}
1296
1297pub fn get_metatable(state: &mut LuaState, objindex: i32) -> bool {
1298    let obj = index_to_value(state, objindex);
1299    let mt: Option<GcRef<LuaTable>> = match &obj {
1300        LuaValue::Table(t) => t.metatable(),
1301        LuaValue::UserData(u) => u.metatable(),
1302        other => {
1303            let idx = other.base_type() as usize;
1304            state.global().mt[idx].clone()
1305        }
1306    };
1307    if let Some(mt_table) = mt {
1308        state.push(LuaValue::Table(mt_table));
1309        true
1310    } else {
1311        false
1312    }
1313}
1314
1315pub fn get_i_uservalue(state: &mut LuaState, idx: i32, n: i32) -> LuaType {
1316    let o = index_to_value(state, idx);
1317    debug_assert!(matches!(o, LuaValue::UserData(_)), "full userdata expected");
1318    if let LuaValue::UserData(ref u) = o {
1319        let uv_count = u.uv.len() as i32;
1320        if n <= 0 || n > uv_count {
1321            state.push(LuaValue::Nil);
1322            LuaType::None
1323        } else {
1324            let val = u.uv[(n - 1) as usize].clone();
1325            let t = val.base_type();
1326            state.push(val);
1327            t
1328        }
1329    } else {
1330        state.push(LuaValue::Nil);
1331        LuaType::None
1332    }
1333}
1334
1335// ── set functions (stack → Lua) ───────────────────────────────────────────────
1336
1337fn aux_set_str(state: &mut LuaState, t: LuaValue, k: &[u8]) -> Result<(), LuaError> {
1338    let str_val = {
1339        let ts = state.intern_str(k)?;
1340        LuaValue::Str(ts)
1341    };
1342    //       luaV_finishfastset(L, t, slot, s2v(L->top.p - 1)); L->top.p--;
1343    //    else { setsvalue2s L->top.p str; api_incr_top;
1344    //           luaV_finishset(L, t, s2v(L->top.p-1), s2v(L->top.p-2), slot);
1345    //           L->top.p -= 2; }
1346    let top = state.top_idx();
1347    let val = state.get_at(top - 1);
1348    state.table_set_with_tm(&t, str_val, val)?;
1349    state.pop();
1350    Ok(())
1351}
1352
1353pub fn set_global(state: &mut LuaState, name: &[u8]) -> Result<(), LuaError> {
1354    let g = get_global_table(state);
1355    aux_set_str(state, g, name)
1356}
1357
1358pub fn set_table(state: &mut LuaState, idx: i32) -> Result<(), LuaError> {
1359    let t = index_to_value(state, idx);
1360    let top = state.top_idx();
1361    let key = state.get_at(top - 2);
1362    let val = state.get_at(top - 1);
1363    state.table_set_with_tm(&t, key, val)?;
1364    state.set_top_idx(top - 2);
1365    Ok(())
1366}
1367
1368pub fn set_field(state: &mut LuaState, idx: i32, k: &[u8]) -> Result<(), LuaError> {
1369    let t = index_to_value(state, idx);
1370    aux_set_str(state, t, k)
1371}
1372
1373pub fn set_i(state: &mut LuaState, idx: i32, n: i64) -> Result<(), LuaError> {
1374    let t = index_to_value(state, idx);
1375    let top = state.top_idx();
1376    let val = state.get_at(top - 1);
1377    let key = LuaValue::Int(n);
1378    state.table_set_with_tm(&t, key, val)?;
1379    state.pop();
1380    Ok(())
1381}
1382
1383/// Variant of `set_i` that accepts a pre-resolved table value instead of a
1384/// stack index. Callers that invoke `set_i` repeatedly on the same table
1385/// (e.g. the shift loops in `table.remove` / `table.insert`) should resolve
1386/// the table once and use this function to avoid calling `index_to_value`
1387/// on every iteration.
1388pub fn set_i_value(state: &mut LuaState, t: &LuaValue, n: i64) -> Result<(), LuaError> {
1389    let top = state.top_idx();
1390    let val = state.get_at(top - 1);
1391    let key = LuaValue::Int(n);
1392    state.table_set_with_tm(t, key, val)?;
1393    state.pop();
1394    Ok(())
1395}
1396
1397fn aux_raw_set(state: &mut LuaState, idx: i32, key: LuaValue, n: u32) -> Result<(), LuaError> {
1398    let t = get_table_value(state, idx)
1399        .ok_or_else(|| LuaError::runtime(format_args!("table expected")))?;
1400    let top = state.top_idx();
1401    let val = state.get_at(top - 1);
1402    t.raw_set(state, key, val)?;
1403    t.invalidate_tm_cache();
1404    let top_val = state.get_at(top - 1);
1405    state.gc().barrier_back(&t, &top_val);
1406    state.set_top_idx(top - n as i32);
1407    Ok(())
1408}
1409
1410pub fn raw_set(state: &mut LuaState, idx: i32) -> Result<(), LuaError> {
1411    let top = state.top_idx();
1412    let key = state.get_at(top - 2);
1413    aux_raw_set(state, idx, key, 2)
1414}
1415
1416pub fn raw_set_p(state: &mut LuaState, idx: i32, p: *const core::ffi::c_void) -> Result<(), LuaError> {
1417    let key = LuaValue::LightUserData(p as *mut core::ffi::c_void);
1418    aux_raw_set(state, idx, key, 1)
1419}
1420
1421pub fn raw_set_i(state: &mut LuaState, idx: i32, n: i64) -> Result<(), LuaError> {
1422    let t = get_table_value(state, idx)
1423        .ok_or_else(|| LuaError::runtime(format_args!("table expected")))?;
1424    let top = state.top_idx();
1425    let val = state.get_at(top - 1);
1426    t.raw_set_int(state, n, val)?;
1427    let top_val = state.get_at(top - 1);
1428    state.gc().barrier_back(&t, &top_val);
1429    state.pop();
1430    Ok(())
1431}
1432
1433/// Returns true if `mt` (a metatable) holds a non-nil `__gc` entry.
1434///
1435/// PORT NOTE: Mirrors the body of C's `tofinalize` in `lgc.c` minus the bits
1436/// that consult per-object GC bits (irrelevant in Phase B's Rc world).
1437fn metatable_has_gc(state: &LuaState, mt: &GcRef<LuaTable>) -> bool {
1438    let name = state.global().tmname[crate::tagmethods::TagMethod::Gc as usize].clone();
1439    !matches!(mt.get_short_str(&name), LuaValue::Nil)
1440}
1441
1442/// Pin `tbl` in `pending_finalizers` if not already present.
1443fn register_finalizable_table(state: &mut LuaState, tbl: &GcRef<LuaTable>) {
1444    let already = state
1445        .global()
1446        .pending_finalizers
1447        .iter()
1448        .any(|t| GcRef::ptr_eq(t, tbl));
1449    if !already {
1450        state.global_mut().pending_finalizers.push(tbl.clone());
1451    }
1452}
1453
1454/// Phase-B `__gc` driver.
1455///
1456/// Scans `pending_finalizers` for tables whose only strong ref is the list
1457/// itself (`Rc::strong_count == 1`), runs their `__gc` metamethod in a
1458/// protected call, then drops the list's pin so the table can be freed.
1459/// Iterates in reverse so the most-recently registered finalizers run first,
1460/// matching C-Lua's order (`finobj` is a LIFO stack).
1461///
1462/// PORT NOTE: This stands in for C-Lua's `GCSatomic` finalizer-promotion step
1463/// plus `GCTM`. The real GC walks the heap to decide which `finobj` entries
1464/// are unreachable; in Phase B we use the `Rc` strong-count as the proxy.
1465/// Replaced by `lua_gc::run_pending_finalizers` when Phase D's incremental
1466/// GC lands.
1467pub fn run_pending_finalizers(state: &mut LuaState) {
1468    let mut did_run = false;
1469    loop {
1470        // `to_be_finalized` was populated by the most recent
1471        // `collect_via_heap` mark phase. Drain in LIFO order so the most
1472        // recently dead object runs its `__gc` first — matches C-Lua's
1473        // `finobj` stack ordering.
1474        let target_idx = {
1475            let to_fin = &state.global().to_be_finalized;
1476            if to_fin.is_empty() { None } else { Some(to_fin.len() - 1) }
1477        };
1478        let Some(i) = target_idx else { break; };
1479        // The Phase-A pre-finalizer weak-value sweep (mirroring C-Lua's
1480        // `clearbyvalues(g, g->weak, NULL)` from `atomic()`) is no longer
1481        // needed: under D-2, weak-table sweeping runs inside the post-mark
1482        // hook of `Heap::full_collect_with_post_mark`, which uses
1483        // reachability instead of strong_count and therefore clears such
1484        // entries BEFORE this finalizer pass runs. The full "bug-in-5.1"
1485        // ordering (finalizer-visible state) still requires reachability-
1486        // based detection of which finalizable tables are about to die — a
1487        // gap tracked under D-2 ephemeron/finalizer follow-up.
1488        let tbl = state.global_mut().to_be_finalized.swap_remove(i);
1489        let mt = tbl.metatable();
1490        let gc_fn = match mt {
1491            Some(ref m) => {
1492                let name = state.global().tmname[crate::tagmethods::TagMethod::Gc as usize].clone();
1493                m.get_short_str(&name)
1494            }
1495            None => LuaValue::Nil,
1496        };
1497        if !matches!(gc_fn, LuaValue::Function(_)) {
1498            continue;
1499        }
1500        did_run = true;
1501        let saved_top = state.top_idx();
1502        let ci_top = state.current_call_info().top;
1503        if saved_top.0 < ci_top.0 {
1504            state.clear_stack_range(saved_top, ci_top);
1505            state.set_top(ci_top);
1506        }
1507        state.push(gc_fn);
1508        state.push(LuaValue::Table(tbl));
1509        let func_idx = state.top_idx() - 2;
1510        let _heap_guard = {
1511            let g = state.global.borrow();
1512            lua_gc::HeapGuard::push(&g.heap)
1513        };
1514        let old_allowhook = state.allowhook;
1515        let old_gcstp = state.global_mut().stop_gc_internal();
1516        state.allowhook = false;
1517        let caller_ci = state.ci;
1518        let caller_status = state.get_ci(caller_ci).callstatus;
1519        state.get_ci_mut(caller_ci).callstatus = caller_status | crate::state::CIST_FIN;
1520        let _ = crate::do_::pcall(
1521            state,
1522            |s| s.call_no_yield(func_idx, 0),
1523            func_idx,
1524            0,
1525        );
1526        state.get_ci_mut(caller_ci).callstatus = caller_status;
1527        state.allowhook = old_allowhook;
1528        state.global_mut().set_gc_stop_flags(old_gcstp);
1529        state.set_top(saved_top);
1530    }
1531    // Post-finalizer weak sweep is also obsolete: any weak entries newly
1532    // exposed by the finalizer pass will be cleared on the NEXT
1533    // `Heap::full_collect_with_post_mark`. We accept the one-cycle lag.
1534    let _ = did_run;
1535}
1536
1537/// Run every still-pending `__gc` finalizer at state close.
1538///
1539/// Mirrors C-Lua's `luaC_freeallobjects` (`lgc.c`), which calls
1540/// `separatetobefnz(g, 1)` to move *all* remaining finalizable objects
1541/// (regardless of reachability) into the to-be-finalized list, then
1542/// `callallpendingfinalizers` to invoke each `__gc` before the objects are
1543/// freed. At `lua_close`, objects the program kept alive to program end —
1544/// e.g. a table held by a global — still have their finalizer run; that is
1545/// what emits messages like `>>> closing state <<<` from `gc.lua`.
1546///
1547/// Phase-B note: the live registry of finalizable objects is
1548/// `pending_finalizers`. A single snapshot of that list is promoted into
1549/// `to_be_finalized` and drained by [`run_pending_finalizers`]. We snapshot
1550/// once (matching C's single `separatetobefnz` call): a finalizer may
1551/// resurrect its object or register new finalizables via `setmetatable`, but
1552/// C does not re-finalize those at close (`gcstp = GCSTPCLS`), so neither do
1553/// we — the freshly-registered entries are left in `pending_finalizers` and
1554/// simply dropped with the state.
1555pub fn run_close_finalizers(state: &mut LuaState) {
1556    let pending: Vec<GcRef<lua_types::value::LuaTable>> =
1557        std::mem::take(&mut state.global_mut().pending_finalizers);
1558    if pending.is_empty() {
1559        return;
1560    }
1561    let mut seen = std::collections::HashSet::<usize>::new();
1562    {
1563        let mut g = state.global_mut();
1564        for tbl in pending {
1565            if seen.insert(tbl.identity()) {
1566                g.to_be_finalized.push(tbl);
1567            }
1568        }
1569    }
1570    run_pending_finalizers(state);
1571}
1572
1573/// Snapshot the currently-live weak tables from
1574/// `GlobalState.weak_tables_registry`, deduplicating by Rc pointer and
1575/// dropping any whose backing storage has been freed. Used by both the
1576/// pre-finalizer and post-finalizer sweeps in [`run_pending_finalizers`]
1577/// and by the explicit `collectgarbage("collect")` path.
1578fn collect_live_weak_tables(state: &mut LuaState) -> Vec<GcRef<lua_types::value::LuaTable>> {
1579    let mut g = state.global_mut();
1580    g.weak_tables_registry.retain(|w| w.strong_count() > 0);
1581    let mut seen = std::collections::HashSet::<usize>::new();
1582    g.weak_tables_registry
1583        .iter()
1584        .filter_map(|w| w.upgrade())
1585        .filter_map(|rc| {
1586            let id = rc.identity();
1587            if seen.insert(id) {
1588                Some(rc)
1589            } else {
1590                None
1591            }
1592        })
1593        .collect()
1594}
1595
1596pub fn set_metatable(state: &mut LuaState, objindex: i32) -> Result<bool, LuaError> {
1597    let top = state.top_idx();
1598    let mt_val = state.get_at(top - 1);
1599    let mt: Option<GcRef<LuaTable>> = if matches!(mt_val, LuaValue::Nil) {
1600        None
1601    } else {
1602        debug_assert!(matches!(mt_val, LuaValue::Table(_)), "table expected");
1603        if let LuaValue::Table(t) = mt_val {
1604            Some(t)
1605        } else {
1606            None
1607        }
1608    };
1609
1610    let obj = index_to_value(state, objindex);
1611    match obj {
1612        LuaValue::Table(ref tbl) => {
1613            if mt.is_some() {
1614                state.gc().obj_barrier(tbl, mt.as_ref().unwrap());
1615            }
1616            tbl.set_metatable(mt.clone());
1617            if tbl.weak_mode() != 0 {
1618                state
1619                    .global_mut()
1620                    .weak_tables_registry
1621                    .push(tbl.downgrade());
1622            }
1623            // Phase-B finalizer registration: if the new metatable carries
1624            // `__gc` and `obj` was not already registered, pin `obj` in the
1625            // pending-finalizers list so that `run_pending_finalizers` can
1626            // invoke the finalizer before the object is freed.
1627            if let Some(ref mt_table) = mt {
1628                if metatable_has_gc(state, mt_table) {
1629                    register_finalizable_table(state, tbl);
1630                }
1631            }
1632        }
1633        LuaValue::UserData(ref ud) => {
1634            if let Some(ref mt_table) = mt {
1635                state.gc().obj_barrier(ud, mt_table);
1636                // TODO(port): luaC_checkfinalizer
1637            }
1638            ud.set_metatable(mt);
1639        }
1640        ref other => {
1641            let idx = other.base_type() as usize;
1642            state.global_mut().mt[idx] = mt;
1643        }
1644    }
1645    state.pop();
1646    Ok(true)
1647}
1648
1649pub fn set_i_uservalue(state: &mut LuaState, idx: i32, n: i32) -> Result<bool, LuaError> {
1650    let o = index_to_value(state, idx);
1651    debug_assert!(matches!(o, LuaValue::UserData(_)), "full userdata expected");
1652    let top = state.top_idx();
1653    let val = state.get_at(top - 1);
1654    let res = if let LuaValue::UserData(ref ud) = o {
1655        let nuvalue = ud.uv.len() as i32;
1656        if n < 1 || n > nuvalue {
1657            false
1658        } else {
1659            // TODO(port): LuaUserData uv field needs interior mutability for write
1660            // ud.uv[(n - 1) as usize] = val.clone();
1661            state.gc().barrier_back(ud, &val);
1662            let _ = (n, ud);
1663            true
1664        }
1665    } else {
1666        false
1667    };
1668    state.pop();
1669    Ok(res)
1670}
1671
1672// ── load/call functions ───────────────────────────────────────────────────────
1673
1674//                            lua_KContext ctx, lua_KFunction k)
1675pub fn call_k(
1676    state: &mut LuaState,
1677    nargs: i32,
1678    nresults: i32,
1679    ctx: isize,
1680    k: Option<fn(&mut LuaState, i32, isize) -> Result<usize, LuaError>>,
1681) -> Result<(), LuaError> {
1682    let top = state.top_idx();
1683    let func_idx = top - (nargs + 1);
1684    //      L->ci->u.c.k = k; L->ci->u.c.ctx = ctx;
1685    //      luaD_call(L, func, nresults);
1686    //    } else {
1687    //      luaD_callnoyield(L, func, nresults);
1688    //    }
1689    if k.is_some() && state.is_yieldable() {
1690        let ci_idx = state.ci;
1691        {
1692            let ci = state.get_ci_mut(ci_idx);
1693            ci.set_u_c_k(k);
1694            ci.set_u_c_ctx(ctx);
1695        }
1696        state.call_at(func_idx, nresults)?;
1697    } else {
1698        state.call_no_yield(func_idx, nresults)?;
1699    }
1700    state.adjust_results(nresults);
1701    Ok(())
1702}
1703
1704//                            lua_KContext ctx, lua_KFunction k)
1705pub fn pcall_k(
1706    state: &mut LuaState,
1707    nargs: i32,
1708    nresults: i32,
1709    errfunc: i32,
1710    ctx: isize,
1711    k: Option<fn(&mut LuaState, i32, isize) -> Result<usize, LuaError>>,
1712) -> Result<LuaStatus, LuaError> {
1713    // Phase D-1c: activate the heap for the duration of this protected call.
1714    // GcRef::new (post D-1e) and any future allocator-aware code will route
1715    // through state.global.heap via with_current_heap(...). Stacked so nested
1716    // pcalls inside the same thread don't clobber each other.
1717    let _heap_guard = {
1718        let g = state.global.borrow();
1719        // The HeapGuard borrows &Heap; we let it live for the function scope.
1720        // The borrow of `g` is dropped immediately; the guard's NonNull
1721        // outlives it (the heap field is pinned inside GlobalState which
1722        // is Rc-managed and won't move).
1723        lua_gc::HeapGuard::push(&g.heap)
1724    };
1725    let err_handler_idx: isize = if errfunc == 0 {
1726        0
1727    } else {
1728        let o = index_to_stack_idx(state, errfunc);
1729        debug_assert!(
1730            matches!(state.get_at(o), LuaValue::Function(_)),
1731            "error handler must be a function"
1732        );
1733        o.0 as isize
1734    };
1735    let top = state.top_idx();
1736    let func_idx = top - (nargs + 1);
1737    if k.is_none() || !state.is_yieldable() {
1738        state.protected_call_raw(func_idx, nresults, StackIdx(err_handler_idx as u32))?;
1739        state.adjust_results(nresults);
1740        return Ok(LuaStatus::Ok);
1741    }
1742    // Yieldable continuation path: arrange for an interrupted call (yield or
1743    // recoverable error) to be resumable. The call is already protected by
1744    // `lua_resume`; real errors must propagate with CIST_YPCALL still set so
1745    // `precover` can run `finish_pcallk`.
1746    //
1747    let ci_idx = state.ci;
1748    let allow = state.allowhook;
1749    let saved_errfunc = state.errfunc;
1750    {
1751        let ci = state.get_ci_mut(ci_idx);
1752        ci.set_u_c_k(k);
1753        ci.set_u_c_ctx(ctx);
1754        ci.set_u2_funcidx(func_idx.0 as i32);
1755        ci.set_u_c_old_errfunc(saved_errfunc);
1756        ci.set_oah(allow);
1757        ci.callstatus |= crate::state::CIST_YPCALL;
1758    }
1759    state.errfunc = err_handler_idx;
1760    let call_result = crate::do_::call(state, func_idx, nresults);
1761    match call_result {
1762        Ok(()) => {
1763            //    L->errfunc = ci->u.c.old_errfunc;
1764            //    status = LUA_OK;
1765            state.get_ci_mut(ci_idx).callstatus &= !crate::state::CIST_YPCALL;
1766            state.errfunc = saved_errfunc;
1767            state.adjust_results(nresults);
1768            Ok(LuaStatus::Ok)
1769        }
1770        Err(crate::state::LuaError::Yield) => {
1771            // Yield must propagate up to lua_resume. The recovery prep stays
1772            // on `ci_idx` so that on resume, `finishCcall` will call
1773            // `finishpcallk` followed by the continuation `k`.
1774            Err(crate::state::LuaError::Yield)
1775        }
1776        Err(e) => {
1777            // Real errors take the same path as C longjmp: they unwind to
1778            // lua_resume's protected runner, which calls precover and then
1779            // finish_pcallk while this C frame still advertises CIST_YPCALL.
1780            Err(e)
1781        }
1782    }
1783}
1784
1785//                          const char *chunkname, const char *mode)
1786// PORT NOTE: lua_Reader (void* callback) is replaced by Box<dyn FnMut>; mode
1787// is &[u8].
1788pub fn load(
1789    state: &mut LuaState,
1790    reader: Box<dyn FnMut() -> Option<Vec<u8>>>,
1791    chunkname: Option<&[u8]>,
1792    mode: Option<&[u8]>,
1793) -> Result<LuaStatus, LuaError> {
1794    let name = chunkname.unwrap_or(b"?");
1795    let z = crate::zio::ZIO::new(reader);
1796    let status = state.protected_parser(z, name, mode);
1797    if status == LuaStatus::Ok {
1798        let top = state.top_idx();
1799        let func_val = state.get_at(top - 1);
1800        if let LuaValue::Function(LuaClosure::Lua(lcl)) = func_val {
1801            if !lcl.upvals.is_empty() {
1802                let gt = get_global_table(state);
1803                let uv = state.new_upval_closed(gt);
1804                lcl.set_upval(0, uv);
1805            }
1806        }
1807    }
1808    Ok(status)
1809}
1810
1811pub fn dump(
1812    state: &LuaState,
1813    writer: &mut dyn FnMut(&[u8]) -> Result<(), LuaError>,
1814    strip: bool,
1815) -> Result<bool, LuaError> {
1816    let top = state.top_idx();
1817    let o = state.get_at(top - 1);
1818    if let LuaValue::Function(LuaClosure::Lua(ref lcl)) = o {
1819        crate::dump::dump(state, &lcl.proto, writer, strip)?;
1820        Ok(true)
1821    } else {
1822        Ok(false)
1823    }
1824}
1825
1826pub fn status(state: &LuaState) -> LuaStatus {
1827    LuaStatus::from_raw(state.status as i32)
1828}
1829
1830// ── garbage collection ────────────────────────────────────────────────────────
1831
1832/// GC operation codes (C: LUA_GC* constants)
1833#[repr(i32)]
1834#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1835pub enum GcWhat {
1836    Stop = 0,
1837    Restart = 1,
1838    Collect = 2,
1839    Count = 3,
1840    CountB = 4,
1841    Step = 5,
1842    SetPause = 6,
1843    SetStepMul = 7,
1844    IsRunning = 9,
1845    Gen = 10,
1846    Inc = 11,
1847}
1848
1849// PORT NOTE: C varargs replaced by explicit GcArgs enum; callers supply parameters directly.
1850pub enum GcArgs {
1851    Stop,
1852    Restart,
1853    Collect,
1854    Count,
1855    CountB,
1856    Step { data: i32 },
1857    SetPause { value: i32 },
1858    SetStepMul { value: i32 },
1859    IsRunning,
1860    Gen { minormul: i32, majormul: i32 },
1861    Inc { pause: i32, stepmul: i32, stepsize: i32 },
1862}
1863
1864pub fn gc(state: &mut LuaState, args: GcArgs) -> i32 {
1865    if state.global().is_gc_stopped_internally() {
1866        return -1;
1867    }
1868    match args {
1869        GcArgs::Stop => {
1870            state.global_mut().set_gc_stop_user();
1871        }
1872        GcArgs::Restart => {
1873            {
1874                let mut g = state.global_mut();
1875                crate::state::set_debt(&mut *g, 0);
1876            }
1877            state.global_mut().clear_gc_stop();
1878        }
1879        GcArgs::Collect => {
1880            if !state.allowhook {
1881                return 0;
1882            }
1883            // Under D-2, weak-table sweep happens INSIDE the heap's
1884            // post-mark hook (see GcHandle::full_collect), driven by
1885            // reachability rather than strong_count. The standalone weak
1886            // sweep that used to run here would now be a no-op against an
1887            // already-clean state and is removed.
1888            state.gc().full_collect();
1889            // Phase-B: drain pending __gc finalizers for tables whose user
1890            // refs have all been dropped. Kept for legacy compat; runs
1891            // after the heap's collect so weak entries have been cleared.
1892            run_pending_finalizers(state);
1893            // PORT NOTE: Phase-B long-string accounting. Reclaim `gc_debt`
1894            // for any tracked long-string Rc whose strong count has dropped
1895            // to zero (either because the weak-table sweep above released
1896            // the last reference, or because the user dropped it directly).
1897            // Without this, `collectgarbage("count")` would report peak
1898            // allocation rather than live bytes — gc.lua's weak-string-key
1899            // block depends on the post-collect count being lower than the
1900            // pre-collect count.
1901            {
1902                let mut g = state.global_mut();
1903                crate::state::reclaim_dead_long_strings(&mut *g);
1904            }
1905            // PORT NOTE: Phase B has no per-allocation totalbytes tracking,
1906            // so total_bytes() only ever shrinks (each `Step` simulates
1907            // freed memory). Refill to a baseline here so subsequent Step
1908            // calls have headroom to actually drop count*1024 — the test
1909            // pattern `collectgarbage(); local x = gcinfo(); collectgarbage('step'); assert(gcinfo()<x)`
1910            // needs gcinfo to be high enough that decrementing by 1 KB is
1911            // observable. Removed in Phase D when real GC tracks bytes.
1912            {
1913                let mut g = state.global_mut();
1914                let target_tb = 32_768_isize;
1915                let cur_tb = g.totalbytes + g.gc_debt;
1916                if cur_tb < target_tb {
1917                    g.totalbytes += target_tb - cur_tb;
1918                }
1919            }
1920        }
1921        GcArgs::Count => {
1922            {
1923                let mut g = state.global_mut();
1924                crate::state::reclaim_dead_long_strings(&mut *g);
1925            }
1926            let g = state.global();
1927            let long_string_bytes: usize = g.gc_tracked_long_strings.iter().map(|(_, sz)| sz).sum();
1928            let total = g.heap.bytes_used() + long_string_bytes;
1929            return (total >> 10) as i32;
1930        }
1931        GcArgs::CountB => {
1932            {
1933                let mut g = state.global_mut();
1934                crate::state::reclaim_dead_long_strings(&mut *g);
1935            }
1936            let g = state.global();
1937            let long_string_bytes: usize = g.gc_tracked_long_strings.iter().map(|(_, sz)| sz).sum();
1938            let total = g.heap.bytes_used() + long_string_bytes;
1939            return (total & 0x3ff) as i32;
1940        }
1941        GcArgs::Step { data } => {
1942            let old_stp = {
1943                let mut g = state.global_mut();
1944                let old = g.gc_stop_flags();
1945                g.clear_gc_stop();
1946                old
1947            };
1948            // C-Lua converts `data` KiB of added debt into work units via
1949            // `stepmul`. We use a simpler mapping: the work-unit count is
1950            // `data * stepmul / 4` (stepmul is the user-tunable speed,
1951            // /4-encoded in `gcstepmul`), with a floor of 1 unit. When
1952            // `data == 0` the call still performs one basic step (matching
1953            // C-Lua's `luaC_step(L)` after `setdebt(g, 0)`).
1954            let stepmul = (state.global().gc_stepmul_param() as isize | 1).max(1);
1955            let work_units = if data == 0 {
1956                stepmul
1957            } else {
1958                let raw = (data as isize).saturating_mul(stepmul);
1959                raw.max(1)
1960            };
1961            if data == 0 {
1962                let mut g = state.global_mut();
1963                crate::state::set_debt(&mut *g, 0);
1964            } else {
1965                let debt = data as isize * 1024 + state.global().gc_debt();
1966                let mut g = state.global_mut();
1967                crate::state::set_debt(&mut *g, debt);
1968            }
1969            let cycle_complete = state.gc().incremental_step(work_units);
1970            if state.global().is_gen_mode() {
1971                state.gc().prune_weak_tables_mark_only();
1972            }
1973            state.global_mut().set_gc_stop_flags(old_stp);
1974            // Phase-B byte accounting: real allocation isn't tracked, so
1975            // simulate C-Lua's post-sweep totalbytes drop here. Halving
1976            // the current `tb` makes `gcinfo() < x` hold across a step
1977            // that completes a cycle (gc.lua `dosteps()` line 194), while
1978            // the floor at 1 KB preserves `set_debt`'s `tb > 0` invariant
1979            // across many back-to-back step calls.
1980            if cycle_complete {
1981                let mut g = state.global_mut();
1982                let floor: isize = 1024;
1983                let cur_tb = g.totalbytes + g.gc_debt;
1984                let new_tb = (cur_tb / 2).max(floor);
1985                if new_tb < cur_tb {
1986                    g.totalbytes -= cur_tb - new_tb;
1987                }
1988            }
1989            // Sync the global gcstate byte for `gc_at_pause()` callers.
1990            {
1991                let heap_state = state.global().heap.gc_state();
1992                let mut g = state.global_mut();
1993                g.gcstate = if heap_state.is_pause() { 0 } else { 1 };
1994            }
1995            return if cycle_complete { 1 } else { 0 };
1996        }
1997        GcArgs::SetPause { value } => {
1998            let old = state.global().gc_pause_param() as i32;
1999            state.global_mut().set_gc_pause_param(value as u8);
2000            return old;
2001        }
2002        GcArgs::SetStepMul { value } => {
2003            let old = state.global().gc_stepmul_param() as i32;
2004            state.global_mut().set_gc_stepmul_param(value as u8);
2005            return old;
2006        }
2007        GcArgs::IsRunning => {
2008            return state.global().gc_running() as i32;
2009        }
2010        GcArgs::Gen { minormul, majormul } => {
2011            let old_mode = if state.global().is_gen_mode() { 10i32 } else { 11i32 };
2012            if minormul != 0 {
2013                state.global_mut().genminormul = minormul as u8;
2014            }
2015            if majormul != 0 {
2016                state.global_mut().set_gc_genmajormul(majormul as u8);
2017            }
2018            state.gc().change_mode(crate::state::GcKind::Generational);
2019            return old_mode;
2020        }
2021        GcArgs::Inc { pause, stepmul, stepsize } => {
2022            let old_mode = if state.global().is_gen_mode() { 10i32 } else { 11i32 };
2023            if pause != 0 {
2024                state.global_mut().set_gc_pause_param(pause as u8);
2025            }
2026            if stepmul != 0 {
2027                state.global_mut().set_gc_stepmul_param(stepmul as u8);
2028            }
2029            if stepsize != 0 {
2030                state.global_mut().gcstepsize = stepsize as u8;
2031            }
2032            state.gc().change_mode(crate::state::GcKind::Incremental);
2033            return old_mode;
2034        }
2035    }
2036    0
2037}
2038
2039// ── miscellaneous functions ───────────────────────────────────────────────────
2040
2041// PORT NOTE: returns Result<Infallible, _> — semantically "always Err". The
2042// translator originally wrote `Result<!, _>` but the `!` type in a return
2043// position is still nightly-only as of Rust 1.93; Infallible is the stable
2044// stand-in. Callsites just pattern-match on Err.
2045pub fn lua_error(state: &mut LuaState) -> Result<Infallible, LuaError> {
2046    //      luaM_error(L);  /* memory error */
2047    //    else
2048    //      luaG_errormsg(L);  /* regular error */
2049    let top = state.top_idx();
2050    let errobj = state.get_at(top - 1);
2051    let is_mem_err = if let LuaValue::Str(ref s) = errobj {
2052        let memerr = state.global().memerrmsg.clone();
2053        GcRef::ptr_eq(s, &memerr)
2054    } else {
2055        false
2056    };
2057    if is_mem_err {
2058        Err(LuaError::Memory)
2059    } else {
2060        Err(LuaError::from_value(errobj))
2061    }
2062}
2063
2064pub fn next(state: &mut LuaState, idx: i32) -> Result<bool, LuaError> {
2065    let t = get_table_value(state, idx)
2066        .ok_or_else(|| LuaError::runtime(format_args!("table expected")))?;
2067    let top = state.top_idx();
2068    let key = state.get_at(top - 1);
2069    match t.next(key)? {
2070        Some((next_key, next_val)) => {
2071            state.set_at(top - 1, next_key);
2072            state.push(next_val);
2073            Ok(true)
2074        }
2075        None => {
2076            state.set_top_idx(top - 1);
2077            Ok(false)
2078        }
2079    }
2080}
2081
2082pub fn to_close(state: &mut LuaState, idx: i32) -> Result<(), LuaError> {
2083    let _level = index_to_stack_idx(state, idx);
2084    // TODO(port): luaF_newtbcupval and to-be-closed variable infrastructure
2085    // not yet translated. Stubbing for Phase A.
2086    Ok(())
2087}
2088
2089pub fn concat(state: &mut LuaState, n: i32) -> Result<(), LuaError> {
2090    if n > 0 {
2091        state.concat(n)?;
2092    } else {
2093        let empty = state.intern_str(b"")?;
2094        state.push(LuaValue::Str(empty));
2095    }
2096    state.gc().check_step();
2097    Ok(())
2098}
2099
2100pub fn len(state: &mut LuaState, idx: i32) -> Result<(), LuaError> {
2101    let t = index_to_value(state, idx);
2102    let result = state.obj_len(&t)?;
2103    state.push(result);
2104    Ok(())
2105}
2106
2107// PORT NOTE: The custom allocator hook is not exposed in the Rust-native API.
2108// Rust's allocator handles all allocation.
2109// These are intentionally omitted.
2110
2111pub fn set_warn_f(
2112    state: &mut LuaState,
2113    f: Option<Box<dyn FnMut(&[u8], bool)>>,
2114) {
2115    // PORT NOTE: ud_warn userdata is folded into the closure per types.tsv.
2116    state.global_mut().warnf = f;
2117}
2118
2119pub fn warning(state: &mut LuaState, msg: &[u8], tocont: bool) {
2120    state.emit_warning(msg, tocont);
2121}
2122
2123pub fn new_userdata_uv(
2124    state: &mut LuaState,
2125    size: usize,
2126    nuvalue: i32,
2127) -> Result<GcRef<LuaUserData>, LuaError> {
2128    debug_assert!(nuvalue >= 0 && nuvalue < u16::MAX as i32, "invalid value");
2129    let u = state.new_userdata(size, nuvalue as usize)?;
2130    state.push(LuaValue::UserData(u.clone()));
2131    state.gc().check_step();
2132    Ok(u)
2133}
2134
2135// ── upvalue access ────────────────────────────────────────────────────────────
2136
2137// PORT NOTE: Returns (name, value) instead of mutating output pointers. The name
2138// is returned as an owned Vec<u8> because Lua upvalue names live in the proto's
2139// LuaString table (GC heap), not in static storage.
2140fn aux_upvalue(
2141    state: &LuaState,
2142    fi: &LuaValue,
2143    n: i32,
2144) -> Option<(Vec<u8>, LuaValue)> {
2145    match fi {
2146        LuaValue::Function(LuaClosure::C(ccl)) => {
2147            let nupvalues = ccl.upvalues.len() as i32;
2148            if n < 1 || n > nupvalues {
2149                return None;
2150            }
2151            Some((Vec::new(), ccl.upvalues[(n - 1) as usize].clone()))
2152        }
2153        LuaValue::Function(LuaClosure::Lua(lcl)) => {
2154            let nupvalues = lcl.upvals.len() as i32;
2155            if n < 1 || n > nupvalues {
2156                return None;
2157            }
2158            let val = state.upvalue_get(lcl, (n - 1) as usize);
2159            // The proto records the static name of each upvalue (e.g. "_ENV"
2160            // for the main chunk's environment upvalue). Stripped chunks have
2161            // no upvalue-name debug info; Lua reports those as "(no name)".
2162            let name: Vec<u8> = lcl
2163                .proto
2164                .upvalues
2165                .get((n - 1) as usize)
2166                .and_then(|ud| ud.name.as_ref())
2167                .map(|s| s.as_bytes().to_vec())
2168                .unwrap_or_else(|| b"(no name)".to_vec());
2169            Some((name, val))
2170        }
2171        _ => None,
2172    }
2173}
2174
2175pub fn get_upvalue(state: &mut LuaState, funcindex: i32, n: i32) -> Option<Vec<u8>> {
2176    let fi = index_to_value(state, funcindex);
2177    if let Some((name, val)) = aux_upvalue(state, &fi, n) {
2178        state.push(val);
2179        Some(name)
2180    } else {
2181        None
2182    }
2183}
2184
2185pub fn setup_value(state: &mut LuaState, funcindex: i32, n: i32) -> Option<Vec<u8>> {
2186    let fi = index_to_value(state, funcindex);
2187    let (name, _) = aux_upvalue(state, &fi, n)?;
2188    let new_val = state.pop();
2189    match &fi {
2190        LuaValue::Function(LuaClosure::Lua(lcl)) => {
2191            state.upvalue_set(lcl, (n - 1) as usize, new_val).ok()?;
2192        }
2193        LuaValue::Function(LuaClosure::C(_ccl)) => {
2194            // TODO(port): C-closure upvalue writes need interior mutability on
2195            // LuaCClosure.upvalues. Not exercised by current tests.
2196            let _ = new_val;
2197        }
2198        _ => return None,
2199    }
2200    Some(name)
2201}
2202
2203// PORT NOTE: returns an index into the upvals vec rather than a pointer-to-pointer.
2204// Returns None if n is out of range.
2205fn get_upval_ref_idx(state: &LuaState, fidx: i32, n: i32) -> Option<usize> {
2206    let fi = index_to_value(state, fidx);
2207    debug_assert!(matches!(fi, LuaValue::Function(LuaClosure::Lua(_))), "Lua function expected");
2208    if let LuaValue::Function(LuaClosure::Lua(ref lcl)) = fi {
2209        let sizeupvalues = lcl.upvals.len() as i32;
2210        if n >= 1 && n <= sizeupvalues {
2211            Some((n - 1) as usize)
2212        } else {
2213            None
2214        }
2215    } else {
2216        None
2217    }
2218}
2219
2220// PORT NOTE: Returns Option<usize> identity instead of raw void*.
2221pub fn upvalue_id(state: &LuaState, fidx: i32, n: i32) -> Option<usize> {
2222    let fi = index_to_value(state, fidx);
2223    match &fi {
2224        LuaValue::Function(LuaClosure::Lua(lcl)) => {
2225            let idx = get_upval_ref_idx(state, fidx, n)?;
2226            // Return the identity of the UpVal GcRef
2227            Some(GcRef::identity(&lcl.upval(idx)))
2228        }
2229        LuaValue::Function(LuaClosure::C(ccl)) => {
2230            if n >= 1 && n <= ccl.upvalues.len() as i32 {
2231                // TODO(port): returning address of upvalue slot not possible without raw ptr.
2232                // Return a synthetic identity based on the closure's identity + n.
2233                Some(GcRef::identity(ccl) ^ (n as usize))
2234            } else {
2235                None
2236            }
2237        }
2238        LuaValue::Function(LuaClosure::LightC(_)) => None,
2239        _ => {
2240            debug_assert!(false, "function expected");
2241            None
2242        }
2243    }
2244}
2245
2246//                                               int fidx2, int n2)
2247pub fn upvalue_join(state: &mut LuaState, fidx1: i32, n1: i32, fidx2: i32, n2: i32) {
2248    let idx1 = match get_upval_ref_idx(state, fidx1, n1) {
2249        Some(i) => i,
2250        None => return,
2251    };
2252    let idx2 = match get_upval_ref_idx(state, fidx2, n2) {
2253        Some(i) => i,
2254        None => return,
2255    };
2256    let f1 = index_to_value(state, fidx1);
2257    let f2 = index_to_value(state, fidx2);
2258    if let (
2259        LuaValue::Function(LuaClosure::Lua(lcl1)),
2260        LuaValue::Function(LuaClosure::Lua(lcl2)),
2261    ) = (&f1, &f2)
2262    {
2263        let shared = lcl2.upval(idx2);
2264        lcl1.set_upval(idx1, shared);
2265    }
2266}
2267
2268// ──────────────────────────────────────────────────────────────────────────
2269// PORT STATUS
2270//   source:        src/lapi.c  (1464 lines, ~47 functions)
2271//   target_crate:  lua-vm
2272//   confidence:    low
2273//   todos:         18
2274//   port_notes:    8
2275//   unsafe_blocks: 0   (must be 0 outside explicit unsafe-budget crates)
2276//   notes:         Heavy use of interior mutability TODOs (GcRef writes for
2277//                  metatables, upvalue writes, userdata uv writes). The
2278//                  index2value helper returns cloned LuaValue not a pointer,
2279//                  so write-back paths that C achieves with TValue* are
2280//                  stubbed. Stack pointer arithmetic faithfully translated to
2281//                  StackIdx (u32) arithmetic. va_list functions (pushvfstring,
2282//                  pushfstring) replaced by &[u8] forwarders. lua_gc varargs
2283//                  replaced by explicit GcArgs enum. Raw pointer returns
2284//                  (topointer, touserdata, upvalueid) return Option<usize>
2285//                  identity values; actual *mut void only legal in lua-gc.
2286//                  lua_pushthread stubbed (needs self_gcref()), lua_xmove
2287//                  stubbed (split-borrow), upvalue_join stubbed (GcRef write).
2288//                  Phase B must wire up: state.grow_stack, state.call_no_yield,
2289//                  state.protected_call_raw, state.adjust_results,
2290//                  state.table_get_with_tm, state.table_set_with_tm,
2291//                  state.arith_op, state.concat, state.obj_len,
2292//                  state.obj_to_string, state.str_to_num, state.table_getn,
2293//                  state.registry_value, state.registry_get,
2294//                  GcRef::identity, GcRef::ptr_eq, GlobalState GC accessors.
2295// ──────────────────────────────────────────────────────────────────────────