Skip to main content

lua_types/
upval.rs

1//! `UpVal` — closure upvalues. PORT_STRATEGY §3.8.
2
3use std::cell::{Cell, Ref, RefCell};
4use crate::StackIdx;
5use crate::value::LuaValue;
6
7/// Discriminator state for an upvalue: either still pointing at a thread's
8/// stack slot, or owning the value after close.
9///
10/// Retained as the public read-side enum for out-of-crate consumers that
11/// pattern-match through `UpVal::slot()`. The canonical storage on `UpVal`
12/// is now a `Cell`-tagged shape; the `RefCell<UpValState>` mirror is
13/// updated in lockstep so existing `slot()` callers keep working.
14#[derive(Debug, Clone)]
15pub enum UpValState {
16    Open {
17        thread_id: usize,
18        idx: StackIdx,
19    },
20    Closed(LuaValue),
21}
22
23/// A closure upvalue. Open upvalues point at a slot on a thread's stack
24/// (referred to by index, since the stack reallocates). Closed upvalues
25/// own the value.
26///
27/// Canonical state lives in two `Cell` fields (the tag and the open payload)
28/// plus a `RefCell<LuaValue>` holding the closed payload. The `state`
29/// `RefCell<UpValState>` mirror is kept consistent so cold consumers that
30/// still call `slot()` see the correct view. The split lets
31/// `state.rs::upvalue_get` / `upvalue_set` short-circuit the Open path with
32/// zero `RefCell` borrow overhead, which is the dominant cost in
33/// fibonacci-class recursion benchmarks.
34#[derive(Debug)]
35pub struct UpVal {
36    open_thread_id: Cell<i64>,
37    open_idx: Cell<u32>,
38    closed_value: RefCell<LuaValue>,
39    pub state: RefCell<UpValState>,
40}
41
42/// Sentinel placed in `open_thread_id` once the upvalue has been closed.
43/// Valid thread ids are non-negative (the main thread is id 0), so -1 is
44/// unambiguous.
45const CLOSED_TAG: i64 = -1;
46
47impl UpVal {
48    pub fn open(thread_id: usize, idx: StackIdx) -> Self {
49        UpVal {
50            open_thread_id: Cell::new(thread_id as i64),
51            open_idx: Cell::new(idx.0),
52            closed_value: RefCell::new(LuaValue::Nil),
53            state: RefCell::new(UpValState::Open { thread_id, idx }),
54        }
55    }
56
57    pub fn closed(v: LuaValue) -> Self {
58        UpVal {
59            open_thread_id: Cell::new(CLOSED_TAG),
60            open_idx: Cell::new(0),
61            closed_value: RefCell::new(v.clone()),
62            state: RefCell::new(UpValState::Closed(v)),
63        }
64    }
65
66    /// Backwards-compat handle on the full `UpValState`. Out-of-crate code
67    /// matches against this through `Ref::deref`. Hot-path callers should
68    /// use `try_open_payload` / `closed_value` instead.
69    pub fn slot(&self) -> Ref<'_, UpValState> { self.state.borrow() }
70
71    pub fn is_open(&self) -> bool { self.open_thread_id.get() >= 0 }
72    pub fn is_closed(&self) -> bool { self.open_thread_id.get() < 0 }
73
74    /// Zero-`RefCell` fast path used by `upvalue_get` / `upvalue_set`.
75    /// Returns `Some((thread_id, idx))` when the upvalue is still open,
76    /// `None` otherwise.
77    #[inline(always)]
78    pub fn try_open_payload(&self) -> Option<(usize, StackIdx)> {
79        let tid = self.open_thread_id.get();
80        if tid < 0 {
81            None
82        } else {
83            Some((tid as usize, StackIdx(self.open_idx.get())))
84        }
85    }
86
87    /// Borrows the closed-side value. Callers must have confirmed the
88    /// upvalue is closed (`try_open_payload` returned `None`).
89    #[inline(always)]
90    pub fn closed_value(&self) -> Ref<'_, LuaValue> { self.closed_value.borrow() }
91
92    pub fn close_with(&self, v: LuaValue) {
93        self.open_thread_id.set(CLOSED_TAG);
94        self.open_idx.set(0);
95        *self.closed_value.borrow_mut() = v.clone();
96        *self.state.borrow_mut() = UpValState::Closed(v);
97    }
98
99    pub fn set_closed_value(&self, v: LuaValue) {
100        self.open_thread_id.set(CLOSED_TAG);
101        self.open_idx.set(0);
102        *self.closed_value.borrow_mut() = v.clone();
103        *self.state.borrow_mut() = UpValState::Closed(v);
104    }
105
106    pub fn try_closed_value(&self) -> Option<std::cell::Ref<'_, LuaValue>> {
107        if self.is_closed() {
108            self.closed_value.try_borrow().ok()
109        } else {
110            None
111        }
112    }
113}
114
115// ──────────────────────────────────────────────────────────────────────────────
116// PORT STATUS
117//   source:        src/lfunc.h, src/lfunc.c (UpVal struct)
118//   target_crate:  lua-types
119//   confidence:    high
120//   todos:         0
121//   port_notes:    0
122//   unsafe_blocks: 0
123//   notes:         UpVal + UpValState (Open/Closed). C uses a TValue* that switches
124//                  between stack-pointing (open) and embedded (closed) via union; we
125//                  use an enum with the equivalent two states. Canonical storage is
126//                  Cell-tagged (open_thread_id, open_idx, closed_value) so hot-path
127//                  upvalue_get/_set skip RefCell borrow guards on the Open branch.
128//                  The RefCell<UpValState> mirror is updated in lockstep so existing
129//                  out-of-crate slot() consumers (api.rs, debug.rs, coro_lib.rs,
130//                  func.rs) keep working without migration.
131// ──────────────────────────────────────────────────────────────────────────────