luna_core/runtime/frame_marker.rs
1//! P17-D v2 Phase 1 — type foundation for LJ-style unified stack-frame
2//! memory.
3//!
4//! In LuaJIT 2.1's LJ_FR2 model, each Lua activation record stores TWO
5//! pieces of metadata INLINE in the value stack, at fixed offsets
6//! relative to the frame's `base`:
7//!
8//! - `stack[base-2]` — the function being called (a closure GCRef).
9//! In our terms: `Value::Closure(Gc<LuaClosure>)` for Lua frames.
10//! - `stack[base-1]` — a 64-bit "frame marker" packing:
11//! - bits 0-2: the frame kind (LJ FRAME_LUA / FRAME_C / FRAME_CONT
12//! / FRAME_VARG). Encoded as the type tag's lower 3 bits.
13//! - bits 3-63: the frame's PC (for Lua frames, a u32 bytecode index
14//! fits comfortably in 61 bits) or a frame-delta (for native /
15//! vararg / continuation frames).
16//!
17//! See `lj_frame.h:33-110` for the upstream macros; this module mirrors
18//! their semantics in luna terms. See
19//! `docs/rfcs/20260622-p17-d-v2-lj-unified-stack/design.md` §1 for the
20//! migration plan that consumes these primitives.
21//!
22//! **Phase 1 (this module)** — pure type definitions + bit-packing
23//! helpers + roundtrip tests. NOT yet consumed by `Vm`. Adding this
24//! module has no behavior or perf impact; Phase 2-4 wires it into
25//! the frame setup/teardown paths.
26
27/// The kind of an activation record, encoded into the lower 3 bits of
28/// a [`FrameMarker`] u64. Mirrors LuaJIT's `FRAME_*` enum from
29/// `lj_frame.h:18-22`.
30///
31/// `Lua` corresponds to a Lua-level function activation; its PC field
32/// holds the next bytecode index to execute on resume.
33/// `Cont` corresponds to a native continuation frame (luna's `Cont`
34/// variant of `CallFrame`). luna doesn't currently emit separate
35/// FRAME_C / FRAME_VARG markers — those PUC distinctions are encoded
36/// elsewhere (e.g., `Frame.from_c`).
37#[derive(Clone, Copy, PartialEq, Eq, Debug)]
38#[repr(u8)]
39pub enum FrameKind {
40 /// A Lua function frame. The upper bits of the marker hold the
41 /// frame's PC (u32 bytecode index into the proto's `code`).
42 Lua = 0,
43 /// A native continuation frame (pcall, xpcall, metamethod
44 /// continuation, etc.). The upper bits hold a frame delta to the
45 /// previous frame's base — not a PC. Continuations have no Lua PC.
46 Cont = 1,
47}
48
49impl FrameKind {
50 /// Decode a tag value (the lower 3 bits of a marker) back into a
51 /// `FrameKind`. Returns `None` for invalid tag values; the caller
52 /// is expected to treat that as a corrupt frame.
53 #[inline]
54 pub fn from_tag(tag: u8) -> Option<Self> {
55 match tag {
56 0 => Some(FrameKind::Lua),
57 1 => Some(FrameKind::Cont),
58 _ => None,
59 }
60 }
61
62 /// The 3-bit tag value used in marker encoding.
63 #[inline]
64 pub fn tag(self) -> u8 {
65 self as u8
66 }
67}
68
69/// Number of low bits reserved for the frame kind tag.
70const FRAME_KIND_BITS: u32 = 3;
71const FRAME_KIND_MASK: u64 = (1 << FRAME_KIND_BITS) - 1;
72
73/// A 64-bit packed activation record marker that lives in the value
74/// stack slot `base-1` of any Lua frame (LJ_FR2 model). Carries the
75/// frame kind (lower 3 bits) + PC-or-delta payload (upper 61 bits).
76///
77/// Construction goes through [`FrameMarker::new_lua`] /
78/// [`FrameMarker::new_cont`] which encode their args correctly;
79/// destruction goes through [`FrameMarker::kind`] / [`FrameMarker::payload`].
80///
81/// Internally a u64 so it round-trips through `Value::Int(i64)` safely
82/// without losing bits — luna's `Value::Int` is a full i64 (no NaN
83/// boxing), so the bit pattern survives the stack store/load cleanly.
84#[derive(Clone, Copy, PartialEq, Eq, Debug)]
85pub struct FrameMarker(u64);
86
87impl FrameMarker {
88 /// Build a `FrameKind::Lua` marker with the given PC. PC must fit
89 /// in 61 bits (a u32 always does). Panics in debug mode on
90 /// overflow; production code is expected to pass a u32.
91 #[inline]
92 pub fn new_lua(pc: u32) -> Self {
93 let payload = (pc as u64) << FRAME_KIND_BITS;
94 FrameMarker(payload | FrameKind::Lua.tag() as u64)
95 }
96
97 /// Build a `FrameKind::Cont` marker with the given frame delta
98 /// (slots between this frame's base and the previous frame's base).
99 /// Delta must fit in 61 bits.
100 #[inline]
101 pub fn new_cont(delta: u32) -> Self {
102 let payload = (delta as u64) << FRAME_KIND_BITS;
103 FrameMarker(payload | FrameKind::Cont.tag() as u64)
104 }
105
106 /// Read the frame kind (lower 3 bits).
107 #[inline]
108 pub fn kind(self) -> Option<FrameKind> {
109 FrameKind::from_tag((self.0 & FRAME_KIND_MASK) as u8)
110 }
111
112 /// Read the PC/delta payload (upper 61 bits, shifted down).
113 /// For Lua frames this is the bytecode PC; for Cont frames the
114 /// caller-base delta.
115 #[inline]
116 pub fn payload(self) -> u32 {
117 (self.0 >> FRAME_KIND_BITS) as u32
118 }
119
120 /// Raw bits, suitable for storing in a `Value::Int(i64)` slot.
121 /// The reverse direction is [`FrameMarker::from_raw`].
122 #[inline]
123 pub fn to_raw(self) -> i64 {
124 self.0 as i64
125 }
126
127 /// Reconstruct a `FrameMarker` from raw bits read out of a value
128 /// stack slot. The caller is responsible for ensuring the slot
129 /// actually held a marker (otherwise the kind decode may fail).
130 #[inline]
131 pub fn from_raw(raw: i64) -> Self {
132 FrameMarker(raw as u64)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn lua_marker_roundtrip_basic() {
142 let m = FrameMarker::new_lua(42);
143 assert_eq!(m.kind(), Some(FrameKind::Lua));
144 assert_eq!(m.payload(), 42);
145 }
146
147 #[test]
148 fn cont_marker_roundtrip_basic() {
149 let m = FrameMarker::new_cont(7);
150 assert_eq!(m.kind(), Some(FrameKind::Cont));
151 assert_eq!(m.payload(), 7);
152 }
153
154 #[test]
155 fn raw_bits_roundtrip_through_i64() {
156 for pc in [0u32, 1, 100, u16::MAX as u32, u32::MAX] {
157 let m = FrameMarker::new_lua(pc);
158 let raw = m.to_raw();
159 let m2 = FrameMarker::from_raw(raw);
160 assert_eq!(m2.kind(), Some(FrameKind::Lua), "kind survives pc={}", pc);
161 assert_eq!(m2.payload(), pc, "payload survives pc={}", pc);
162 }
163 }
164
165 #[test]
166 fn cont_payload_survives_full_u32() {
167 for delta in [0u32, 1, 100, u32::MAX] {
168 let m = FrameMarker::new_cont(delta);
169 let m2 = FrameMarker::from_raw(m.to_raw());
170 assert_eq!(m2.kind(), Some(FrameKind::Cont));
171 assert_eq!(m2.payload(), delta);
172 }
173 }
174
175 #[test]
176 fn kind_tag_values_match_lj() {
177 // LJ frame.h: FRAME_LUA=0, FRAME_C=1, FRAME_CONT=2, FRAME_VARG=3.
178 // luna currently uses 0=Lua, 1=Cont (Cont covers both PUC's C-
179 // continuation and metamethod-continuation cases; we don't
180 // emit FRAME_C / FRAME_VARG markers yet).
181 assert_eq!(FrameKind::Lua.tag(), 0);
182 assert_eq!(FrameKind::Cont.tag(), 1);
183 assert_eq!(FrameKind::from_tag(0), Some(FrameKind::Lua));
184 assert_eq!(FrameKind::from_tag(1), Some(FrameKind::Cont));
185 assert_eq!(
186 FrameKind::from_tag(2),
187 None,
188 "FRAME_CONT_LJ not emitted yet"
189 );
190 assert_eq!(FrameKind::from_tag(7), None, "invalid tag rejected");
191 }
192
193 #[test]
194 fn kind_decode_safe_on_invalid() {
195 // Garbage marker (e.g., when stack[base-1] was overwritten by
196 // a regular Value::Int with low bits != 0 or 1) returns None
197 // rather than panicking. Callers that need correctness must
198 // validate before consuming.
199 let garbage = FrameMarker::from_raw(0x_8000_0000_0000_0007u64 as i64);
200 assert_eq!(garbage.kind(), None);
201 }
202}