Skip to main content

lua_vm/
func.rs

1//! Auxiliary functions to manipulate prototypes and closures.
2//!
3//! Port of `reference/lua-5.4.7/src/lfunc.c` (295 lines, 16 functions).
4//! The companion header `lfunc.h` is merged here per PORTING.md §1.
5//!
6//! # Design notes
7//!
8//! The C implementation uses two intrusive linked lists managed through pointer
9//! fields embedded in stack slots and upvalue objects:
10//!
11//! - **`openupval`**: a singly-linked list of `UpVal`s sorted by stack level
12//!   (highest first), threaded through `UpVal.u.open.next / .previous`.
13//! - **`tbclist`**: a to-be-closed variable list encoded as `unsigned short` delta
14//!   offsets stored inside `StackValue.tbclist.delta`.
15//!
16//! Both are replaced in the Rust port:
17//! - `openupval` → `LuaState.openupval: Vec<GcRef<UpVal>>` (descending by StackIdx).
18//! - `tbclist`   → `LuaState.tbclist: Vec<StackIdx>` (back = most recent entry).
19//!
20//! The delta-encoding machinery (MAXDELTA, dummy nodes) is an artifact of the u16
21//! delta field and is entirely superseded by the `Vec<StackIdx>` model.
22
23// PORT NOTE: `LuaProto` is currently a stub in crate::state (from lstate.c's
24// partial port in state.rs). The full `LuaProto` definition belongs in
25// crate::object (lobject.c → object.rs). Fields referenced below will compile
26// once object.rs is written; see TODO(port) at each field site.
27
28// PORT NOTE: `GcRef<T> = Rc<T>` in Phase A–C provides no interior mutability.
29// `close_upval` and `init_upvals` must mutate `UpVal` and `LuaClosure` values
30// that are shared through `GcRef`. In Phase B, the design options are:
31//   (a) `GcRef<T> = Rc<RefCell<T>>` for mutable GC objects, or
32//   (b) a custom `GcCell<T>` wrapper with conditional interior mutability.
33// Both `close_upval` and `init_upvals` carry `TODO(port)` at the mutation sites.
34
35#[allow(unused_imports)]
36use crate::prelude::*;
37
38use crate::state::{GcRef, LuaState, LuaValue, UpVal};
39use lua_types::error::LuaError;
40pub use lua_types::{CallInfoIdx, StackIdx};
41
42// ── lfunc.h constants ─────────────────────────────────────────────────────────
43
44// macros.tsv: CLOSEKTOP → const CLOSE_K_TOP: i32 = -1
45/// Sentinel status meaning "close upvalues but preserve the stack top."
46/// Passed as `status` to `close` / `prep_call_close_mth`.
47pub(crate) const CLOSE_K_TOP: i32 = -1;
48
49// ── Closure allocation ────────────────────────────────────────────────────────
50
51/// Fills a Lua closure's upvalue slots with freshly-allocated closed upvalues,
52/// each holding `LuaValue::Nil`. Used when compiling closures that capture no
53/// live stack variables.
54///
55pub(crate) fn init_upvals(
56    state: &mut LuaState,
57    cl: &GcRef<lua_types::LuaLClosure>,
58) -> Result<(), LuaError> {
59    //      GCObject *o = luaC_newobj(L, LUA_VUPVAL, sizeof(UpVal));
60    //      UpVal *uv = gco2upv(o);
61    //      uv->v.p = &uv->u.value;  /* make it closed */
62    //      setnilvalue(uv->v.p);    /* *o = LuaValue::Nil */
63    //      cl->upvals[i] = uv;
64    //      luaC_objbarrier(L, cl, uv);
65    //  }
66    //
67    // In Rust: create UpVal::Closed(Nil) for each slot; GC barrier is no-op Phase A–C.
68
69    // TODO(port): GcRef<T> = Rc<T> has no interior mutability. Mutating
70    // `cl.upvals[i]` here requires either Rc<RefCell<LuaClosure>> or Rc::get_mut.
71    // The code below captures the intended logic; it will not compile until
72    // GcRef provides a borrow_mut() path (Phase B design decision).
73    let n = cl.upvals.len();
74    for i in 0..n {
75        let uv: GcRef<UpVal> = state.new_upval_closed(LuaValue::Nil);
76        // TODO(port): cl.borrow_mut().as_lua_mut().upvals[i] = Some(uv.clone());
77        // Requires interior mutability; see PORT NOTE at top of file.
78        let _ = (i, uv);
79    }
80    Ok(())
81}
82
83// ── Open-upvalue management ───────────────────────────────────────────────────
84
85/// Creates a new open upvalue for stack slot `level`, inserts it into
86/// `state.openupval` at `insert_pos`, and registers the thread in the
87/// global `twups` list if necessary.
88///
89fn new_open_upval(state: &mut LuaState, level: StackIdx, insert_pos: usize) -> GcRef<UpVal> {
90    //    UpVal *uv = gco2upv(o);
91    //    UpVal *next = *prev;
92    //    uv->v.p = s2v(level);   /* current value lives in the stack */
93    //    uv->u.open.next = next;
94    //    uv->u.open.previous = prev;
95    //    if (next) next->u.open.previous = &uv->u.open.next;
96    //    *prev = uv;
97    //
98    // In Rust: intrusive next/previous fields are gone; Vec insertion replaces
99    // the pointer-threading. The `prev` parameter (UpVal **) becomes `insert_pos`.
100    //
101    // The home thread of the upvalue is whichever thread is currently
102    // executing `find_upval` — it captures one of that thread's stack
103    // slots. Phase E-3 makes this id real so `upvalue_get`/`upvalue_set`
104    // can dispatch through `GlobalState::cross_thread_upvals` when a
105    // coroutine reads or writes an upvalue belonging to its parent.
106    let owner_tid = state.global().current_thread_id as usize;
107    let uv: GcRef<UpVal> = state.new_upval_open(owner_tid, level);
108    // PORT NOTE: Vec insert maintains descending StackIdx order (highest first),
109    // mirroring the C intrusive list where the head is always the topmost slot.
110    state.openupval.insert(insert_pos, uv.clone());
111    // macros.tsv: isintwups → state.in_twups()
112    // TODO(port): implement state.in_twups() and the twups insertion. The method needs to
113    // check whether this LuaState is already in global.twups. Requires either a flag on
114    // LuaState or a scan of global.twups. See also lstate.h discussion in state.rs.
115    if !state_in_twups(state) {
116        // TODO(port): state.global_mut().twups.push(gc_ref_to_this_thread(state));
117        // Deferred: obtaining a GcRef<LuaState> to self requires Arc/Rc self-reference
118        // which is an unsolved design problem for Phase E coroutines.
119    }
120    uv
121}
122
123/// Finds or creates an open upvalue for stack slot `level`.
124///
125/// Searches `state.openupval` (sorted descending by StackIdx) for an existing
126/// open upvalue at exactly `level`. If found, returns it. Otherwise, inserts a
127/// new one at the correct sorted position and returns it.
128///
129pub(crate) fn find_upval(state: &mut LuaState, level: StackIdx) -> GcRef<UpVal> {
130    debug_assert!(
131        state_in_twups(state) || state.openupval.is_empty(),
132        "thread must be in twups if it has open upvalues"
133    );
134    //    while ((p = *pp) != NULL && uplevel(p) >= level) {
135    //      lua_assert(!isdead(G(L), p));
136    //      if (uplevel(p) == level) return p;  /* found */
137    //      pp = &p->u.open.next;
138    //    }
139    //    return newupval(L, level, pp);
140    //
141    // The list is sorted descending. We scan from index 0 (highest) downward.
142    // When we find an entry with idx < level we've passed the insertion point.
143    let mut insert_pos = state.openupval.len(); // default: append at end
144    for (i, uv_ref) in state.openupval.iter().enumerate() {
145        // macros.tsv: uplevel → extract thread_stack_idx from UpVal::Open
146        let uv_idx = match &*uv_ref.slot() {
147            lua_types::UpValState::Open {
148                thread_id: _,
149                idx: thread_stack_idx,
150            } => *thread_stack_idx,
151            lua_types::UpValState::Closed(_) => {
152                debug_assert!(false, "closed upvalue found in openupval list");
153                continue;
154            }
155        };
156        if uv_idx.0 >= level.0 {
157            if uv_idx == level {
158                return uv_ref.clone();
159            }
160            // uv_idx.0 > level.0: this entry is higher on the stack; keep searching.
161        } else {
162            // uv_idx.0 < level.0: correct insertion point reached.
163            insert_pos = i;
164            break;
165        }
166    }
167    new_open_upval(state, level, insert_pos)
168}
169
170// ── Close-method call helpers ─────────────────────────────────────────────────
171
172/// Calls the `__close` metamethod on `obj` with error argument `err`.
173/// `yy` controls whether the call is yieldable (true) or non-yieldable (false).
174///
175/// This function assumes EXTRA_STACK free slots are available.
176///
177fn call_close_method(
178    state: &mut LuaState,
179    obj: LuaValue,
180    err: Option<LuaValue>,
181    yy: bool,
182) -> Result<(), LuaError> {
183    //    const TValue *tm = luaT_gettmbyobj(L, obj, TM_CLOSE);
184    //    setobj2s(L, top, tm);     /* push metamethod */
185    //    setobj2s(L, top + 1, obj); /* 1st arg: self */
186    //    setobj2s(L, top + 2, err); /* 2nd arg: error message */
187    //    L->top.p = top + 3;
188    //    if (yy) luaD_call(L, top, 0);
189    //    else    luaD_callnoyield(L, top, 0);
190    //
191    // In Rust: state.push() manages the top pointer; no pointer arithmetic needed.
192    // setobj2s → state.push(value.clone())
193    // macros.tsv: luaT_gettmbyobj → state.get_tm_by_obj(&obj, TagMethod::Close)
194    let tm = state.get_tm_by_obj(&obj, lua_types::tagmethod::TagMethod::Close);
195    let top = state.top;
196    state.push(tm);
197    state.push(obj);
198    if let Some(err) = err {
199        state.push(err);
200    }
201    // TODO(port): state.call(top, 0) / state.call_noyield(top, 0) —
202    // these methods live in do_.rs (ldo.c); cross-module call.
203    if yy {
204        state.lua_call(top, 0)?;
205    } else {
206        state.lua_callnoyield(top, 0)?;
207    }
208    Ok(())
209}
210
211/// Checks that the value at `level` has a `__close` metamethod, raising a
212/// runtime error if it does not.
213///
214fn check_close_mth(state: &mut LuaState, level: StackIdx) -> Result<(), LuaError> {
215    //    if (ttisnil(tm)) {
216    //      int idx = cast_int(level - L->ci->func.p);
217    //      const char *vname = luaG_findlocal(L, L->ci, idx, NULL);
218    //      if (vname == NULL) vname = "?";
219    //      luaG_runerror(L, "variable '%s' got a non-closable value", vname);
220    //    }
221    //
222    // macros.tsv: s2v(level) → state.stack_at(level) — returns &LuaValue
223    // macros.tsv: ttisnil(tm) → matches!(tm, LuaValue::Nil)
224    let val = state.get_stack_value(level).clone();
225    let tm = state.get_tm_by_obj(&val, lua_types::tagmethod::TagMethod::Close);
226    if matches!(tm, LuaValue::Nil) {
227        // macros.tsv: cast_int → x as i32
228        // CallInfo.func is the StackIdx of the function on the stack.
229        let func_idx = state.current_ci().func;
230        let idx = (level.0 as i32) - (func_idx.0 as i32);
231        let vname_owned: Vec<u8> = state
232            .debug_find_local(state.ci, idx)
233            .unwrap_or_else(|| b"?".to_vec());
234        // PORT NOTE: Lua variable names are ASCII identifiers; `escape_ascii`
235        // produces a Display-compatible wrapper for the byte slice.
236        return Err(LuaError::runtime(format_args!(
237            "variable '{}' got a non-closable value",
238            vname_owned.escape_ascii()
239        )));
240    }
241    Ok(())
242}
243
244/// Prepares and calls the closing method for the variable at `level`.
245///
246/// If `status == CLOSE_K_TOP`, the error argument passed to `__close` is nil.
247/// Otherwise, `set_error_obj` is called to materialise the error at `level + 1`
248/// before the close method is invoked.
249///
250fn prep_call_close_mth(
251    state: &mut LuaState,
252    level: StackIdx,
253    status: i32,
254    yy: bool,
255) -> Result<(), LuaError> {
256    //    TValue *errobj;
257    //    if (status == CLOSEKTOP)
258    //      errobj = &G(L)->nilvalue;  /* error object is nil */
259    //    else {  /* luaD_seterrorobj will set top to level+2 */
260    //      errobj = s2v(level + 1);
261    //      luaD_seterrorobj(L, status, level + 1);
262    //    }
263    //    callclosemethod(L, uv, errobj, yy);
264    //
265    // macros.tsv: s2v(level) → state.stack_at(level), returning &LuaValue
266    // Clone before any mutable operations to avoid borrow conflicts.
267    let uv = state.get_stack_value(level).clone();
268    let err = if state.global().lua_version == lua_types::LuaVersion::V55 {
269        if status == CLOSE_K_TOP || status == lua_types::LuaStatus::Ok as i32 {
270            None
271        } else {
272            state.set_error_obj(status, StackIdx(level.0 + 1))?;
273            Some(state.get_stack_value(StackIdx(level.0 + 1)).clone())
274        }
275    } else if status == CLOSE_K_TOP {
276        Some(LuaValue::Nil)
277    } else {
278        // TODO(port): state.set_error_obj(status, ...) lives in do_.rs (ldo.c).
279        state.set_error_obj(status, StackIdx(level.0 + 1))?;
280        Some(state.get_stack_value(StackIdx(level.0 + 1)).clone())
281    };
282    call_close_method(state, uv, err, yy)
283}
284
285// ── To-be-closed variable management ─────────────────────────────────────────
286
287/// Inserts the variable at `level` into the to-be-closed (`tbc`) list.
288///
289/// If the value is falsy (nil or false) it does not need closing and the
290/// function returns immediately. Otherwise it verifies that the value has a
291/// `__close` metamethod, then records it in `state.tbclist`.
292///
293pub(crate) fn new_tbc_upval(state: &mut LuaState, level: StackIdx) -> Result<(), LuaError> {
294    // In Rust: tbclist is Vec<StackIdx>, "current head" = last element.
295    debug_assert!(
296        state.tbclist.last().map_or(true, |&top| level.0 > top.0),
297        "new tbc entry must be above current tbclist head"
298    );
299    // macros.tsv: l_isfalse → matches!(o, LuaValue::Nil | LuaValue::Bool(false))
300    // Clone before borrow to avoid aliasing with later mutable calls.
301    let val = state.get_stack_value(level).clone();
302    if matches!(val, LuaValue::Nil | LuaValue::Bool(false)) {
303        return Ok(());
304    }
305    check_close_mth(state, level)?;
306    //   while (cast_uint(level - L->tbclist.p) > MAXDELTA) {
307    //     L->tbclist.p += MAXDELTA;
308    //     L->tbclist.p->tbclist.delta = 0;  /* dummy node */
309    //   }
310    //   level->tbclist.delta = cast(unsigned short, level - L->tbclist.p);
311    //   L->tbclist.p = level;
312    //
313    // PORT NOTE: The MAXDELTA / dummy-node mechanism is a C-only optimisation
314    // required because `StackValue.tbclist.delta` is a `u16` (max 65535). With
315    // `Vec<StackIdx>` the index fits a u32 and no dummy nodes are ever needed.
316    state.tbclist.push(level);
317    Ok(())
318}
319
320/// Closes all open upvalues whose stack index is ≥ `level`, transitioning each
321/// from `UpVal::Open { thread_id: _, idx: thread_stack_idx }` to `UpVal::Closed(value)` by copying
322/// the current stack value into the upvalue's own storage.
323///
324pub(crate) fn close_upval(state: &mut LuaState, level: StackIdx) {
325    //      TValue *slot = &uv->u.value;
326    //      lua_assert(uplevel(uv) < L->top.p);
327    //      luaF_unlinkupval(uv);
328    //      setobj(L, slot, uv->v.p);  /* copy stack value into upvalue */
329    //      uv->v.p = slot;            /* now the value lives here */
330    //      if (!iswhite(uv)) { nw2black(uv); luaC_barrier(L, uv, slot); }
331    //  }
332    //
333    // openupval is sorted descending; front element is the topmost open upvalue.
334    loop {
335        let uv = match state.openupval.first() {
336            Some(uv) => uv.clone(),
337            None => break,
338        };
339        let uv_idx = match &*uv.slot() {
340            lua_types::UpValState::Open {
341                thread_id: _,
342                idx: thread_stack_idx,
343            } => *thread_stack_idx,
344            lua_types::UpValState::Closed(_) => {
345                // Cross-thread close/reset paths can leave a stale closed
346                // upvalue in this Vec-backed open list. The C intrusive list
347                // cannot represent that state; in Rust, unlink it and keep
348                // closing the remaining open entries.
349                state.openupval.remove(0);
350                continue;
351            }
352        };
353        if uv_idx.0 < level.0 {
354            break;
355        }
356        // PORT NOTE: C asserts `uplevel(uv) < L->top.p` because the C stack is a
357        // contiguous block where slots above top are undefined. The Rust stack is
358        // a `Vec<StackValue>` whose backing storage outlives any top movement, so
359        // reading `stack[uv_idx]` is always valid here even when `state.top` has
360        // been rolled back below the upvalue (which is exactly what happens on
361        // pcall error unwind, e.g. when `assert_fn` calls `set_top(L, 1)` before
362        // raising). Dropping the C-style assertion lets close_upval correctly
363        // close upvalues during error unwind regardless of top position.
364        state.openupval.remove(0);
365        let stack_val = state.get_stack_value(uv_idx).clone();
366        uv.close_with(stack_val);
367        // macros.tsv: iswhite → obj.is_white(); nw2black → obj.set_black()
368        //             luaC_barrier → state.gc().barrier(p, v) — no-op Phase A–C
369        // TODO(port): GC color methods (is_white, set_black) on GcRef<UpVal>;
370        // Phase D only. Omitted in Phase A–C.
371    }
372}
373
374/// Removes the most-recent entry from `state.tbclist`.
375///
376/// The C version must also skip over any delta==0 "dummy" nodes inserted to
377/// bridge gaps larger than MAXDELTA. In Rust no dummy nodes are ever inserted,
378/// so this is a straight `Vec::pop`.
379///
380fn pop_tbc_list(state: &mut LuaState) {
381    //    lua_assert(tbc->tbclist.delta > 0);  /* first element cannot be dummy */
382    //    tbc -= tbc->tbclist.delta;
383    //    while (tbc > L->stack.p && tbc->tbclist.delta == 0)
384    //      tbc -= MAXDELTA;  /* skip dummy nodes */
385    //    L->tbclist.p = tbc;
386    //
387    // PORT NOTE: Delta-encoding dropped (see new_tbc_upval). Just pop.
388    state.tbclist.pop();
389}
390
391/// Closes all upvalues and to-be-closed variables down to `level`, invoking
392/// `__close` metamethods as needed. Returns the (stable) `level` index.
393///
394/// `status` is passed to `prep_call_close_mth` to determine the error argument:
395/// `CLOSE_K_TOP` means nil; other statuses produce the appropriate error object.
396/// `yy` controls yieldability of the close-method calls.
397///
398pub(crate) fn close(
399    state: &mut LuaState,
400    level: StackIdx,
401    status: i32,
402    yy: bool,
403) -> Result<StackIdx, LuaError> {
404    // macros.tsv: savestack → idx (StackIdx is already stable across reallocs in Rust)
405    // PORT NOTE: savestack / restorestack are no-ops here. In C they save/restore a
406    // pointer as a byte-offset because the stack may reallocate during close-method
407    // calls. In Rust, StackIdx is an index into Vec and remains valid after any resize.
408
409    close_upval(state, level);
410    //      StkId tbc = L->tbclist.p;
411    //      poptbclist(L);
412    //      prepcallclosemth(L, tbc, status, yy);
413    //      level = restorestack(L, levelrel);
414    //    }
415    while state
416        .tbclist
417        .last()
418        .copied()
419        .map_or(false, |tbc| tbc.0 >= level.0)
420    {
421        let tbc = state
422            .tbclist
423            .last()
424            .copied()
425            .expect("tbclist non-empty (just checked)");
426        pop_tbc_list(state);
427        prep_call_close_mth(state, tbc, status, yy)?;
428    }
429    Ok(level)
430}
431
432// ── Debug helpers ─────────────────────────────────────────────────────────────
433
434/// Returns the byte-string name of the `local_number`-th local variable that is
435/// active at bytecode position `pc` in prototype `f`, or `None` if no such
436/// variable exists.
437///
438/// Variables are scanned in order. A variable is active when
439/// `startpc <= pc < endpc`. The first active variable is numbered 1.
440///
441pub(crate) fn get_local_name(
442    f: &crate::state::LuaProto,
443    local_number: i32,
444    pc: i32,
445) -> Option<&[u8]> {
446    //    for (i = 0; i < f->sizelocvars && f->locvars[i].startpc <= pc; i++) {
447    //      if (pc < f->locvars[i].endpc) {  /* is variable active? */
448    //        local_number--;
449    //        if (local_number == 0)
450    //          return getstr(f->locvars[i].varname);
451    //      }
452    //    }
453    //    return NULL;
454    //
455    // macros.tsv: getstr(ts) → ts.as_bytes()  returning &[u8]
456    //
457    // TODO(port): `f.locvars` does not exist on the current LuaProto stub in state.rs.
458    // This will compile once LuaProto gains its full set of fields from object.rs.
459    // The logic below faithfully translates the C loop.
460    let mut remaining = local_number;
461    // We break early once startpc > pc (variables are ordered by startpc).
462    for lv in f.locvars.iter() {
463        if lv.startpc > pc {
464            break;
465        }
466        if pc < lv.endpc {
467            remaining -= 1;
468            if remaining == 0 {
469                // macros.tsv: getstr → ts.as_bytes()
470                return Some(lv.varname.as_bytes());
471            }
472        }
473    }
474    None
475}
476
477// ── Private helpers (Rust-only) ───────────────────────────────────────────────
478
479/// Returns `true` if this thread is already registered in `global.twups`.
480///
481/// iff its twups pointer doesn't point back to itself).
482///
483/// PORT NOTE: In Phase A–D with coroutines stubbed there is effectively a
484/// single thread. The actual `GlobalState.twups` Vec management (insertion in
485/// `new_open_upval`) is deferred to Phase D/E and would require a GcRef-to-self.
486/// Until then we treat every thread as conceptually present in twups, which
487/// satisfies the invariant `state_in_twups || openupval.is_empty()` asserted by
488/// `find_upval`. The actual twups list does not yet drive any behaviour.
489fn state_in_twups(state: &LuaState) -> bool {
490    let _ = state;
491    true
492}
493
494// ── Trait stubs needed for compilation ───────────────────────────────────────
495
496/// Stub methods on `LuaState` assumed by this module.
497///
498/// These will be implemented in their home modules (do_.rs, debug.rs, tagmethods.rs)
499/// and removed from this file in Phase B.
500impl LuaState {
501    /// Returns the `LuaValue` at stack index `idx`.
502    ///
503    /// macros.tsv: `s2v → state.stack_at(idx)`.
504    pub(crate) fn get_stack_value(&self, idx: StackIdx) -> &LuaValue {
505        // TODO(port): bounds-check and return &self.stack[idx.0 as usize].val
506        &self.stack[idx.0 as usize].val
507    }
508
509    /// Returns the current CallInfo (active call frame).
510    ///
511    pub(crate) fn current_ci(&self) -> &crate::state::CallInfo {
512        // TODO(port): return &self.call_info[self.ci.0 as usize]
513        &self.call_info[self.ci.0 as usize]
514    }
515
516    /// Looks up the `__close` (or other) metamethod for a value.
517    ///
518    /// macros.tsv: `fasttm → state.fast_tm(et, e)`.
519    pub(crate) fn get_tm_by_obj(
520        &mut self,
521        val: &LuaValue,
522        tm: lua_types::tagmethod::TagMethod,
523    ) -> LuaValue {
524        let mt: Option<GcRef<lua_types::value::LuaTable>> = match val {
525            LuaValue::Table(t) => t.metatable(),
526            LuaValue::UserData(u) => u.metatable(),
527            other => {
528                let type_idx = other.base_type() as usize;
529                self.global().mt[type_idx].clone()
530            }
531        };
532        match mt {
533            Some(mt_ref) => {
534                let ename = self.global().tmname[tm as usize].clone();
535                mt_ref.get_short_str(&ename)
536            }
537            None => LuaValue::Nil,
538        }
539    }
540
541    /// Calls a Lua or C function (yieldable).
542    ///
543    pub(crate) fn lua_call(&mut self, top: StackIdx, nresults: i32) -> Result<(), LuaError> {
544        crate::do_::call(self, top, nresults)
545    }
546
547    /// Calls a Lua or C function (non-yieldable).
548    ///
549    pub(crate) fn lua_callnoyield(&mut self, top: StackIdx, nresults: i32) -> Result<(), LuaError> {
550        crate::do_::callnoyield(self, top, nresults)
551    }
552
553    /// Sets the error object at a given stack index for a given status code.
554    ///
555    pub(crate) fn set_error_obj(&mut self, status: i32, idx: StackIdx) -> Result<(), LuaError> {
556        let s = lua_types::status::LuaStatus::from_raw(status);
557        crate::do_::set_error_obj(self, s, idx);
558        Ok(())
559    }
560
561    /// Returns the local-variable name at frame position `n` for CallInfo `ci`.
562    ///
563    pub(crate) fn debug_find_local(&self, ci: CallInfoIdx, n: i32) -> Option<Vec<u8>> {
564        crate::debug::find_local(self, ci, n, None)
565    }
566}
567
568// ──────────────────────────────────────────────────────────────────────────
569// PORT STATUS
570//   source:        src/lfunc.c  (295 lines, 16 functions)
571//   target_crate:  lua-vm
572//   confidence:    medium
573//   todos:         36
574//   port_notes:    7
575//   unsafe_blocks: 0
576//   notes:         Logic is faithful. Two blockers for Phase B:
577//                  (1) GcRef<UpVal> needs interior mutability (Rc<RefCell<UpVal>>)
578//                      so close_upval and init_upvals can mutate in-place.
579//                  (2) LuaProto stub in state.rs must gain full field list from
580//                      object.rs before new_proto / get_local_name compile.
581//                  LuaClosureLua.proto needs Option<> wrapper for NULL init in
582//                  new_lua_closure. Stub methods on LuaState (get_tm_by_obj,
583//                  lua_call, set_error_obj, debug_find_local) must be removed
584//                  once their home modules are written (do_.rs, debug.rs,
585//                  tagmethods.rs). The 36 TODO(port) markers include both the
586//                  core design blockers and the stub-method placeholders; the
587//                  stub-method TODOs will auto-resolve as other modules land.
588// ──────────────────────────────────────────────────────────────────────────