Skip to main content

lua_vm/
api.rs

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