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}