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