Skip to main content

pathland_core/
opcode.rs

1//! The fixed-size 16-byte Pathland opcode.
2//!
3//! Layout (see `spec/OPCODE.md`):
4//!
5//! ```text
6//! [Category: u8][Command: u8][Flags: u16][A: u32][B: u32][C: u32]
7//! ```
8//!
9//! All multi-byte fields are little-endian on the wire. `size_of::<Opcode>() == 16`,
10//! so a 64-byte cache line holds exactly 4 opcodes.
11
12use core::ptr;
13
14/// Size of one opcode in bytes.
15pub const OPCODE_SIZE: usize = 16;
16
17/// A fixed-size 16-byte opcode.
18///
19/// This struct intentionally mirrors the wire layout and is `#[repr(C, packed)]`
20/// per the specification. Because it is packed, fields may be unaligned; always
21/// read fields through the accessor methods (which use unaligned reads) rather
22/// than by value.
23#[repr(C, packed)]
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct Opcode {
26    /// Opcode category (see `constants::Category`).
27    pub category: u8,
28    /// Command within the category (see `constants::Command*`).
29    pub command: u8,
30    /// Category/command-specific flags.
31    pub flags: u16,
32    /// Primary payload.
33    pub a: u32,
34    /// Secondary payload.
35    pub b: u32,
36    /// Tertiary payload.
37    pub c: u32,
38}
39
40impl Opcode {
41    /// Byte size of an opcode (`16`).
42    pub const SIZE: usize = OPCODE_SIZE;
43
44    /// Construct an opcode from raw fields.
45    #[inline]
46    pub const fn new(category: u8, command: u8, flags: u16, a: u32, b: u32, c: u32) -> Self {
47        Self {
48            category,
49            command,
50            flags,
51            a,
52            b,
53            c,
54        }
55    }
56
57    /// The opcode category.
58    #[inline]
59    pub fn category(&self) -> u8 {
60        self.category
61    }
62
63    /// The command within the category.
64    #[inline]
65    pub fn command(&self) -> u8 {
66        self.command
67    }
68
69    /// The 16-bit flags word.
70    #[inline]
71    pub fn flags(&self) -> u16 {
72        // SAFETY: read_unaligned is well-defined for packed fields.
73        unsafe { ptr::addr_of!(self.flags).read_unaligned() }
74    }
75
76    /// Primary payload (little-endian u32).
77    #[inline]
78    pub fn a(&self) -> u32 {
79        // SAFETY: read_unaligned is well-defined for packed fields.
80        unsafe { ptr::addr_of!(self.a).read_unaligned() }
81    }
82
83    /// Secondary payload (little-endian u32).
84    #[inline]
85    pub fn b(&self) -> u32 {
86        // SAFETY: read_unaligned is well-defined for packed fields.
87        unsafe { ptr::addr_of!(self.b).read_unaligned() }
88    }
89
90    /// Tertiary payload (little-endian u32).
91    #[inline]
92    pub fn c(&self) -> u32 {
93        // SAFETY: read_unaligned is well-defined for packed fields.
94        unsafe { ptr::addr_of!(self.c).read_unaligned() }
95    }
96
97    /// Interpret `A` as an `f32` (bit-preserving).
98    #[inline]
99    pub fn a_f32(&self) -> f32 {
100        f32::from_bits(self.a())
101    }
102
103    /// Interpret `B` as an `f32` (bit-preserving).
104    #[inline]
105    pub fn b_f32(&self) -> f32 {
106        f32::from_bits(self.b())
107    }
108
109    /// Interpret `C` as an `f32` (bit-preserving).
110    #[inline]
111    pub fn c_f32(&self) -> f32 {
112        f32::from_bits(self.c())
113    }
114
115    /// Serialize to the 16-byte wire representation (little-endian).
116    pub fn to_bytes(&self) -> [u8; OPCODE_SIZE] {
117        let mut out = [0u8; OPCODE_SIZE];
118        out[0] = self.category;
119        out[1] = self.command;
120        // SAFETY: read_unaligned is well-defined for packed fields.
121        let flags = unsafe { ptr::addr_of!(self.flags).read_unaligned() };
122        let a = unsafe { ptr::addr_of!(self.a).read_unaligned() };
123        let b = unsafe { ptr::addr_of!(self.b).read_unaligned() };
124        let c = unsafe { ptr::addr_of!(self.c).read_unaligned() };
125        out[2..4].copy_from_slice(&flags.to_le_bytes());
126        out[4..8].copy_from_slice(&a.to_le_bytes());
127        out[8..12].copy_from_slice(&b.to_le_bytes());
128        out[12..16].copy_from_slice(&c.to_le_bytes());
129        out
130    }
131
132    /// Deserialize from the 16-byte wire representation (little-endian).
133    pub fn from_bytes(bytes: &[u8; OPCODE_SIZE]) -> Self {
134        let category = bytes[0];
135        let command = bytes[1];
136        let flags = u16::from_le_bytes([bytes[2], bytes[3]]);
137        let a = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
138        let b = u32::from_le_bytes(bytes[8..12].try_into().unwrap());
139        let c = u32::from_le_bytes(bytes[12..16].try_into().unwrap());
140        Self {
141            category,
142            command,
143            flags,
144            a,
145            b,
146            c,
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn size_is_16_bytes() {
157        assert_eq!(core::mem::size_of::<Opcode>(), 16);
158    }
159
160    #[test]
161    fn round_trips_through_bytes() {
162        let op = Opcode::new(0x01, 0x01, 0x0001, 0x1122_3344, 0x5566_7788, 0x99AA_BBCC);
163        let bytes = op.to_bytes();
164        let back = Opcode::from_bytes(&bytes);
165        assert_eq!(op, back);
166    }
167
168    #[test]
169    fn known_byte_layout() {
170        // TREE / CREATE_NODE, no flags, nodeId=1, componentType=VSTACK(0x0010), c=0
171        let op = Opcode::new(0x01, 0x01, 0x0000, 1, 0x0010, 0);
172        assert_eq!(
173            op.to_bytes(),
174            [0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
175        );
176    }
177
178    #[test]
179    fn float_payloads_round_trip() {
180        let op = Opcode::new(0x02, 0x01, 0, 42, 100.0f32.to_bits(), 200.0f32.to_bits());
181        let back = Opcode::from_bytes(&op.to_bytes());
182        assert_eq!(back.a(), 42);
183        assert_eq!(back.b_f32(), 100.0);
184        assert_eq!(back.c_f32(), 200.0);
185    }
186}