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}