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