Skip to main content

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}