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;
6
7/// A closure upvalue. Open upvalues point at a slot on a thread's stack
8/// (referred to by index, since the stack reallocates). Closed upvalues
9/// own the value.
10///
11/// State lives entirely in three `Cell` fields and is single-source-of-truth.
12/// `open_thread_id` doubles as the open/closed discriminant: a non-negative
13/// value is the owning thread id of an open upvalue; the [`CLOSED_TAG`]
14/// sentinel (`-1`) means the upvalue is closed and its payload is in
15/// `closed_value`. Valid thread ids are non-negative (the main thread is id 0),
16/// so the sentinel is unambiguous. `open_idx` is the stack slot of an open
17/// upvalue. Closing is terminal — there is no re-open path — so a `CLOSED_TAG`
18/// tag never reverts.
19///
20/// Read the open shape with [`try_open_payload`](UpVal::try_open_payload)
21/// (`None` once closed) and the closed payload with
22/// [`closed_value`](UpVal::closed_value) / [`try_closed_value`](UpVal::try_closed_value).
23/// The all-`Cell` layout lets `state.rs::upvalue_get` / `upvalue_set`
24/// short-circuit the Open path with zero borrow-guard overhead, which is the
25/// dominant cost in fibonacci-class recursion benchmarks.
26#[derive(Debug)]
27pub struct UpVal {
28    open_thread_id: Cell<i64>,
29    open_idx: Cell<u32>,
30    closed_value: Cell<LuaValue>,
31}
32
33/// Sentinel placed in `open_thread_id` once the upvalue has been closed.
34/// Valid thread ids are non-negative (the main thread is id 0), so -1 is
35/// unambiguous.
36const CLOSED_TAG: i64 = -1;
37
38impl UpVal {
39    pub fn open(thread_id: usize, idx: StackIdx) -> Self {
40        UpVal {
41            open_thread_id: Cell::new(thread_id as i64),
42            open_idx: Cell::new(idx.0),
43            closed_value: Cell::new(LuaValue::Nil),
44        }
45    }
46
47    pub fn closed(v: LuaValue) -> Self {
48        UpVal {
49            open_thread_id: Cell::new(CLOSED_TAG),
50            open_idx: Cell::new(0),
51            closed_value: Cell::new(v),
52        }
53    }
54
55    pub fn is_open(&self) -> bool {
56        self.open_thread_id.get() >= 0
57    }
58    pub fn is_closed(&self) -> bool {
59        self.open_thread_id.get() < 0
60    }
61
62    /// Zero-overhead read of the open shape used by `upvalue_get` /
63    /// `upvalue_set` and every out-of-crate consumer that inspects an open
64    /// upvalue's `(thread_id, idx)`. Returns `Some((thread_id, idx))` when the
65    /// upvalue is still open, `None` once it has been closed.
66    #[inline(always)]
67    pub fn try_open_payload(&self) -> Option<(usize, StackIdx)> {
68        let tid = self.open_thread_id.get();
69        if tid < 0 {
70            None
71        } else {
72            Some((tid as usize, StackIdx(self.open_idx.get())))
73        }
74    }
75
76    /// Returns the closed-side value. Callers must have confirmed the
77    /// upvalue is closed (`try_open_payload` returned `None`).
78    #[inline(always)]
79    pub fn closed_value(&self) -> LuaValue {
80        self.closed_value.get()
81    }
82
83    pub fn close_with(&self, v: LuaValue) {
84        self.open_thread_id.set(CLOSED_TAG);
85        self.open_idx.set(0);
86        self.closed_value.set(v);
87    }
88
89    pub fn set_closed_value(&self, v: LuaValue) {
90        self.open_thread_id.set(CLOSED_TAG);
91        self.open_idx.set(0);
92        self.closed_value.set(v);
93    }
94
95    pub fn try_closed_value(&self) -> Option<LuaValue> {
96        if self.is_closed() {
97            Some(self.closed_value.get())
98        } else {
99            None
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn closed_scalar_write_updates_canonical_value() {
110        let uv = UpVal::closed(LuaValue::Int(1));
111
112        uv.set_closed_value(LuaValue::Int(2));
113
114        assert_eq!(uv.closed_value(), LuaValue::Int(2));
115        assert_eq!(uv.try_closed_value(), Some(LuaValue::Int(2)));
116        assert!(uv.is_closed());
117        assert_eq!(uv.try_open_payload(), None);
118    }
119
120    #[test]
121    fn close_with_sets_cell_closed_state() {
122        let uv = UpVal::open(7, StackIdx(3));
123        assert_eq!(uv.try_open_payload(), Some((7, StackIdx(3))));
124
125        uv.close_with(LuaValue::Bool(true));
126
127        assert_eq!(uv.closed_value(), LuaValue::Bool(true));
128        assert_eq!(uv.try_closed_value(), Some(LuaValue::Bool(true)));
129        assert!(uv.is_closed());
130        assert_eq!(uv.try_open_payload(), None);
131    }
132}
133
134// ──────────────────────────────────────────────────────────────────────────────
135// PORT STATUS
136//   source:        src/lfunc.h, src/lfunc.c (UpVal struct)
137//   target_crate:  lua-types
138//   confidence:    high
139//   todos:         0
140//   port_notes:    0
141//   unsafe_blocks: 0
142//   notes:         UpVal with Open/Closed state. C uses a TValue* that switches
143//                  between stack-pointing (open) and embedded (closed) via union; we
144//                  use three Cell fields as the single source of truth. open_thread_id
145//                  is the open/closed discriminant (CLOSED_TAG = -1 means closed),
146//                  open_idx is the open stack slot, closed_value holds the closed
147//                  payload. Hot-path upvalue_get/_set read the Cells directly with no
148//                  RefCell borrow guards. Read open shape via try_open_payload(),
149//                  closed payload via closed_value()/try_closed_value().
150// ──────────────────────────────────────────────────────────────────────────────