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