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// ──────────────────────────────────────────────────────────────────────────────