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
35use std::rc::Rc;
36#[allow(unused_imports)] use crate::prelude::*;
37
38use crate::{
39    state::{
40        GcRef, LuaClosureC, LuaClosureLua, LuaState, LuaValue, UpVal,
41    },
42    tagmethods::TagMethod,
43};
44// TODO(port): import paths will stabilize in Phase B. LuaError lives in
45// lua_types::error once that crate is populated; for now we import from crate::state.
46use lua_types::error::LuaError;
47pub use lua_types::{CallInfoIdx, StackIdx};
48
49// ── lfunc.h constants ─────────────────────────────────────────────────────────
50
51// C: #define CLOSEKTOP (-1)  (lfunc.h)
52// macros.tsv: CLOSEKTOP → const CLOSE_K_TOP: i32 = -1
53/// Sentinel status meaning "close upvalues but preserve the stack top."
54/// Passed as `status` to `close` / `prep_call_close_mth`.
55pub(crate) const CLOSE_K_TOP: i32 = -1;
56
57// C: #define MAXUPVAL 255  (lfunc.h)
58// macros.tsv: MAXUPVAL → const MAX_UPVAL: u8 = 255
59/// Maximum number of upvalues in a single closure (Lua or C).
60/// The value must fit in a VM register (u8).
61pub(crate) const MAX_UPVAL: u8 = 255;
62
63// C: #define MAXMISS 10  (lfunc.h)
64// macros.tsv: MAXMISS → const MAX_MISS: u32 = 10
65/// Maximum consecutive misses before giving up the closure cache in `LuaProto`.
66pub(crate) const MAX_MISS: u32 = 10;
67
68// ── Closure allocation ────────────────────────────────────────────────────────
69
70/// Allocates a new C closure with `nupvals` upvalue slots, all initialised to
71/// `LuaValue::Nil`.
72///
73/// The caller is responsible for setting the function pointer (`f`) and
74/// populating the upvalue slots before exposing the closure to Lua code.
75///
76/// C: `CClosure *luaF_newCclosure(lua_State *L, int nupvals)`
77pub(crate) fn new_c_closure(
78    state: &mut LuaState,
79    nupvals: u8,
80) -> GcRef<crate::state::LuaClosure> {
81    // C: GCObject *o = luaC_newobj(L, LUA_VCCL, sizeCclosure(nupvals));
82    //    CClosure *c = gco2ccl(o);
83    //    c->nupvalues = cast_byte(nupvals);
84    //    return c;
85    //
86    // sizeCclosure is a C allocation-size helper; dropped — Vec handles sizing.
87    // cast_byte(nupvals) → nupvals as u8 (already u8).
88    // luaC_newobj → state.gc().new_obj(...) — in Phase A–C just Rc allocation.
89    // gco2ccl → gc.cast_c_closure() — unnecessary in Rust, enum variant is the cast.
90    //
91    // TODO(port): LuaClosureC.f must be set by the caller. The C pattern allocates
92    // then immediately assigns `c->f = fn`. Either make `f: Option<LuaCFunction>` in
93    // LuaClosureC, or add a `new_c_closure(state, nupvals, f)` parameter. For now we
94    // store a dummy; reconcile in Phase B.
95    let closure = crate::state::LuaClosure::C(GcRef::new(LuaClosureC {
96        // C: c->f is set by caller; placeholder index 0 here. LuaCFnPtr is a
97        // registry index (see lua-types::closure); the caller overwrites it.
98        func: DUMMY_C_FUNCTION_IDX,
99        upvalues: vec![LuaValue::Nil; nupvals as usize],
100    }));
101    // C: luaC_newobj registers with the GC. In Phase A–C this is Rc::new.
102    GcRef::new(closure)
103}
104
105/// Allocates a new Lua closure with `nupvals` upvalue slots (all `None`).
106///
107/// The caller must set the `proto` field and populate `upvals` before the
108/// closure is executed.
109///
110/// C: `LClosure *luaF_newLclosure(lua_State *L, int nupvals)`
111pub(crate) fn new_lua_closure(
112    state: &mut LuaState,
113    nupvals: u8,
114) -> GcRef<crate::state::LuaClosure> {
115    // C: GCObject *o = luaC_newobj(L, LUA_VLCL, sizeLclosure(nupvals));
116    //    LClosure *c = gco2lcl(o);
117    //    c->p = NULL;
118    //    c->nupvalues = cast_byte(nupvals);
119    //    while (nupvals--) c->upvals[nupvals] = NULL;
120    //    return c;
121    //
122    // sizeLclosure → dropped (Vec handles sizing).
123    // c->p = NULL → proto field will be set by caller.
124    // TODO(port): LuaClosureLua.proto is GcRef<LuaProto> (non-optional per types.tsv).
125    // The C code allows NULL here (set later). Either use Option<GcRef<LuaProto>> in
126    // the Rust struct, or require proto at construction time. Reconcile in Phase B.
127    // For Phase A we use a sentinel value; this line will not compile as-is.
128    let _ = state; // state used for GC registration in Phase D
129    let _ = nupvals;
130    // TODO(phase-b): LuaClosureLua.proto is non-optional; need a placeholder
131    // until the caller assigns. Using LuaProto::placeholder() for Phase B compile.
132    let lcl = GcRef::new(LuaClosureLua::placeholder());
133    let closure = crate::state::LuaClosure::Lua(lcl);
134    GcRef::new(closure)
135}
136
137/// Fills a Lua closure's upvalue slots with freshly-allocated closed upvalues,
138/// each holding `LuaValue::Nil`. Used when compiling closures that capture no
139/// live stack variables.
140///
141/// C: `void luaF_initupvals(lua_State *L, LClosure *cl)`
142pub(crate) fn init_upvals(state: &mut LuaState, cl: &GcRef<lua_types::LuaLClosure>) -> Result<(), LuaError> {
143    // C: for (i = 0; i < cl->nupvalues; i++) {
144    //      GCObject *o = luaC_newobj(L, LUA_VUPVAL, sizeof(UpVal));
145    //      UpVal *uv = gco2upv(o);
146    //      uv->v.p = &uv->u.value;  /* make it closed */
147    //      setnilvalue(uv->v.p);    /* *o = LuaValue::Nil */
148    //      cl->upvals[i] = uv;
149    //      luaC_objbarrier(L, cl, uv);
150    //  }
151    //
152    // In Rust: create UpVal::Closed(Nil) for each slot; GC barrier is no-op Phase A–C.
153
154    // TODO(port): GcRef<T> = Rc<T> has no interior mutability. Mutating
155    // `cl.upvals[i]` here requires either Rc<RefCell<LuaClosure>> or Rc::get_mut.
156    // The code below captures the intended logic; it will not compile until
157    // GcRef provides a borrow_mut() path (Phase B design decision).
158    let n = cl.upvals.len();
159    for i in 0..n {
160        // C: luaC_newobj(L, LUA_VUPVAL, sizeof(UpVal)) → Rc::new(UpVal::Closed(Nil))
161        let uv: GcRef<UpVal> = state.new_upval_closed(LuaValue::Nil);
162        // TODO(port): cl.borrow_mut().as_lua_mut().upvals[i] = Some(uv.clone());
163        // Requires interior mutability; see PORT NOTE at top of file.
164        let _ = (i, uv);
165        // C: luaC_objbarrier(L, cl, uv) → state.gc().obj_barrier(cl, &uv) — no-op Phase A–C
166    }
167    Ok(())
168}
169
170// ── Open-upvalue management ───────────────────────────────────────────────────
171
172/// Creates a new open upvalue for stack slot `level`, inserts it into
173/// `state.openupval` at `insert_pos`, and registers the thread in the
174/// global `twups` list if necessary.
175///
176/// C: `static UpVal *newupval(lua_State *L, StkId level, UpVal **prev)`
177fn new_open_upval(
178    state: &mut LuaState,
179    level: StackIdx,
180    insert_pos: usize,
181) -> GcRef<UpVal> {
182    // C: GCObject *o = luaC_newobj(L, LUA_VUPVAL, sizeof(UpVal));
183    //    UpVal *uv = gco2upv(o);
184    //    UpVal *next = *prev;
185    //    uv->v.p = s2v(level);   /* current value lives in the stack */
186    //    uv->u.open.next = next;
187    //    uv->u.open.previous = prev;
188    //    if (next) next->u.open.previous = &uv->u.open.next;
189    //    *prev = uv;
190    //
191    // In Rust: intrusive next/previous fields are gone; Vec insertion replaces
192    // the pointer-threading. The `prev` parameter (UpVal **) becomes `insert_pos`.
193    //
194    // C: UpVal.v.p = s2v(level) → UpVal::Open { thread_id: current, idx: level }
195    // The home thread of the upvalue is whichever thread is currently
196    // executing `find_upval` — it captures one of that thread's stack
197    // slots. Phase E-3 makes this id real so `upvalue_get`/`upvalue_set`
198    // can dispatch through `GlobalState::cross_thread_upvals` when a
199    // coroutine reads or writes an upvalue belonging to its parent.
200    let owner_tid = state.global().current_thread_id as usize;
201    let uv: GcRef<UpVal> = state.new_upval_open(owner_tid, level);
202    // PORT NOTE: Vec insert maintains descending StackIdx order (highest first),
203    // mirroring the C intrusive list where the head is always the topmost slot.
204    state.openupval.insert(insert_pos, uv.clone());
205    // C: if (!isintwups(L)) { L->twups = G(L)->twups; G(L)->twups = L; }
206    // macros.tsv: isintwups → state.in_twups()
207    // TODO(port): implement state.in_twups() and the twups insertion. The method needs to
208    // check whether this LuaState is already in global.twups. Requires either a flag on
209    // LuaState or a scan of global.twups. See also lstate.h discussion in state.rs.
210    if !state_in_twups(state) {
211        // TODO(port): state.global_mut().twups.push(gc_ref_to_this_thread(state));
212        // Deferred: obtaining a GcRef<LuaState> to self requires Arc/Rc self-reference
213        // which is an unsolved design problem for Phase E coroutines.
214    }
215    uv
216}
217
218/// Finds or creates an open upvalue for stack slot `level`.
219///
220/// Searches `state.openupval` (sorted descending by StackIdx) for an existing
221/// open upvalue at exactly `level`. If found, returns it. Otherwise, inserts a
222/// new one at the correct sorted position and returns it.
223///
224/// C: `UpVal *luaF_findupval(lua_State *L, StkId level)`
225pub(crate) fn find_upval(state: &mut LuaState, level: StackIdx) -> GcRef<UpVal> {
226    // C: lua_assert(isintwups(L) || L->openupval == NULL);
227    debug_assert!(
228        state_in_twups(state) || state.openupval.is_empty(),
229        "thread must be in twups if it has open upvalues"
230    );
231    // C: UpVal **pp = &L->openupval;
232    //    while ((p = *pp) != NULL && uplevel(p) >= level) {
233    //      lua_assert(!isdead(G(L), p));
234    //      if (uplevel(p) == level) return p;  /* found */
235    //      pp = &p->u.open.next;
236    //    }
237    //    return newupval(L, level, pp);
238    //
239    // The list is sorted descending. We scan from index 0 (highest) downward.
240    // When we find an entry with idx < level we've passed the insertion point.
241    let mut insert_pos = state.openupval.len(); // default: append at end
242    for (i, uv_ref) in state.openupval.iter().enumerate() {
243        // C: lua_assert(!isdead(G(L), p)) — GC liveness; no-op in Phase A–C
244        // macros.tsv: uplevel → extract thread_stack_idx from UpVal::Open
245        let uv_idx = match &*uv_ref.slot() {
246            lua_types::UpValState::Open { thread_id: _, idx: thread_stack_idx } => *thread_stack_idx,
247            lua_types::UpValState::Closed(_) => {
248                debug_assert!(false, "closed upvalue found in openupval list");
249                continue;
250            }
251        };
252        if uv_idx.0 >= level.0 {
253            if uv_idx == level {
254                // C: if (uplevel(p) == level) return p;
255                return uv_ref.clone();
256            }
257            // uv_idx.0 > level.0: this entry is higher on the stack; keep searching.
258        } else {
259            // uv_idx.0 < level.0: correct insertion point reached.
260            insert_pos = i;
261            break;
262        }
263    }
264    // C: return newupval(L, level, pp);
265    new_open_upval(state, level, insert_pos)
266}
267
268// ── Close-method call helpers ─────────────────────────────────────────────────
269
270/// Calls the `__close` metamethod on `obj` with error argument `err`.
271/// `yy` controls whether the call is yieldable (true) or non-yieldable (false).
272///
273/// This function assumes EXTRA_STACK free slots are available.
274///
275/// C: `static void callclosemethod(lua_State *L, TValue *obj, TValue *err, int yy)`
276fn call_close_method(
277    state: &mut LuaState,
278    obj: LuaValue,
279    err: LuaValue,
280    yy: bool,
281) -> Result<(), LuaError> {
282    // C: StkId top = L->top.p;
283    //    const TValue *tm = luaT_gettmbyobj(L, obj, TM_CLOSE);
284    //    setobj2s(L, top, tm);     /* push metamethod */
285    //    setobj2s(L, top + 1, obj); /* 1st arg: self */
286    //    setobj2s(L, top + 2, err); /* 2nd arg: error message */
287    //    L->top.p = top + 3;
288    //    if (yy) luaD_call(L, top, 0);
289    //    else    luaD_callnoyield(L, top, 0);
290    //
291    // In Rust: state.push() manages the top pointer; no pointer arithmetic needed.
292    // setobj2s → state.push(value.clone())
293    // macros.tsv: luaT_gettmbyobj → state.get_tm_by_obj(&obj, TagMethod::Close)
294    let tm = state.get_tm_by_obj(&obj, lua_types::tagmethod::TagMethod::Close);
295    let top = state.top;
296    state.push(tm);
297    state.push(obj);
298    state.push(err);
299    // C: if (yy) luaD_call(L, top, 0); else luaD_callnoyield(L, top, 0);
300    // TODO(port): state.call(top, 0) / state.call_noyield(top, 0) —
301    // these methods live in do_.rs (ldo.c); cross-module call.
302    if yy {
303        state.lua_call(top, 0)?;
304    } else {
305        state.lua_callnoyield(top, 0)?;
306    }
307    Ok(())
308}
309
310/// Checks that the value at `level` has a `__close` metamethod, raising a
311/// runtime error if it does not.
312///
313/// C: `static void checkclosemth(lua_State *L, StkId level)`
314fn check_close_mth(state: &mut LuaState, level: StackIdx) -> Result<(), LuaError> {
315    // C: const TValue *tm = luaT_gettmbyobj(L, s2v(level), TM_CLOSE);
316    //    if (ttisnil(tm)) {
317    //      int idx = cast_int(level - L->ci->func.p);
318    //      const char *vname = luaG_findlocal(L, L->ci, idx, NULL);
319    //      if (vname == NULL) vname = "?";
320    //      luaG_runerror(L, "variable '%s' got a non-closable value", vname);
321    //    }
322    //
323    // macros.tsv: s2v(level) → state.stack_at(level) — returns &LuaValue
324    // macros.tsv: ttisnil(tm) → matches!(tm, LuaValue::Nil)
325    let val = state.get_stack_value(level).clone();
326    let tm = state.get_tm_by_obj(&val, lua_types::tagmethod::TagMethod::Close);
327    if matches!(tm, LuaValue::Nil) {
328        // C: int idx = cast_int(level - L->ci->func.p);
329        // macros.tsv: cast_int → x as i32
330        // CallInfo.func is the StackIdx of the function on the stack.
331        let func_idx = state.current_ci().func;
332        // C: level - L->ci->func.p — distance from the function slot to the variable
333        let idx = (level.0 as i32) - (func_idx.0 as i32);
334        // C: const char *vname = luaG_findlocal(L, L->ci, idx, NULL);
335        let vname_owned: Vec<u8> = state.debug_find_local(state.ci, idx).unwrap_or_else(|| b"?".to_vec());
336        // C: luaG_runerror(L, "variable '%s' got a non-closable value", vname);
337        // PORT NOTE: Lua variable names are ASCII identifiers; `escape_ascii`
338        // produces a Display-compatible wrapper for the byte slice.
339        return Err(LuaError::runtime(format_args!(
340            "variable '{}' got a non-closable value",
341            vname_owned.escape_ascii()
342        )));
343    }
344    Ok(())
345}
346
347/// Prepares and calls the closing method for the variable at `level`.
348///
349/// If `status == CLOSE_K_TOP`, the error argument passed to `__close` is nil.
350/// Otherwise, `set_error_obj` is called to materialise the error at `level + 1`
351/// before the close method is invoked.
352///
353/// C: `static void prepcallclosemth(lua_State *L, StkId level, int status, int yy)`
354fn prep_call_close_mth(
355    state: &mut LuaState,
356    level: StackIdx,
357    status: i32,
358    yy: bool,
359) -> Result<(), LuaError> {
360    // C: TValue *uv = s2v(level);  /* value being closed */
361    //    TValue *errobj;
362    //    if (status == CLOSEKTOP)
363    //      errobj = &G(L)->nilvalue;  /* error object is nil */
364    //    else {  /* luaD_seterrorobj will set top to level+2 */
365    //      errobj = s2v(level + 1);
366    //      luaD_seterrorobj(L, status, level + 1);
367    //    }
368    //    callclosemethod(L, uv, errobj, yy);
369    //
370    // macros.tsv: s2v(level) → state.stack_at(level), returning &LuaValue
371    // Clone before any mutable operations to avoid borrow conflicts.
372    let uv = state.get_stack_value(level).clone();
373    let err = if status == CLOSE_K_TOP {
374        // C: errobj = &G(L)->nilvalue  — canonical nil; in Rust just Nil
375        LuaValue::Nil
376    } else {
377        // C: luaD_seterrorobj(L, status, level + 1)
378        // TODO(port): state.set_error_obj(status, ...) lives in do_.rs (ldo.c).
379        state.set_error_obj(status, StackIdx(level.0 + 1))?;
380        // C: errobj = s2v(level + 1)
381        state.get_stack_value(StackIdx(level.0 + 1)).clone()
382    };
383    // C: callclosemethod(L, uv, errobj, yy);
384    call_close_method(state, uv, err, yy)
385}
386
387// ── To-be-closed variable management ─────────────────────────────────────────
388
389/// Inserts the variable at `level` into the to-be-closed (`tbc`) list.
390///
391/// If the value is falsy (nil or false) it does not need closing and the
392/// function returns immediately. Otherwise it verifies that the value has a
393/// `__close` metamethod, then records it in `state.tbclist`.
394///
395/// C: `void luaF_newtbcupval(lua_State *L, StkId level)`
396pub(crate) fn new_tbc_upval(state: &mut LuaState, level: StackIdx) -> Result<(), LuaError> {
397    // C: lua_assert(level > L->tbclist.p);
398    // In Rust: tbclist is Vec<StackIdx>, "current head" = last element.
399    debug_assert!(
400        state.tbclist.last().map_or(true, |&top| level.0 > top.0),
401        "new tbc entry must be above current tbclist head"
402    );
403    // C: if (l_isfalse(s2v(level))) return;
404    // macros.tsv: l_isfalse → matches!(o, LuaValue::Nil | LuaValue::Bool(false))
405    // Clone before borrow to avoid aliasing with later mutable calls.
406    let val = state.get_stack_value(level).clone();
407    if matches!(val, LuaValue::Nil | LuaValue::Bool(false)) {
408        return Ok(());
409    }
410    // C: checkclosemth(L, level);
411    check_close_mth(state, level)?;
412    // C: The original delta-encoding loop:
413    //   while (cast_uint(level - L->tbclist.p) > MAXDELTA) {
414    //     L->tbclist.p += MAXDELTA;
415    //     L->tbclist.p->tbclist.delta = 0;  /* dummy node */
416    //   }
417    //   level->tbclist.delta = cast(unsigned short, level - L->tbclist.p);
418    //   L->tbclist.p = level;
419    //
420    // PORT NOTE: The MAXDELTA / dummy-node mechanism is a C-only optimisation
421    // required because `StackValue.tbclist.delta` is a `u16` (max 65535). With
422    // `Vec<StackIdx>` the index fits a u32 and no dummy nodes are ever needed.
423    state.tbclist.push(level);
424    Ok(())
425}
426
427/// Removes the given open upvalue from `state.openupval`.
428///
429/// The C version manipulates intrusive doubly-linked list pointers in O(1). In
430/// Rust we use `Vec::retain` which is O(n) but correct. Phase B can optimise
431/// this if profiling identifies it as hot.
432///
433/// C: `void luaF_unlinkupval(UpVal *uv)` — signature extended with `state`.
434///
435/// PORT NOTE: The original C signature takes only `UpVal *uv` (no `lua_State *`
436/// needed for intrusive-list surgery). In Rust, state is required to find and
437/// remove from the Vec. The public signature is intentionally extended.
438pub(crate) fn unlink_upval(state: &mut LuaState, uv: &GcRef<UpVal>) {
439    // C: lua_assert(upisopen(uv));
440    // macros.tsv: upisopen → matches!(uv, UpVal::Open { .. })
441    debug_assert!(
442        uv.is_open(),
443        "unlink_upval called on a closed upvalue"
444    );
445    // C: *uv->u.open.previous = uv->u.open.next;
446    //    if (uv->u.open.next) uv->u.open.next->u.open.previous = uv->u.open.previous;
447    //
448    // In Rust: find by pointer identity (Rc::ptr_eq) and remove.
449    // PERF(port): O(n) retain vs O(1) intrusive unlink — profile in Phase B.
450    state.openupval.retain(|candidate| !GcRef::ptr_eq(candidate, uv));
451}
452
453/// Closes all open upvalues whose stack index is ≥ `level`, transitioning each
454/// from `UpVal::Open { thread_id: _, idx: thread_stack_idx }` to `UpVal::Closed(value)` by copying
455/// the current stack value into the upvalue's own storage.
456///
457/// C: `void luaF_closeupval(lua_State *L, StkId level)`
458pub(crate) fn close_upval(state: &mut LuaState, level: StackIdx) {
459    // C: while ((uv = L->openupval) != NULL && (upl = uplevel(uv)) >= level) {
460    //      TValue *slot = &uv->u.value;
461    //      lua_assert(uplevel(uv) < L->top.p);
462    //      luaF_unlinkupval(uv);
463    //      setobj(L, slot, uv->v.p);  /* copy stack value into upvalue */
464    //      uv->v.p = slot;            /* now the value lives here */
465    //      if (!iswhite(uv)) { nw2black(uv); luaC_barrier(L, uv, slot); }
466    //  }
467    //
468    // openupval is sorted descending; front element is the topmost open upvalue.
469    loop {
470        let uv = match state.openupval.first() {
471            Some(uv) => uv.clone(),
472            None => break,
473        };
474        let uv_idx = match &*uv.slot() {
475            lua_types::UpValState::Open { thread_id: _, idx: thread_stack_idx } => *thread_stack_idx,
476            lua_types::UpValState::Closed(_) => {
477                // Cross-thread close/reset paths can leave a stale closed
478                // upvalue in this Vec-backed open list. The C intrusive list
479                // cannot represent that state; in Rust, unlink it and keep
480                // closing the remaining open entries.
481                state.openupval.remove(0);
482                continue;
483            }
484        };
485        if uv_idx.0 < level.0 {
486            break;
487        }
488        // PORT NOTE: C asserts `uplevel(uv) < L->top.p` because the C stack is a
489        // contiguous block where slots above top are undefined. The Rust stack is
490        // a `Vec<StackValue>` whose backing storage outlives any top movement, so
491        // reading `stack[uv_idx]` is always valid here even when `state.top` has
492        // been rolled back below the upvalue (which is exactly what happens on
493        // pcall error unwind, e.g. when `assert_fn` calls `set_top(L, 1)` before
494        // raising). Dropping the C-style assertion lets close_upval correctly
495        // close upvalues during error unwind regardless of top position.
496        state.openupval.remove(0);
497        let stack_val = state.get_stack_value(uv_idx).clone();
498        uv.close_with(stack_val);
499        // C: if (!iswhite(uv)) { nw2black(uv); luaC_barrier(L, uv, slot); }
500        // macros.tsv: iswhite → obj.is_white(); nw2black → obj.set_black()
501        //             luaC_barrier → state.gc().barrier(p, v) — no-op Phase A–C
502        // TODO(port): GC color methods (is_white, set_black) on GcRef<UpVal>;
503        // Phase D only. Omitted in Phase A–C.
504    }
505}
506
507/// Removes the most-recent entry from `state.tbclist`.
508///
509/// The C version must also skip over any delta==0 "dummy" nodes inserted to
510/// bridge gaps larger than MAXDELTA. In Rust no dummy nodes are ever inserted,
511/// so this is a straight `Vec::pop`.
512///
513/// C: `static void poptbclist(lua_State *L)`
514fn pop_tbc_list(state: &mut LuaState) {
515    // C: StkId tbc = L->tbclist.p;
516    //    lua_assert(tbc->tbclist.delta > 0);  /* first element cannot be dummy */
517    //    tbc -= tbc->tbclist.delta;
518    //    while (tbc > L->stack.p && tbc->tbclist.delta == 0)
519    //      tbc -= MAXDELTA;  /* skip dummy nodes */
520    //    L->tbclist.p = tbc;
521    //
522    // PORT NOTE: Delta-encoding dropped (see new_tbc_upval). Just pop.
523    state.tbclist.pop();
524}
525
526/// Closes all upvalues and to-be-closed variables down to `level`, invoking
527/// `__close` metamethods as needed. Returns the (stable) `level` index.
528///
529/// `status` is passed to `prep_call_close_mth` to determine the error argument:
530/// `CLOSE_K_TOP` means nil; other statuses produce the appropriate error object.
531/// `yy` controls yieldability of the close-method calls.
532///
533/// C: `StkId luaF_close(lua_State *L, StkId level, int status, int yy)`
534pub(crate) fn close(
535    state: &mut LuaState,
536    level: StackIdx,
537    status: i32,
538    yy: bool,
539) -> Result<StackIdx, LuaError> {
540    // C: ptrdiff_t levelrel = savestack(L, level);
541    // macros.tsv: savestack → idx (StackIdx is already stable across reallocs in Rust)
542    // PORT NOTE: savestack / restorestack are no-ops here. In C they save/restore a
543    // pointer as a byte-offset because the stack may reallocate during close-method
544    // calls. In Rust, StackIdx is an index into Vec and remains valid after any resize.
545
546    // C: luaF_closeupval(L, level);
547    close_upval(state, level);
548    // C: while (L->tbclist.p >= level) {
549    //      StkId tbc = L->tbclist.p;
550    //      poptbclist(L);
551    //      prepcallclosemth(L, tbc, status, yy);
552    //      level = restorestack(L, levelrel);
553    //    }
554    while state.tbclist.last().copied().map_or(false, |tbc| tbc.0 >= level.0) {
555        // C: StkId tbc = L->tbclist.p;
556        let tbc = state
557            .tbclist
558            .last()
559            .copied()
560            .expect("tbclist non-empty (just checked)");
561        // C: poptbclist(L);
562        pop_tbc_list(state);
563        // C: prepcallclosemth(L, tbc, status, yy);
564        prep_call_close_mth(state, tbc, status, yy)?;
565        // C: level = restorestack(L, levelrel); — no-op in Rust (StackIdx is stable)
566    }
567    Ok(level)
568}
569
570// ── Prototype management ──────────────────────────────────────────────────────
571
572/// Allocates and zero-initialises a new `LuaProto`.
573///
574/// All slice fields start empty; the caller (parser / compiler) fills them in.
575///
576/// C: `Proto *luaF_newproto(lua_State *L)`
577pub(crate) fn new_proto(state: &mut LuaState) -> GcRef<crate::state::LuaProto> {
578    // C: GCObject *o = luaC_newobj(L, LUA_VPROTO, sizeof(Proto));
579    //    Proto *f = gco2p(o);
580    //    f->k = NULL;    f->sizek = 0;
581    //    f->p = NULL;    f->sizep = 0;
582    //    f->code = NULL; f->sizecode = 0;
583    //    f->lineinfo = NULL;    f->sizelineinfo = 0;
584    //    f->abslineinfo = NULL; f->sizeabslineinfo = 0;
585    //    f->upvalues = NULL;    f->sizeupvalues = 0;
586    //    f->numparams = 0;
587    //    f->is_vararg = 0;
588    //    f->maxstacksize = 0;
589    //    f->locvars = NULL;     f->sizelocvars = 0;
590    //    f->linedefined = 0;
591    //    f->lastlinedefined = 0;
592    //    f->source = NULL;
593    //    return f;
594    //
595    // In Rust: Vec and Option field types subsume all size companions and NULL checks.
596    // TODO(port): LuaProto in crate::state is currently a stub (`pub struct LuaProto;`).
597    // The full struct definition (with all fields from types.tsv) must land in
598    // object.rs (lobject.c → crate::object). The Rc::new below will only work once
599    // that struct has fields. This translation captures the intended initialisation.
600    state.new_proto()
601}
602
603/// Frees a function prototype and all its sub-arrays.
604///
605/// In C this explicitly calls `luaM_freearray` for each sub-array and then
606/// `luaM_free` for the proto itself. In Rust, `Drop` releases all memory when
607/// the last `GcRef<LuaProto>` (i.e., `Rc<LuaProto>`) is dropped.
608///
609/// C: `void luaF_freeproto(lua_State *L, Proto *f)`
610pub(crate) fn free_proto(_state: &mut LuaState, _f: GcRef<crate::state::LuaProto>) {
611    // C: luaM_freearray(L, f->code, f->sizecode);
612    //    luaM_freearray(L, f->p,    f->sizep);
613    //    luaM_freearray(L, f->k,    f->sizek);
614    //    luaM_freearray(L, f->lineinfo,    f->sizelineinfo);
615    //    luaM_freearray(L, f->abslineinfo, f->sizeabslineinfo);
616    //    luaM_freearray(L, f->locvars,  f->sizelocvars);
617    //    luaM_freearray(L, f->upvalues, f->sizeupvalues);
618    //    luaM_free(L, f);
619    //
620    // macros.tsv: luaM_freearray → no-op (Rust Drop handles deallocation)
621    //             luaM_free      → no-op
622    //
623    // PORT NOTE: All explicit frees are no-ops. The GcRef (Rc) reference count drops
624    // to zero when `_f` is dropped at the end of this function, which in turn drops
625    // all Vec fields recursively. No action needed in Phase A–D; Phase D GC will
626    // call this via the `Collectable` finaliser interface.
627}
628
629// ── Debug helpers ─────────────────────────────────────────────────────────────
630
631/// Returns the byte-string name of the `local_number`-th local variable that is
632/// active at bytecode position `pc` in prototype `f`, or `None` if no such
633/// variable exists.
634///
635/// Variables are scanned in order. A variable is active when
636/// `startpc <= pc < endpc`. The first active variable is numbered 1.
637///
638/// C: `const char *luaF_getlocalname(const Proto *f, int local_number, int pc)`
639pub(crate) fn get_local_name(
640    f: &crate::state::LuaProto,
641    local_number: i32,
642    pc: i32,
643) -> Option<&[u8]> {
644    // C: int i;
645    //    for (i = 0; i < f->sizelocvars && f->locvars[i].startpc <= pc; i++) {
646    //      if (pc < f->locvars[i].endpc) {  /* is variable active? */
647    //        local_number--;
648    //        if (local_number == 0)
649    //          return getstr(f->locvars[i].varname);
650    //      }
651    //    }
652    //    return NULL;
653    //
654    // macros.tsv: getstr(ts) → ts.as_bytes()  returning &[u8]
655    //
656    // TODO(port): `f.locvars` does not exist on the current LuaProto stub in state.rs.
657    // This will compile once LuaProto gains its full set of fields from object.rs.
658    // The logic below faithfully translates the C loop.
659    let mut remaining = local_number;
660    // C: f->locvars[i].startpc <= pc is the loop continuation condition.
661    // We break early once startpc > pc (variables are ordered by startpc).
662    for lv in f.locvars.iter() {
663        if lv.startpc > pc {
664            break;
665        }
666        if pc < lv.endpc {
667            // C: local_number--;
668            remaining -= 1;
669            if remaining == 0 {
670                // C: return getstr(f->locvars[i].varname);
671                // macros.tsv: getstr → ts.as_bytes()
672                return Some(lv.varname.as_bytes());
673            }
674        }
675    }
676    // C: return NULL;
677    None
678}
679
680// ── Private helpers (Rust-only) ───────────────────────────────────────────────
681
682/// Sentinel index into `GlobalState.c_functions` used as a placeholder when a
683/// CClosure is first allocated, before its real function pointer is set by
684/// the caller. Calling through this index is a bug; the caller must overwrite
685/// the slot before the closure is invoked.
686const DUMMY_C_FUNCTION_IDX: crate::state::LuaCFnPtr = usize::MAX;
687
688/// Returns `true` if this thread is already registered in `global.twups`.
689///
690/// C: `isintwups(L)` → `L->twups != L` (intrusive list: thread is in twups
691/// iff its twups pointer doesn't point back to itself).
692///
693/// PORT NOTE: In Phase A–D with coroutines stubbed there is effectively a
694/// single thread. The actual `GlobalState.twups` Vec management (insertion in
695/// `new_open_upval`) is deferred to Phase D/E and would require a GcRef-to-self.
696/// Until then we treat every thread as conceptually present in twups, which
697/// satisfies the invariant `state_in_twups || openupval.is_empty()` asserted by
698/// `find_upval`. The actual twups list does not yet drive any behaviour.
699fn state_in_twups(state: &LuaState) -> bool {
700    let _ = state;
701    true
702}
703
704// ── Trait stubs needed for compilation ───────────────────────────────────────
705
706/// Stub methods on `LuaState` assumed by this module.
707///
708/// These will be implemented in their home modules (do_.rs, debug.rs, tagmethods.rs)
709/// and removed from this file in Phase B.
710impl LuaState {
711    /// Returns the `LuaValue` at stack index `idx`.
712    ///
713    /// C: `s2v(level)` (access the TValue inside a StackValue).
714    /// macros.tsv: `s2v → state.stack_at(idx)`.
715    pub(crate) fn get_stack_value(&self, idx: StackIdx) -> &LuaValue {
716        // TODO(port): bounds-check and return &self.stack[idx.0 as usize].val
717        &self.stack[idx.0 as usize].val
718    }
719
720    /// Returns the current CallInfo (active call frame).
721    ///
722    /// C: `L->ci` (dereferenced).
723    pub(crate) fn current_ci(&self) -> &crate::state::CallInfo {
724        // TODO(port): return &self.call_info[self.ci.0 as usize]
725        &self.call_info[self.ci.0 as usize]
726    }
727
728    /// Looks up the `__close` (or other) metamethod for a value.
729    ///
730    /// C: `luaT_gettmbyobj(L, obj, TM_CLOSE)`.
731    /// macros.tsv: `fasttm → state.fast_tm(et, e)`.
732    pub(crate) fn get_tm_by_obj(
733        &mut self,
734        val: &LuaValue,
735        tm: lua_types::tagmethod::TagMethod,
736    ) -> LuaValue {
737        let mt: Option<GcRef<lua_types::value::LuaTable>> = match val {
738            LuaValue::Table(t) => t.metatable(),
739            LuaValue::UserData(u) => u.metatable(),
740            other => {
741                let type_idx = other.base_type() as usize;
742                self.global().mt[type_idx].clone()
743            }
744        };
745        match mt {
746            Some(mt_ref) => {
747                let ename = self.global().tmname[tm as usize].clone();
748                mt_ref.get_short_str(&ename)
749            }
750            None => LuaValue::Nil,
751        }
752    }
753
754    /// Calls a Lua or C function (yieldable).
755    ///
756    /// C: `luaD_call(L, top, nresults)`.
757    pub(crate) fn lua_call(&mut self, top: StackIdx, nresults: i32) -> Result<(), LuaError> {
758        crate::do_::call(self, top, nresults)
759    }
760
761    /// Calls a Lua or C function (non-yieldable).
762    ///
763    /// C: `luaD_callnoyield(L, top, nresults)`.
764    pub(crate) fn lua_callnoyield(
765        &mut self,
766        top: StackIdx,
767        nresults: i32,
768    ) -> Result<(), LuaError> {
769        crate::do_::callnoyield(self, top, nresults)
770    }
771
772    /// Sets the error object at a given stack index for a given status code.
773    ///
774    /// C: `luaD_seterrorobj(L, status, level)`.
775    pub(crate) fn set_error_obj(
776        &mut self,
777        status: i32,
778        idx: StackIdx,
779    ) -> Result<(), LuaError> {
780        let s = lua_types::status::LuaStatus::from_raw(status);
781        crate::do_::set_error_obj(self, s, idx);
782        Ok(())
783    }
784
785    /// Returns the local-variable name at frame position `n` for CallInfo `ci`.
786    ///
787    /// C: `luaG_findlocal(L, ci, n, NULL)`.
788    pub(crate) fn debug_find_local(
789        &self,
790        ci: CallInfoIdx,
791        n: i32,
792    ) -> Option<Vec<u8>> {
793        crate::debug::find_local(self, ci, n, None)
794    }
795}
796
797// ──────────────────────────────────────────────────────────────────────────
798// PORT STATUS
799//   source:        src/lfunc.c  (295 lines, 16 functions)
800//   target_crate:  lua-vm
801//   confidence:    medium
802//   todos:         36
803//   port_notes:    7
804//   unsafe_blocks: 0
805//   notes:         Logic is faithful. Two blockers for Phase B:
806//                  (1) GcRef<UpVal> needs interior mutability (Rc<RefCell<UpVal>>)
807//                      so close_upval and init_upvals can mutate in-place.
808//                  (2) LuaProto stub in state.rs must gain full field list from
809//                      object.rs before new_proto / get_local_name compile.
810//                  LuaClosureLua.proto needs Option<> wrapper for NULL init in
811//                  new_lua_closure. Stub methods on LuaState (get_tm_by_obj,
812//                  lua_call, set_error_obj, debug_find_local) must be removed
813//                  once their home modules are written (do_.rs, debug.rs,
814//                  tagmethods.rs). The 36 TODO(port) markers include both the
815//                  core design blockers and the stub-method placeholders; the
816//                  stub-method TODOs will auto-resolve as other modules land.
817// ──────────────────────────────────────────────────────────────────────────