Skip to main content

lua_vm/
api.rs

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