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