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