Skip to main content

lua_vm/
api.rs

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