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