Skip to main content

lua_vm/
func.rs

1//! Auxiliary functions to manipulate prototypes and closures.
2//!
3//! Ported from `lfunc.c`/`lfunc.h`.
4//!
5//! # Design notes
6//!
7//! The C implementation uses two intrusive linked lists managed through pointer
8//! fields embedded in stack slots and upvalue objects:
9//!
10//! - **`openupval`**: a singly-linked list of `UpVal`s sorted by stack level
11//!   (highest first), threaded through `UpVal.u.open.next / .previous`.
12//! - **`tbclist`**: a to-be-closed variable list encoded as `unsigned short` delta
13//!   offsets stored inside `StackValue.tbclist.delta`.
14//!
15//! Both are replaced in the Rust port:
16//! - `openupval` → `LuaState.openupval: Vec<GcRef<UpVal>>` (descending by StackIdx).
17//! - `tbclist`   → `LuaState.tbclist: Vec<StackIdx>` (back = most recent entry).
18//!
19//! The delta-encoding machinery (MAXDELTA, dummy nodes) is an artifact of the u16
20//! delta field and is entirely superseded by the `Vec<StackIdx>` model.
21
22#[allow(unused_imports)]
23use crate::prelude::*;
24
25use crate::state::{GcRef, LuaState, LuaValue, UpVal};
26use lua_types::error::LuaError;
27pub use lua_types::{CallInfoIdx, StackIdx};
28
29// ── lfunc.h constants ─────────────────────────────────────────────────────────
30
31/// Sentinel status meaning "close upvalues but preserve the stack top."
32/// Passed as `status` to `close` / `prep_call_close_mth`.
33pub(crate) const CLOSE_K_TOP: i32 = -1;
34
35// ── Closure allocation ────────────────────────────────────────────────────────
36
37// C's `luaF_initupvals` (called from `ldo.c::f_parser`) fills a freshly
38// loaded or parsed main closure's upvalue slots, which `luaU_undump` /
39// `luaY_parser` leave NULL. This port has no equivalent: a
40// `Cell<GcRef<UpVal>>` slot is non-nullable, so every closure-producing path
41// (`undump` via `state.new_lclosure`, the text parser hook) fills its slots
42// with fresh closed nil upvalues at construction rather than deferring the
43// fill. The deferred-fill helper was therefore removed as dead code (issue
44// #276); see the note in `do_.rs::f_parser`.
45
46// ── Open-upvalue management ───────────────────────────────────────────────────
47
48/// Creates a new open upvalue for stack slot `level`, inserts it into
49/// `state.openupval` at `insert_pos`, and registers the thread in the
50/// global `twups` list if necessary.
51///
52fn new_open_upval(state: &mut LuaState, level: StackIdx, insert_pos: usize) -> GcRef<UpVal> {
53    // C's intrusive next/previous fields are gone; Vec insertion replaces the
54    // pointer-threading, and the `prev` parameter (UpVal **) becomes
55    // `insert_pos`.
56    //
57    // The home thread of the upvalue is whichever thread is currently
58    // executing `find_upval` — it captures one of that thread's stack slots.
59    // `GlobalState::cross_thread_upvals` is what a coroutine actually reads
60    // or writes through when accessing an upvalue belonging to its parent.
61    let owner_tid = state.global().current_thread_id;
62    let uv: GcRef<UpVal> = state.new_upval_open(owner_tid, level);
63    // Vec insert maintains descending StackIdx order (highest first),
64    // mirroring the C intrusive list where the head is always the topmost slot.
65    state.openupval.insert(insert_pos, uv.clone());
66    if !state_in_twups(state) {
67    }
68    uv
69}
70
71/// Finds or creates an open upvalue for stack slot `level`.
72///
73/// Searches `state.openupval` (sorted descending by StackIdx) for an existing
74/// open upvalue at exactly `level`. If found, returns it. Otherwise, inserts a
75/// new one at the correct sorted position and returns it.
76///
77pub(crate) fn find_upval(state: &mut LuaState, level: StackIdx) -> GcRef<UpVal> {
78    debug_assert!(
79        state_in_twups(state) || state.openupval.is_empty(),
80        "thread must be in twups if it has open upvalues"
81    );
82    // The list is sorted descending. We scan from index 0 (highest) downward.
83    // When we find an entry with idx < level we've passed the insertion point.
84    let mut insert_pos = state.openupval.len(); // default: append at end
85    for (i, uv_ref) in state.openupval.iter().enumerate() {
86        let uv_idx = match uv_ref.try_open_payload() {
87            Some((_thread_id, thread_stack_idx)) => thread_stack_idx,
88            None => {
89                debug_assert!(false, "closed upvalue found in openupval list");
90                continue;
91            }
92        };
93        if uv_idx.0 >= level.0 {
94            if uv_idx == level {
95                return uv_ref.clone();
96            }
97            // uv_idx.0 > level.0: this entry is higher on the stack; keep searching.
98        } else {
99            // uv_idx.0 < level.0: correct insertion point reached.
100            insert_pos = i;
101            break;
102        }
103    }
104    new_open_upval(state, level, insert_pos)
105}
106
107// ── Close-method call helpers ─────────────────────────────────────────────────
108
109/// Calls the `__close` metamethod on `obj` with error argument `err`.
110/// `yy` controls whether the call is yieldable (true) or non-yieldable (false).
111///
112/// This function assumes EXTRA_STACK free slots are available.
113///
114fn call_close_method(
115    state: &mut LuaState,
116    obj: LuaValue,
117    err: Option<LuaValue>,
118    yy: bool,
119) -> Result<(), LuaError> {
120    // state.push() manages the top pointer; no pointer arithmetic needed.
121    let tm = state.get_tm_by_obj(&obj, lua_types::tagmethod::TagMethod::Close);
122    let top = state.top;
123    state.push(tm);
124    state.push(obj);
125    if let Some(err) = err {
126        state.push(err);
127    }
128    if yy {
129        state.lua_call(top, 0)?;
130    } else {
131        state.lua_callnoyield(top, 0)?;
132    }
133    Ok(())
134}
135
136/// Checks that the value at `level` has a `__close` metamethod, raising a
137/// runtime error if it does not.
138///
139fn check_close_mth(state: &mut LuaState, level: StackIdx) -> Result<(), LuaError> {
140    let val = state.get_stack_value(level).clone();
141    let tm = state.get_tm_by_obj(&val, lua_types::tagmethod::TagMethod::Close);
142    if matches!(tm, LuaValue::Nil) {
143        // CallInfo.func is the StackIdx of the function on the stack.
144        let func_idx = state.current_ci().func;
145        let idx = (level.0 as i32) - (func_idx.0 as i32);
146        let vname_owned: Vec<u8> = state
147            .debug_find_local(state.ci, idx)
148            .unwrap_or_else(|| b"?".to_vec());
149        // Lua variable names are ASCII identifiers; `escape_ascii` produces a
150        // Display-compatible wrapper for the byte slice.
151        return Err(LuaError::runtime(format_args!(
152            "variable '{}' got a non-closable value",
153            vname_owned.escape_ascii()
154        )));
155    }
156    Ok(())
157}
158
159/// Prepares and calls the closing method for the variable at `level`.
160///
161/// If `status == CLOSE_K_TOP`, the error argument passed to `__close` is nil.
162/// Otherwise, `set_error_obj` is called to materialise the error at `level + 1`
163/// before the close method is invoked.
164///
165fn prep_call_close_mth(
166    state: &mut LuaState,
167    level: StackIdx,
168    status: i32,
169    yy: bool,
170) -> Result<(), LuaError> {
171    // Clone before any mutable operations to avoid borrow conflicts.
172    let uv = state.get_stack_value(level).clone();
173    let err = if state.global().lua_version == lua_types::LuaVersion::V55 {
174        if status == CLOSE_K_TOP || status == lua_types::LuaStatus::Ok as i32 {
175            None
176        } else {
177            state.set_error_obj(status, StackIdx(level.0 + 1))?;
178            Some(state.get_stack_value(StackIdx(level.0 + 1)).clone())
179        }
180    } else if status == CLOSE_K_TOP {
181        Some(LuaValue::Nil)
182    } else {
183        state.set_error_obj(status, StackIdx(level.0 + 1))?;
184        Some(state.get_stack_value(StackIdx(level.0 + 1)).clone())
185    };
186    call_close_method(state, uv, err, yy)
187}
188
189// ── To-be-closed variable management ─────────────────────────────────────────
190
191/// Inserts the variable at `level` into the to-be-closed (`tbc`) list.
192///
193/// If the value is falsy (nil or false) it does not need closing and the
194/// function returns immediately. Otherwise it verifies that the value has a
195/// `__close` metamethod, then records it in `state.tbclist`.
196///
197pub(crate) fn new_tbc_upval(state: &mut LuaState, level: StackIdx) -> Result<(), LuaError> {
198    // In Rust: tbclist is Vec<StackIdx>, "current head" = last element.
199    debug_assert!(
200        state.tbclist.last().map_or(true, |&top| level.0 > top.0),
201        "new tbc entry must be above current tbclist head"
202    );
203    // Clone before borrow to avoid aliasing with later mutable calls.
204    let val = state.get_stack_value(level).clone();
205    if matches!(val, LuaValue::Nil | LuaValue::Bool(false)) {
206        return Ok(());
207    }
208    check_close_mth(state, level)?;
209    // The MAXDELTA / dummy-node mechanism in C is an optimisation required
210    // because `StackValue.tbclist.delta` is a `u16` (max 65535). With
211    // `Vec<StackIdx>` the index fits a u32 and no dummy nodes are ever needed.
212    state.tbclist.push(level);
213    Ok(())
214}
215
216/// Closes all open upvalues whose stack index is ≥ `level`, transitioning each
217/// from `UpVal::Open { thread_id: _, idx: thread_stack_idx }` to `UpVal::Closed(value)` by copying
218/// the current stack value into the upvalue's own storage.
219///
220pub(crate) fn close_upval(state: &mut LuaState, level: StackIdx) {
221    // openupval is sorted descending; front element is the topmost open upvalue.
222    loop {
223        let uv = match state.openupval.first() {
224            Some(uv) => uv.clone(),
225            None => break,
226        };
227        let uv_idx = match uv.try_open_payload() {
228            Some((_thread_id, thread_stack_idx)) => thread_stack_idx,
229            None => {
230                // Cross-thread close/reset paths can leave a stale closed
231                // upvalue in this Vec-backed open list. The C intrusive list
232                // cannot represent that state; in Rust, unlink it and keep
233                // closing the remaining open entries.
234                state.openupval.remove(0);
235                continue;
236            }
237        };
238        if uv_idx.0 < level.0 {
239            break;
240        }
241        // C asserts `uplevel(uv) < L->top.p` because the C stack is a
242        // contiguous block where slots above top are undefined. The Rust stack is
243        // a `Vec<StackValue>` whose backing storage outlives any top movement, so
244        // reading `stack[uv_idx]` is always valid here even when `state.top` has
245        // been rolled back below the upvalue (which is exactly what happens on
246        // pcall error unwind, e.g. when `assert_fn` calls `set_top(L, 1)` before
247        // raising). Dropping the C-style assertion lets close_upval correctly
248        // close upvalues during error unwind regardless of top position.
249        state.openupval.remove(0);
250        let stack_val = state.get_stack_value(uv_idx).clone();
251        uv.close_with(stack_val);
252    }
253}
254
255/// Removes the most-recent entry from `state.tbclist`.
256///
257/// The C version must also skip over any delta==0 "dummy" nodes inserted to
258/// bridge gaps larger than MAXDELTA. In Rust no dummy nodes are ever inserted,
259/// so this is a straight `Vec::pop`.
260///
261fn pop_tbc_list(state: &mut LuaState) {
262    // Delta-encoding dropped (see new_tbc_upval). Just pop.
263    state.tbclist.pop();
264}
265
266/// Closes all upvalues and to-be-closed variables down to `level`, invoking
267/// `__close` metamethods as needed. Returns the (stable) `level` index.
268///
269/// `status` is passed to `prep_call_close_mth` to determine the error argument:
270/// `CLOSE_K_TOP` means nil; other statuses produce the appropriate error object.
271/// `yy` controls yieldability of the close-method calls.
272///
273pub(crate) fn close(
274    state: &mut LuaState,
275    level: StackIdx,
276    status: i32,
277    yy: bool,
278) -> Result<StackIdx, LuaError> {
279    // savestack / restorestack are no-ops here. In C they save/restore a
280    // pointer as a byte-offset because the stack may reallocate during close-method
281    // calls. Here, StackIdx is an index into Vec and remains valid after any resize.
282
283    close_upval(state, level);
284    while state
285        .tbclist
286        .last()
287        .copied()
288        .map_or(false, |tbc| tbc.0 >= level.0)
289    {
290        let tbc = state
291            .tbclist
292            .last()
293            .copied()
294            .expect("tbclist non-empty (just checked)");
295        pop_tbc_list(state);
296        prep_call_close_mth(state, tbc, status, yy)?;
297    }
298    Ok(level)
299}
300
301// ── Debug helpers ─────────────────────────────────────────────────────────────
302
303/// Returns the byte-string name of the `local_number`-th local variable that is
304/// active at bytecode position `pc` in prototype `f`, or `None` if no such
305/// variable exists.
306///
307/// Variables are scanned in order. A variable is active when
308/// `startpc <= pc < endpc`. The first active variable is numbered 1.
309///
310pub(crate) fn get_local_name(
311    f: &crate::state::LuaProto,
312    local_number: i32,
313    pc: i32,
314) -> Option<&[u8]> {
315    let mut remaining = local_number;
316    // We break early once startpc > pc (variables are ordered by startpc).
317    for lv in f.locvars.iter() {
318        if lv.startpc > pc {
319            break;
320        }
321        if pc < lv.endpc {
322            remaining -= 1;
323            if remaining == 0 {
324                return Some(lv.varname.as_bytes());
325            }
326        }
327    }
328    None
329}
330
331// ── Private helpers (Rust-only) ───────────────────────────────────────────────
332
333/// Returns `true` if this thread is already registered in `global.twups`.
334///
335/// Always returns `true`. This module never inserts into `GlobalState.twups`
336/// (see `new_open_upval`); the check exists to satisfy the invariant
337/// `state_in_twups || openupval.is_empty()` asserted by `find_upval`.
338fn state_in_twups(state: &LuaState) -> bool {
339    let _ = state;
340    true
341}
342
343// ── LuaState methods this module depends on ──────────────────────────────────
344
345/// `LuaState` methods this module depends on, implemented here by delegating
346/// to their home modules (do_.rs, debug.rs).
347impl LuaState {
348    /// Returns the `LuaValue` at stack index `idx`.
349    pub(crate) fn get_stack_value(&self, idx: StackIdx) -> &LuaValue {
350        &self.stack[idx.0 as usize].val
351    }
352
353    /// Returns the current CallInfo (active call frame).
354    ///
355    pub(crate) fn current_ci(&self) -> &crate::state::CallInfo {
356        &self.call_info[self.ci.0 as usize]
357    }
358
359    /// Looks up the `__close` (or other) metamethod for a value.
360    ///
361    pub(crate) fn get_tm_by_obj(
362        &mut self,
363        val: &LuaValue,
364        tm: lua_types::tagmethod::TagMethod,
365    ) -> LuaValue {
366        let mt: Option<GcRef<lua_types::value::LuaTable>> = match val {
367            LuaValue::Table(t) => t.metatable(),
368            LuaValue::UserData(u) => u.metatable(),
369            other => {
370                let type_idx = other.base_type() as usize;
371                self.global().mt[type_idx].clone()
372            }
373        };
374        match mt {
375            Some(mt_ref) => {
376                let ename = self.global().tmname[tm as usize].clone();
377                mt_ref.get_short_str(&ename)
378            }
379            None => LuaValue::Nil,
380        }
381    }
382
383    /// Calls a Lua or C function (yieldable).
384    ///
385    pub(crate) fn lua_call(&mut self, top: StackIdx, nresults: i32) -> Result<(), LuaError> {
386        crate::do_::call(self, top, nresults)
387    }
388
389    /// Calls a Lua or C function (non-yieldable).
390    ///
391    pub(crate) fn lua_callnoyield(&mut self, top: StackIdx, nresults: i32) -> Result<(), LuaError> {
392        crate::do_::callnoyield(self, top, nresults)
393    }
394
395    /// Sets the error object at a given stack index for a given status code.
396    ///
397    pub(crate) fn set_error_obj(&mut self, status: i32, idx: StackIdx) -> Result<(), LuaError> {
398        let s = lua_types::status::LuaStatus::from_raw(status);
399        crate::do_::set_error_obj(self, s, idx);
400        Ok(())
401    }
402
403    /// Returns the local-variable name at frame position `n` for CallInfo `ci`.
404    ///
405    pub(crate) fn debug_find_local(&self, ci: CallInfoIdx, n: i32) -> Option<Vec<u8>> {
406        crate::debug::find_local(self, ci, n, None)
407    }
408}