Skip to main content

lua_vm/
api.rs

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