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