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}