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