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