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// ──────────────────────────────────────────────────────────────────────────