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/// Open/closed state of an [`UpVal`], stored in a single [`Cell`].
8///
9/// The open and closed states are mutually exclusive, so overlapping their
10/// storage in one enum keeps the payload at 24 bytes — `GcBox<UpVal>` = 48,
11/// one libmalloc size-class below the 56-byte three-`Cell` layout it replaces
12/// (#113 candidate 1: the 56→48 crossing drops the box from the 64-byte class
13/// into the 48-byte class). `Open` carries the owning thread id as a full
14/// `u64`; thread ids come from a globally monotonic, never-reused `u64`
15/// counter, so the domain must not be narrowed. `idx` is the stack slot the
16/// open upvalue refers to (the stack reallocates, so it is held by index).
17/// `Closed` owns the value.
18#[derive(Debug, Clone, Copy)]
19enum UpValState {
20 Open { thread_id: u64, idx: u32 },
21 Closed(LuaValue),
22}
23
24/// A closure upvalue. Open upvalues point at a slot on a thread's stack
25/// (referred to by index, since the stack reallocates). Closed upvalues
26/// own the value.
27///
28/// State lives entirely in one [`Cell<UpValState>`] and is
29/// single-source-of-truth. Closing is terminal — there is no re-open path — so
30/// the state never reverts from `Closed` back to `Open`.
31///
32/// Read the open shape with [`try_open_payload`](UpVal::try_open_payload)
33/// (`None` once closed) and the closed payload with
34/// [`closed_value`](UpVal::closed_value) / [`try_closed_value`](UpVal::try_closed_value).
35/// The `Cell` layout lets `state.rs::upvalue_get` / `upvalue_set` short-circuit
36/// the Open path with zero borrow-guard overhead, which is the dominant cost in
37/// fibonacci-class recursion benchmarks.
38#[derive(Debug)]
39pub struct UpVal {
40 state: Cell<UpValState>,
41}
42
43/// `UpVal` is a GC-boxed hot object; every byte multiplies across the live
44/// upvalue population (≈100k on closure_ops). The tagged-`Cell` layout keeps
45/// the payload at 24 bytes so `GcBox<UpVal>` is 48 bytes — one libmalloc class
46/// below the previous 56. Gated to 64-bit because the byte count is a
47/// pointer-width claim (the wasm32 build has a 32-bit layout).
48#[cfg(target_pointer_width = "64")]
49const _: () = assert!(std::mem::size_of::<UpVal>() == 24);
50#[cfg(target_pointer_width = "64")]
51const _: () = assert!(std::mem::size_of::<lua_gc::GcBox<UpVal>>() == 48);
52
53impl UpVal {
54 pub fn open(thread_id: u64, idx: StackIdx) -> Self {
55 UpVal {
56 state: Cell::new(UpValState::Open {
57 thread_id,
58 idx: idx.0,
59 }),
60 }
61 }
62
63 pub fn closed(v: LuaValue) -> Self {
64 UpVal {
65 state: Cell::new(UpValState::Closed(v)),
66 }
67 }
68
69 pub fn is_open(&self) -> bool {
70 matches!(self.state.get(), UpValState::Open { .. })
71 }
72 pub fn is_closed(&self) -> bool {
73 matches!(self.state.get(), UpValState::Closed(_))
74 }
75
76 /// Zero-overhead read of the open shape used by `upvalue_get` /
77 /// `upvalue_set` and every out-of-crate consumer that inspects an open
78 /// upvalue's `(thread_id, idx)`. Returns `Some((thread_id, idx))` when the
79 /// upvalue is still open, `None` once it has been closed. `thread_id` is a
80 /// `u64` end-to-end so the never-reused monotonic id domain is preserved on
81 /// every target, including 32-bit `usize` ones (wasm32).
82 #[inline(always)]
83 pub fn try_open_payload(&self) -> Option<(u64, StackIdx)> {
84 match self.state.get() {
85 UpValState::Open { thread_id, idx } => Some((thread_id, StackIdx(idx))),
86 UpValState::Closed(_) => None,
87 }
88 }
89
90 /// Returns the closed-side value. Callers must have confirmed the
91 /// upvalue is closed (`try_open_payload` returned `None`); an open upvalue
92 /// reports [`LuaValue::Nil`], matching the value its closed slot held under
93 /// the previous layout.
94 #[inline(always)]
95 pub fn closed_value(&self) -> LuaValue {
96 match self.state.get() {
97 UpValState::Closed(v) => v,
98 UpValState::Open { .. } => LuaValue::Nil,
99 }
100 }
101
102 pub fn close_with(&self, v: LuaValue) {
103 self.state.set(UpValState::Closed(v));
104 }
105
106 pub fn set_closed_value(&self, v: LuaValue) {
107 self.state.set(UpValState::Closed(v));
108 }
109
110 pub fn try_closed_value(&self) -> Option<LuaValue> {
111 match self.state.get() {
112 UpValState::Closed(v) => Some(v),
113 UpValState::Open { .. } => None,
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn closed_scalar_write_updates_canonical_value() {
124 let uv = UpVal::closed(LuaValue::Int(1));
125
126 uv.set_closed_value(LuaValue::Int(2));
127
128 assert_eq!(uv.closed_value(), LuaValue::Int(2));
129 assert_eq!(uv.try_closed_value(), Some(LuaValue::Int(2)));
130 assert!(uv.is_closed());
131 assert_eq!(uv.try_open_payload(), None);
132 }
133
134 #[test]
135 fn close_with_sets_cell_closed_state() {
136 let uv = UpVal::open(7, StackIdx(3));
137 assert_eq!(uv.try_open_payload(), Some((7, StackIdx(3))));
138
139 uv.close_with(LuaValue::Bool(true));
140
141 assert_eq!(uv.closed_value(), LuaValue::Bool(true));
142 assert_eq!(uv.try_closed_value(), Some(LuaValue::Bool(true)));
143 assert!(uv.is_closed());
144 assert_eq!(uv.try_open_payload(), None);
145 }
146
147 /// A thread id above `i64::MAX` (and far above `u32::MAX`) must round-trip
148 /// exactly — thread ids come from a never-reused monotonic `u64` counter.
149 /// It would read back negative under an `i64` discriminant and truncate
150 /// under a `u32`/32-bit-`usize` id, so this guards the full domain
151 /// end-to-end.
152 #[test]
153 fn open_upvalue_preserves_full_u64_thread_id() {
154 const BIG_TID: u64 = 0xFEDC_BA98_7654_3210;
155 assert!(BIG_TID > i64::MAX as u64);
156 let uv = UpVal::open(BIG_TID, StackIdx(9));
157 assert_eq!(uv.try_open_payload(), Some((BIG_TID, StackIdx(9))));
158 assert!(uv.is_open());
159 assert_eq!(uv.try_closed_value(), None);
160 }
161
162 /// `closed_value()` on an open upvalue reports `Nil`, matching the value
163 /// its closed slot held under the previous three-`Cell` layout (the
164 /// byte-identical edge preserved by the tagged-`Cell` representation).
165 #[test]
166 fn open_upvalue_closed_value_reports_nil() {
167 let uv = UpVal::open(3, StackIdx(1));
168 assert!(uv.is_open());
169 assert_eq!(uv.closed_value(), LuaValue::Nil);
170 }
171}