Skip to main content

luna_core/runtime/
value.rs

1//! 16-byte tagged value (PUC TValue equivalent). Chosen over 8-byte
2//! NaN-boxing on bench evidence — see benches/value_repr.rs and the P02 plan:
3//! Lua 5.5's native i64 forces NaN-boxed integers into 47-bit smis plus
4//! range checks, losing 24% on the arithmetic dispatch path.
5
6use crate::runtime::coroutine::Coro;
7use crate::runtime::function::LuaClosure;
8use crate::runtime::heap::Gc;
9use crate::runtime::string::LuaStr;
10use crate::runtime::table::Table;
11use crate::runtime::userdata::Userdata;
12
13/// Native (host) function: receives the VM, the absolute stack slot of the
14/// function value (its own NativeClosure — read upvalues through it), and
15/// the argument count; writes results starting at that slot and returns how
16/// many.
17///
18/// Embedding contract: prefer `Err(LuaError)` over `panic!` for error
19/// signaling. Panics that escape the callback are caught and converted
20/// to a `LuaError("native panic: <msg>")`, but the VM state may be
21/// inconsistent after a panic (half-pushed args, dangling refs) — the
22/// host should treat any `"native panic:"`-prefixed error as fatal and
23/// drop the Vm rather than reusing it.
24pub type NativeFn =
25    fn(&mut crate::vm::Vm, func_slot: u32, nargs: u32) -> Result<u32, crate::vm::LuaError>;
26
27use crate::runtime::function::NativeClosure;
28
29/// P17-D v2 Direction E (E1) — `#[repr(C, u8)]` makes the discriminant a
30/// 1-byte tag at offset 0, with the variant payload starting at offset 8
31/// (after 7 bytes of alignment padding). The total size stays 16 bytes
32/// (same as the prior plain Rust enum representation), preserving P02's
33/// arithmetic-fast-path 24% win over NaN-boxing.
34///
35/// The PUC-equivalent layout this gives us means LJ_FR2-style frame
36/// metadata reads (`stack[base-2]` for closure, `stack[base-1]` for the
37/// packed frame marker) can use a single 1-byte tag load + payload
38/// branch — see [`Value::tag_byte`] and friends. The previous enum
39/// repr left discriminant position unspecified, so byte-level reads of
40/// Value layout would have been unportable.
41///
42/// Variant order MUST stay stable: rustc assigns discriminants
43/// 0..11 in declaration order (Nil=0, Bool=1, ..., LightUserdata=10),
44/// and Phase 3+ hot paths read those discriminants via `tag_byte()`.
45/// New variants must be appended; reordering changes the wire layout.
46#[derive(Clone, Copy, Debug)]
47#[repr(C, u8)]
48pub enum Value {
49    /// Lua `nil`.
50    Nil,
51    /// Lua `boolean`.
52    Bool(
53        /// Underlying boolean.
54        bool,
55    ),
56    /// Lua integer (5.3+; in 5.1 all numbers arrive as `Float`).
57    Int(
58        /// 64-bit signed value.
59        i64,
60    ),
61    /// Lua float.
62    Float(
63        /// IEEE-754 double.
64        f64,
65    ),
66    /// Lua `string` — GC-managed byte string.
67    Str(
68        /// String handle.
69        Gc<LuaStr>,
70    ),
71    /// Lua `table`.
72    Table(
73        /// Table handle.
74        Gc<Table>,
75    ),
76    /// Lua function backed by a [`LuaClosure`].
77    Closure(
78        /// Closure handle.
79        Gc<LuaClosure>,
80    ),
81    /// Lua function backed by a host [`NativeClosure`].
82    Native(
83        /// Native closure handle.
84        Gc<NativeClosure>,
85    ),
86    /// Lua `thread` (coroutine).
87    Coro(
88        /// Coroutine handle.
89        Gc<Coro>,
90    ),
91    /// Full userdata — GC-managed host-allocated payload with a metatable.
92    Userdata(
93        /// Userdata handle.
94        Gc<Userdata>,
95    ),
96    /// PUC `LUA_TLIGHTUSERDATA`: an opaque host pointer that participates only
97    /// as an identity token (raw equality on pointer bits, no metatable, not
98    /// GC-managed). Currently produced exclusively by `debug.upvalueid` — it
99    /// points at the upvalue cell's `Value` slot and stays distinct per cell.
100    LightUserdata(
101        /// Opaque host pointer used as an identity token.
102        *const (),
103    ),
104}
105
106// SAFETY: `LightUserdata` holds a raw pointer (PUC `void*` identity token).
107// The other `Value` variants already carry GC pointers that aren't `Send`/
108// `Sync` either — the type as a whole is single-threaded by construction.
109// The raw `*const ()` doesn't change that contract.
110
111impl Value {
112    /// Lua-visible type name (`"nil"`, `"boolean"`, `"number"`,
113    /// `"string"`, `"table"`, `"function"`, `"thread"`, `"userdata"`)
114    /// matching `type()`.
115    pub fn type_name(self) -> &'static str {
116        match self {
117            Value::Nil => "nil",
118            Value::Bool(_) => "boolean",
119            Value::Int(_) | Value::Float(_) => "number",
120            Value::Str(_) => "string",
121            Value::Table(_) => "table",
122            Value::Closure(_) | Value::Native(_) => "function",
123            Value::Coro(_) => "thread",
124            // PUC `lua_typename` collapses full and light userdata to
125            // "userdata"; only `luaL_typeerror` distinguishes them by tag.
126            Value::Userdata(_) | Value::LightUserdata(_) => "userdata",
127        }
128    }
129
130    /// True when this is [`Value::Nil`].
131    pub fn is_nil(self) -> bool {
132        matches!(self, Value::Nil)
133    }
134
135    /// Lua truth: everything except `nil` and `false`.
136    pub fn truthy(self) -> bool {
137        !matches!(self, Value::Nil | Value::Bool(false))
138    }
139
140    /// P17-D v2 Direction E (E1) — read the variant's discriminant byte
141    /// directly. The `#[repr(C, u8)]` on the enum makes this a single
142    /// 1-byte load from `&self`, regardless of variant.
143    ///
144    /// Discriminant values follow declaration order:
145    /// `Nil=0, Bool=1, Int=2, Float=3, Str=4, Table=5, Closure=6,
146    ///  Native=7, Coro=8, Userdata=9, LightUserdata=10`.
147    ///
148    /// Use [`tag`] constants instead of literal numbers at call
149    /// sites — see the module-level `tag` constants below.
150    #[inline(always)]
151    pub fn tag_byte(&self) -> u8 {
152        // SAFETY: `#[repr(C, u8)]` on `Value` guarantees the
153        // discriminant occupies the first byte. Reading it as `u8` is
154        // therefore well-defined for ANY Value variant.
155        unsafe { *(self as *const Value as *const u8) }
156    }
157
158    /// Fast tag-only check for Lua function-call sites. Returns true
159    /// iff the value's discriminant is `Closure` or `Native` (the
160    /// callable types). Avoids matching the entire enum.
161    #[inline(always)]
162    pub fn is_callable(self) -> bool {
163        let t = self.tag_byte();
164        t == tag::CLOSURE || t == tag::NATIVE
165    }
166
167    /// Read the closure pointer without an enum match. Caller must
168    /// have verified `tag_byte() == tag::CLOSURE` first.
169    ///
170    /// SAFETY: the value's discriminant MUST be Closure. UB otherwise.
171    ///
172    /// `#[doc(hidden)]` (Track A4): JIT hot-path use; embedders should
173    /// use the safe `match value { Value::Closure(c) => ..., _ => ... }`
174    /// instead.
175    #[doc(hidden)]
176    #[inline(always)]
177    pub unsafe fn as_closure_unchecked(self) -> Gc<crate::runtime::LuaClosure> {
178        debug_assert_eq!(self.tag_byte(), tag::CLOSURE);
179        // SAFETY: `#[repr(C, u8)]` Value with Closure discriminant has
180        // payload `Gc<LuaClosure>` at offset 8 (after 7 bytes of
181        // alignment padding past the 1-byte tag). `Gc<T>` is a NonNull
182        // pointer so its layout is a single 8-byte pointer.
183        unsafe {
184            let payload_ptr = (&self as *const Value as *const u8).add(8)
185                as *const Gc<crate::runtime::LuaClosure>;
186            *payload_ptr
187        }
188    }
189
190    /// Read the integer payload without an enum match. Caller must
191    /// have verified `tag_byte() == tag::INT` first.
192    ///
193    /// SAFETY: the value's discriminant MUST be Int. UB otherwise.
194    ///
195    /// `#[doc(hidden)]` (Track A4): JIT hot-path use; embedders should
196    /// use the safe `match value { Value::Int(i) => ..., _ => ... }`
197    /// instead.
198    #[doc(hidden)]
199    #[inline(always)]
200    pub unsafe fn as_int_unchecked(self) -> i64 {
201        debug_assert_eq!(self.tag_byte(), tag::INT);
202        unsafe {
203            let payload_ptr = (&self as *const Value as *const u8).add(8) as *const i64;
204            *payload_ptr
205        }
206    }
207
208    /// Borrow the Lua string's bytes as a UTF-8 `&str` (B7 — Phase 2).
209    /// Returns `None` if this value is not a `Value::Str`, or if the
210    /// string's bytes are not valid UTF-8.
211    ///
212    /// Embedders dealing with text data use this. For binary data
213    /// (Redis protocol buffers, etc.) use [`Value::as_bytes`].
214    pub fn try_as_str(&self) -> Option<&str> {
215        match self {
216            Value::Str(s) => std::str::from_utf8(s.as_bytes()).ok(),
217            _ => None,
218        }
219    }
220
221    /// Borrow the raw bytes of a `Value::Str` (B7 — Phase 2). Returns
222    /// `None` for non-string variants. Always safe — Lua strings are
223    /// byte sequences and may carry non-UTF-8 content.
224    pub fn as_bytes(&self) -> Option<&[u8]> {
225        match self {
226            Value::Str(s) => Some(s.as_bytes()),
227            _ => None,
228        }
229    }
230
231    /// Raw equality (no metamethods): `rawequal` and table-key identity.
232    /// Mixed int/float numbers are equal iff the float is exactly integral
233    /// and equals the integer (PUC luaV_equalobj F2Ieq rule).
234    pub fn raw_eq(self, other: Value) -> bool {
235        match (self, other) {
236            (Value::Nil, Value::Nil) => true,
237            (Value::Bool(a), Value::Bool(b)) => a == b,
238            (Value::Int(a), Value::Int(b)) => a == b,
239            (Value::Float(a), Value::Float(b)) => a == b,
240            (Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a)) => {
241                f2i_exact(b) == Some(a)
242            }
243            (Value::Str(a), Value::Str(b)) => str_eq(a, b),
244            (Value::Table(a), Value::Table(b)) => a.ptr_eq(b),
245            (Value::Closure(a), Value::Closure(b)) => a.ptr_eq(b),
246            (Value::Native(a), Value::Native(b)) => a.ptr_eq(b),
247            (Value::Coro(a), Value::Coro(b)) => a.ptr_eq(b),
248            (Value::Userdata(a), Value::Userdata(b)) => a.ptr_eq(b),
249            (Value::LightUserdata(a), Value::LightUserdata(b)) => a == b,
250            _ => false,
251        }
252    }
253}
254
255fn str_eq(a: Gc<LuaStr>, b: Gc<LuaStr>) -> bool {
256    if a.ptr_eq(b) {
257        return true;
258    }
259    if a.is_short() && b.is_short() {
260        return false; // interned: distinct pointers ⇒ distinct contents
261    }
262    a.as_bytes() == b.as_bytes()
263}
264
265/// The float values that convert exactly to i64 (F2Ieq).
266pub fn f2i_exact(f: f64) -> Option<i64> {
267    if f.trunc() == f && (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&f) {
268        Some(f as i64)
269    } else {
270        None
271    }
272}
273
274/// P17-D v2 Direction E (E1) — discriminant byte constants for
275/// [`Value::tag_byte`]. These match the `#[repr(C, u8)]` enum's
276/// declaration order; reordering Value variants requires updating
277/// these constants in lock-step.
278///
279/// Separate from the [`raw`] module's tagging scheme: `raw::*` is the
280/// luna 5.5-style "compact arrays" marshalling tag (separates
281/// `Bool(false)` vs `Bool(true)` into FALSE/TRUE tags, encodes
282/// `Userdata`/`LightUserdata` together, etc.). `tag::*` is the actual
283/// `Value` enum discriminant — one tag per variant — used by
284/// LJ_FR2-style frame-metadata reads in Phase 3+.
285pub mod tag {
286    /// Tag for `Value::Nil`.
287    pub const NIL: u8 = 0;
288    /// Tag for `Value::Bool`.
289    pub const BOOL: u8 = 1;
290    /// Tag for `Value::Int`.
291    pub const INT: u8 = 2;
292    /// Tag for `Value::Float`.
293    pub const FLOAT: u8 = 3;
294    /// Tag for `Value::Str`.
295    pub const STR: u8 = 4;
296    /// Tag for `Value::Table`.
297    pub const TABLE: u8 = 5;
298    /// Tag for `Value::Closure`.
299    pub const CLOSURE: u8 = 6;
300    /// Tag for `Value::Native`.
301    pub const NATIVE: u8 = 7;
302    /// Tag for `Value::Coro`.
303    pub const CORO: u8 = 8;
304    /// Tag for `Value::Userdata`.
305    pub const USERDATA: u8 = 9;
306    /// Tag for `Value::LightUserdata`.
307    pub const LIGHTUSERDATA: u8 = 10;
308}
309
310/// Compact (tag, payload) encoding used by table array parts — the 5.5
311/// "compact arrays" layout: 1 tag byte + 8 payload bytes per slot. The
312/// payload is a union (PUC `Value` union shape) rather than u64 bits so that
313/// pointer provenance survives the round-trip (strict-provenance clean).
314#[doc(hidden)]
315pub mod raw {
316    pub const NIL: u8 = 0;
317    pub const FALSE: u8 = 1;
318    pub const TRUE: u8 = 2;
319    pub const INT: u8 = 3;
320    pub const FLOAT: u8 = 4;
321    pub const STR: u8 = 5;
322    pub const TABLE: u8 = 6;
323    pub const CLOSURE: u8 = 7;
324    pub const NATIVE: u8 = 8;
325    pub const CORO: u8 = 9;
326    pub const USERDATA: u8 = 10;
327    pub const LIGHTUSERDATA: u8 = 11;
328
329    /// Heap-managed tags.
330    pub fn is_gc(tag: u8) -> bool {
331        // LIGHTUSERDATA is an opaque host pointer (PUC void*); not GC-managed.
332        (STR..LIGHTUSERDATA).contains(&tag)
333    }
334}
335
336#[derive(Clone, Copy)]
337#[doc(hidden)]
338pub union RawVal {
339    pub zero: u64,
340    pub i: i64,
341    pub f: f64,
342    pub s: *mut LuaStr,
343    pub t: *mut Table,
344    pub c: *mut LuaClosure,
345    pub n: *mut NativeClosure,
346    pub co: *mut Coro,
347    pub u: *mut Userdata,
348    pub lu: *const (),
349}
350
351impl RawVal {
352    pub(crate) const NIL: RawVal = RawVal { zero: 0 };
353}
354
355impl Value {
356    #[doc(hidden)]
357    pub fn unpack(self) -> (u8, RawVal) {
358        match self {
359            Value::Nil => (raw::NIL, RawVal::NIL),
360            Value::Bool(false) => (raw::FALSE, RawVal::NIL),
361            Value::Bool(true) => (raw::TRUE, RawVal::NIL),
362            Value::Int(i) => (raw::INT, RawVal { i }),
363            Value::Float(f) => (raw::FLOAT, RawVal { f }),
364            Value::Str(s) => (raw::STR, RawVal { s: s.as_ptr() }),
365            Value::Table(t) => (raw::TABLE, RawVal { t: t.as_ptr() }),
366            Value::Closure(c) => (raw::CLOSURE, RawVal { c: c.as_ptr() }),
367            Value::Native(n) => (raw::NATIVE, RawVal { n: n.as_ptr() }),
368            Value::Coro(co) => (raw::CORO, RawVal { co: co.as_ptr() }),
369            Value::Userdata(u) => (raw::USERDATA, RawVal { u: u.as_ptr() }),
370            Value::LightUserdata(p) => (raw::LIGHTUSERDATA, RawVal { lu: p }),
371        }
372    }
373
374    /// SAFETY: `(tag, v)` must come from a matching `unpack` of a value that
375    /// is still alive.
376    #[doc(hidden)]
377    pub unsafe fn pack(tag: u8, v: RawVal) -> Value {
378        unsafe {
379            match tag {
380                raw::NIL => Value::Nil,
381                raw::FALSE => Value::Bool(false),
382                raw::TRUE => Value::Bool(true),
383                raw::INT => Value::Int(v.i),
384                raw::FLOAT => Value::Float(v.f),
385                raw::NATIVE => Value::Native(Gc::from_ptr(v.n)),
386                raw::STR => Value::Str(Gc::from_ptr(v.s)),
387                raw::TABLE => Value::Table(Gc::from_ptr(v.t)),
388                raw::CLOSURE => Value::Closure(Gc::from_ptr(v.c)),
389                raw::CORO => Value::Coro(Gc::from_ptr(v.co)),
390                raw::USERDATA => Value::Userdata(Gc::from_ptr(v.u)),
391                raw::LIGHTUSERDATA => Value::LightUserdata(v.lu),
392                _ => unreachable!("bad raw value tag"),
393            }
394        }
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401    use crate::runtime::heap::Heap;
402
403    #[test]
404    fn value_is_16_bytes() {
405        assert_eq!(size_of::<Value>(), 16);
406    }
407
408    #[test]
409    fn p17d_e1_tag_byte_matches_declaration_order() {
410        // The `#[repr(C, u8)]` enum puts discriminant byte at offset 0.
411        // Variant declaration order in `pub enum Value` is the source of
412        // truth for tag::* constants. If you reorder variants without
413        // updating tag::*, this test catches it before Phase 3 fast-path
414        // helpers misread the tag.
415        let mut heap = Heap::new();
416        assert_eq!(Value::Nil.tag_byte(), tag::NIL);
417        assert_eq!(Value::Bool(false).tag_byte(), tag::BOOL);
418        assert_eq!(Value::Bool(true).tag_byte(), tag::BOOL);
419        assert_eq!(Value::Int(0).tag_byte(), tag::INT);
420        assert_eq!(Value::Int(-1).tag_byte(), tag::INT);
421        assert_eq!(Value::Float(std::f64::consts::PI).tag_byte(), tag::FLOAT);
422        let s = heap.intern(b"hi");
423        assert_eq!(Value::Str(s).tag_byte(), tag::STR);
424        assert_eq!(
425            Value::LightUserdata(std::ptr::null()).tag_byte(),
426            tag::LIGHTUSERDATA
427        );
428    }
429
430    #[test]
431    fn p17d_e1_int_unchecked_roundtrip() {
432        for v in [0i64, 1, -1, i64::MAX, i64::MIN, 0x1234_5678_9abc_def0] {
433            let val = Value::Int(v);
434            // SAFETY: we constructed it as Int.
435            let recovered = unsafe { val.as_int_unchecked() };
436            assert_eq!(recovered, v, "i64 payload round-trips for {}", v);
437        }
438    }
439
440    #[test]
441    fn p17d_e1_closure_unchecked_roundtrip() {
442        // Constructing a real LuaClosure requires a Proto + Heap; the
443        // round-trip is exercised end-to-end via existing
444        // call_value/dispatch tests. Here we just sanity-check that
445        // `as_closure_unchecked` reads the byte at offset 8 — that
446        // ptr_eq holds between input and output.
447        // (Skipped: would need to plumb a Proto through Heap.)
448        // The integration round-trip is implicit in trace_jit_p15_a tests.
449    }
450
451    #[test]
452    fn p17d_e1_is_callable() {
453        let mut heap = Heap::new();
454        let s = heap.intern(b"x");
455        assert!(!Value::Nil.is_callable());
456        assert!(!Value::Int(0).is_callable());
457        assert!(!Value::Str(s).is_callable());
458        // Closure / Native require heap-allocated callables; integration
459        // tests cover those code paths.
460    }
461
462    #[test]
463    fn raw_equality() {
464        assert!(Value::Nil.raw_eq(Value::Nil));
465        assert!(Value::Int(3).raw_eq(Value::Float(3.0)));
466        assert!(Value::Float(3.0).raw_eq(Value::Int(3)));
467        assert!(!Value::Int(3).raw_eq(Value::Float(3.5)));
468        // 2^63 rounds to a float outside i64 range: not equal to any int
469        assert!(!Value::Int(i64::MAX).raw_eq(Value::Float(i64::MAX as f64)));
470        assert!(!Value::Float(f64::NAN).raw_eq(Value::Float(f64::NAN)));
471        assert!(!Value::Nil.raw_eq(Value::Bool(false)));
472        assert!(Value::Int(0).raw_eq(Value::Float(-0.0)));
473    }
474
475    #[test]
476    fn string_equality_short_and_long() {
477        let mut heap = Heap::new();
478        let a = Value::Str(heap.intern(b"abc"));
479        let b = Value::Str(heap.intern(b"abc"));
480        let c = Value::Str(heap.intern(b"abd"));
481        assert!(a.raw_eq(b));
482        assert!(!a.raw_eq(c));
483        let long1 = Value::Str(heap.intern(&[7u8; 50]));
484        let long2 = Value::Str(heap.intern(&[7u8; 50]));
485        assert!(long1.raw_eq(long2));
486    }
487
488    #[test]
489    fn pack_roundtrip() {
490        let cases = [
491            Value::Nil,
492            Value::Bool(true),
493            Value::Bool(false),
494            Value::Int(-42),
495            Value::Float(0.5),
496        ];
497        for v in cases {
498            let (t, b) = v.unpack();
499            assert!(unsafe { Value::pack(t, b) }.raw_eq(v));
500        }
501    }
502
503    #[test]
504    fn f2i_exact_boundaries() {
505        // exact decimal literals, not powi: miri perturbs non-exact float ops
506        assert_eq!(f2i_exact(0.0), Some(0));
507        assert_eq!(f2i_exact(-0.0), Some(0));
508        assert_eq!(f2i_exact(9007199254740992.0), Some(1 << 53));
509        assert_eq!(f2i_exact(-9223372036854775808.0), Some(i64::MIN));
510        assert_eq!(f2i_exact(9223372036854775808.0), None); // one past i64::MAX
511        assert_eq!(f2i_exact(0.5), None);
512        assert_eq!(f2i_exact(f64::NAN), None);
513        assert_eq!(f2i_exact(f64::INFINITY), None);
514    }
515}