Skip to main content

lua_types/
upval.rs

1//! `UpVal` — closure upvalues. PORT_STRATEGY §3.8.
2
3use crate::value::LuaValue;
4use crate::StackIdx;
5use std::cell::{Cell, Ref, RefCell};
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/// kept for existing `slot()` callers that need the open/closed shape.
14#[derive(Debug, Clone)]
15pub enum UpValState {
16    Open { thread_id: usize, idx: StackIdx },
17    Closed(LuaValue),
18}
19
20/// A closure upvalue. Open upvalues point at a slot on a thread's stack
21/// (referred to by index, since the stack reallocates). Closed upvalues
22/// own the value.
23///
24/// Canonical state lives in two `Cell` fields (the tag and the open payload)
25/// plus a `Cell<LuaValue>` holding the closed payload. The `state`
26/// `RefCell<UpValState>` mirror is kept for cold consumers that still call
27/// `slot()` to inspect the open/closed shape. Scalar-to-scalar closed writes
28/// may leave the mirror's scalar payload stale; callers that need the current
29/// closed value must use `closed_value` / `try_closed_value`. The split lets
30/// `state.rs::upvalue_get` / `upvalue_set` short-circuit the Open path with
31/// zero `RefCell` borrow overhead, which is the dominant cost in
32/// fibonacci-class recursion benchmarks.
33#[derive(Debug)]
34pub struct UpVal {
35    open_thread_id: Cell<i64>,
36    open_idx: Cell<u32>,
37    closed_value: Cell<LuaValue>,
38    pub state: RefCell<UpValState>,
39}
40
41/// Sentinel placed in `open_thread_id` once the upvalue has been closed.
42/// Valid thread ids are non-negative (the main thread is id 0), so -1 is
43/// unambiguous.
44const CLOSED_TAG: i64 = -1;
45
46impl UpVal {
47    pub fn open(thread_id: usize, idx: StackIdx) -> Self {
48        UpVal {
49            open_thread_id: Cell::new(thread_id as i64),
50            open_idx: Cell::new(idx.0),
51            closed_value: Cell::new(LuaValue::Nil),
52            state: RefCell::new(UpValState::Open { thread_id, idx }),
53        }
54    }
55
56    pub fn closed(v: LuaValue) -> Self {
57        UpVal {
58            open_thread_id: Cell::new(CLOSED_TAG),
59            open_idx: Cell::new(0),
60            closed_value: Cell::new(v),
61            state: RefCell::new(UpValState::Closed(v)),
62        }
63    }
64
65    /// Backwards-compat handle on the full `UpValState`. Out-of-crate code
66    /// matches against this through `Ref::deref`. Hot-path callers should
67    /// use `try_open_payload` / `closed_value` instead.
68    pub fn slot(&self) -> Ref<'_, UpValState> {
69        self.state.borrow()
70    }
71
72    pub fn is_open(&self) -> bool {
73        self.open_thread_id.get() >= 0
74    }
75    pub fn is_closed(&self) -> bool {
76        self.open_thread_id.get() < 0
77    }
78
79    /// Zero-`RefCell` fast path used by `upvalue_get` / `upvalue_set`.
80    /// Returns `Some((thread_id, idx))` when the upvalue is still open,
81    /// `None` otherwise.
82    #[inline(always)]
83    pub fn try_open_payload(&self) -> Option<(usize, StackIdx)> {
84        let tid = self.open_thread_id.get();
85        if tid < 0 {
86            None
87        } else {
88            Some((tid as usize, StackIdx(self.open_idx.get())))
89        }
90    }
91
92    /// Returns the closed-side value. Callers must have confirmed the
93    /// upvalue is closed (`try_open_payload` returned `None`).
94    #[inline(always)]
95    pub fn closed_value(&self) -> LuaValue {
96        self.closed_value.get()
97    }
98
99    pub fn close_with(&self, v: LuaValue) {
100        self.open_thread_id.set(CLOSED_TAG);
101        self.open_idx.set(0);
102        self.closed_value.set(v);
103        *self.state.borrow_mut() = UpValState::Closed(v);
104    }
105
106    pub fn set_closed_value(&self, v: LuaValue) {
107        self.open_thread_id.set(CLOSED_TAG);
108        self.open_idx.set(0);
109        let old_collectable = self.closed_value.get().is_collectable();
110        self.closed_value.set(v);
111        if old_collectable || v.is_collectable() {
112            *self.state.borrow_mut() = UpValState::Closed(v);
113        }
114    }
115
116    pub fn try_closed_value(&self) -> Option<LuaValue> {
117        if self.is_closed() {
118            Some(self.closed_value.get())
119        } else {
120            None
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn closed_scalar_write_updates_canonical_value() {
131        let uv = UpVal::closed(LuaValue::Int(1));
132
133        uv.set_closed_value(LuaValue::Int(2));
134
135        assert_eq!(uv.closed_value(), LuaValue::Int(2));
136        assert_eq!(uv.try_closed_value(), Some(LuaValue::Int(2)));
137        match &*uv.slot() {
138            UpValState::Closed(v) => assert_eq!(*v, LuaValue::Int(1)),
139            UpValState::Open { .. } => panic!("closed upvalue mirror became open"),
140        };
141    }
142
143    #[test]
144    fn close_with_refreshes_legacy_mirror() {
145        let uv = UpVal::open(7, StackIdx(3));
146
147        uv.close_with(LuaValue::Bool(true));
148
149        assert_eq!(uv.closed_value(), LuaValue::Bool(true));
150        match &*uv.slot() {
151            UpValState::Closed(v) => assert_eq!(*v, LuaValue::Bool(true)),
152            UpValState::Open { .. } => panic!("closed upvalue mirror stayed open"),
153        };
154    }
155}
156
157// ──────────────────────────────────────────────────────────────────────────────
158// PORT STATUS
159//   source:        src/lfunc.h, src/lfunc.c (UpVal struct)
160//   target_crate:  lua-types
161//   confidence:    high
162//   todos:         0
163//   port_notes:    0
164//   unsafe_blocks: 0
165//   notes:         UpVal + UpValState (Open/Closed). C uses a TValue* that switches
166//                  between stack-pointing (open) and embedded (closed) via union; we
167//                  use an enum with the equivalent two states. Canonical storage is
168//                  Cell-tagged (open_thread_id, open_idx, closed_value) so hot-path
169//                  upvalue_get/_set skip RefCell borrow guards on the Open branch.
170//                  The RefCell<UpValState> mirror is retained for existing
171//                  out-of-crate slot() consumers (api.rs, debug.rs, coro_lib.rs,
172//                  func.rs). Scalar closed-value writes update the canonical payload
173//                  without refreshing the mirror payload, so value reads should use
174//                  closed_value()/try_closed_value().
175// ──────────────────────────────────────────────────────────────────────────────