Skip to main content

bloqade_lanes_bytecode_core/isa/
program.rs

1//! Flat program container for the vihaco-backed ISA.
2//!
3//! A program is a [`Version`] plus a flat `Vec<`[`Instruction`]`>` — no
4//! functions, labels, or string interner (our programs are a single flat
5//! instruction list; see <https://github.com/QuEraComputing/bloqade-lanes/issues/769>).
6//! vihaco's [`vihaco::module::Module`] / [`vihaco::ProgramLoader`] carry that
7//! structured-language machinery, so we keep a thin container and delegate the
8//! per-instruction work to vihaco's derived codec ([`WriteBytes`]/[`FromBytes`])
9//! and text parser ([`vihaco_parser_core::Parse`]).
10//!
11//! ## Binary layout (native, breaking vs. legacy `BLQD`)
12//!
13//! ```text
14//! magic    : 5 bytes  = b"LANES"
15//! version  : u32 LE   = (major << 16) | minor
16//! code     : N * INSTRUCTION_WIDTH bytes (vihaco fixed-width words)
17//! ```
18//!
19//! There is no section container: instruction words are fixed-width and
20//! self-delimiting, so the code section is simply the remaining bytes.
21
22use std::io::Cursor;
23
24use vihaco::instruction::{FromBytes, WriteBytes};
25use vihaco::module::Module;
26use vihaco::value::{Type, Value};
27
28use super::{INSTRUCTION_WIDTH, Instruction};
29use crate::version::Version;
30
31/// 5-byte magic identifying a native vihaco-backed Bloqade Lanes program.
32/// Distinct from the legacy `BLQD` container so the two can't be confused.
33pub const MAGIC: &[u8; 5] = b"LANES";
34
35/// Header length: [`MAGIC`] (5 bytes) followed by a packed u32 version.
36const HEADER_LEN: usize = MAGIC.len() + 4;
37
38/// Consumer metadata carried in `Module::extra` (vihaco has no version field).
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct LanesInfo {
41    pub version: Version,
42}
43
44impl Default for LanesInfo {
45    fn default() -> Self {
46        Self {
47            version: Version::new(0, 0),
48        }
49    }
50}
51
52impl std::fmt::Display for LanesInfo {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        write!(f, "version {}", self.version)
55    }
56}
57
58/// A Bloqade Lanes program: a vihaco `Module` specialised to our ISA. A single
59/// `@main` function's worth of flat code plus the version in `extra`.
60pub type Program = Module<Instruction, Value, Type, LanesInfo>;
61
62/// Build a `Program` from a version + flat instruction list. This is the ONE
63/// constructor used by both binary and text loading, so all `Program`s built
64/// from the same (version, code) compare equal regardless of source.
65#[allow(clippy::field_reassign_with_default)] // `Module` is a foreign type; struct-literal init is not possible
66pub fn from_code(version: Version, code: Vec<Instruction>) -> Program {
67    let mut m = Program::default();
68    m.code = code;
69    m.extra = LanesInfo { version };
70    m
71}
72
73/// Error from binary (de)serialization.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum BinaryError {
76    /// First five bytes were not [`MAGIC`].
77    BadMagic,
78    /// Buffer ended before a complete header or instruction word.
79    Truncated { expected: usize, got: usize },
80    /// The code region length is not a multiple of [`INSTRUCTION_WIDTH`].
81    UnalignedCode { len: usize },
82    /// A word held an opcode/payload vihaco could not decode.
83    Decode { pc: usize, message: String },
84}
85
86impl std::fmt::Display for BinaryError {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            BinaryError::BadMagic => write!(f, "bad magic bytes (expected LANES)"),
90            BinaryError::Truncated { expected, got } => {
91                write!(f, "truncated: expected {expected} bytes, got {got}")
92            }
93            BinaryError::UnalignedCode { len } => write!(
94                f,
95                "code length {len} is not a multiple of {INSTRUCTION_WIDTH}"
96            ),
97            BinaryError::Decode { pc, message } => {
98                write!(f, "decode error at instruction {pc}: {message}")
99            }
100        }
101    }
102}
103
104impl std::error::Error for BinaryError {}
105
106/// Serialize to the native binary format (see module docs).
107pub fn to_binary(program: &Program) -> Vec<u8> {
108    let mut buf = Vec::with_capacity(HEADER_LEN + program.code.len() * INSTRUCTION_WIDTH as usize);
109    buf.extend_from_slice(MAGIC);
110    let packed: u32 = program.extra.version.into();
111    buf.extend_from_slice(&packed.to_le_bytes());
112    for inst in &program.code {
113        // WriteBytes into a Vec is infallible.
114        inst.write_bytes(&mut buf)
115            .expect("writing instruction bytes to a Vec cannot fail");
116    }
117    buf
118}
119
120/// Deserialize from the native binary format.
121pub fn from_binary(bytes: &[u8]) -> Result<Program, BinaryError> {
122    if bytes.len() < HEADER_LEN {
123        return Err(BinaryError::Truncated {
124            expected: HEADER_LEN,
125            got: bytes.len(),
126        });
127    }
128    if &bytes[0..MAGIC.len()] != MAGIC {
129        return Err(BinaryError::BadMagic);
130    }
131    let packed = u32::from_le_bytes([
132        bytes[MAGIC.len()],
133        bytes[MAGIC.len() + 1],
134        bytes[MAGIC.len() + 2],
135        bytes[MAGIC.len() + 3],
136    ]);
137    let version = Version::from(packed);
138
139    let code = &bytes[HEADER_LEN..];
140    let width = INSTRUCTION_WIDTH as usize;
141    if !code.len().is_multiple_of(width) {
142        return Err(BinaryError::UnalignedCode { len: code.len() });
143    }
144
145    let count = code.len() / width;
146    let mut instructions = Vec::with_capacity(count);
147    let mut cursor = Cursor::new(code);
148    for pc in 0..count {
149        let inst = Instruction::from_bytes(&mut cursor).map_err(|e| BinaryError::Decode {
150            pc,
151            message: e.to_string(),
152        })?;
153        instructions.push(inst);
154    }
155
156    Ok(from_code(version, instructions))
157}
158
159#[cfg(test)]
160#[allow(clippy::approx_constant)] // illustrative sample floats, not math constants
161mod tests {
162    use super::*;
163
164    fn sample() -> Program {
165        use vihaco::value::Value;
166        use vihaco_cpu::Instruction as Cpu;
167        from_code(
168            Version::new(1, 2),
169            vec![
170                Instruction::Cpu(Cpu::Const(Value::F64(1.5))),
171                Instruction::Cpu(Cpu::Const(Value::I64(-42))),
172                Instruction::Cpu(Cpu::Dup),
173                Instruction::ConstLoc(0x0000_0000_0100_0000),
174                Instruction::ConstLane(0x0000_0000_0000_0001),
175                Instruction::ConstZone(0x0000_0003),
176                Instruction::InitialFill(2),
177                Instruction::Move(1),
178                Instruction::LocalRz(1),
179                Instruction::LocalR(3),
180                Instruction::GlobalRz,
181                Instruction::Cz,
182                Instruction::Measure(1),
183                Instruction::AwaitMeasure,
184                Instruction::NewArray(2, 10, 20),
185                Instruction::GetItem(2),
186                Instruction::SetDetector,
187                Instruction::Cpu(Cpu::Halt),
188                Instruction::Return,
189            ],
190        )
191    }
192
193    #[test]
194    fn binary_round_trips() {
195        let program = sample();
196        let bytes = to_binary(&program);
197        assert_eq!(&bytes[0..MAGIC.len()], MAGIC);
198        assert_eq!(
199            bytes.len(),
200            HEADER_LEN + program.code.len() * INSTRUCTION_WIDTH as usize
201        );
202        assert_eq!(from_binary(&bytes).unwrap(), program);
203    }
204
205    #[test]
206    fn binary_preserves_version() {
207        let bytes = to_binary(&sample());
208        assert_eq!(
209            from_binary(&bytes).unwrap().extra.version,
210            Version::new(1, 2)
211        );
212    }
213
214    #[test]
215    fn empty_program_round_trips() {
216        let program = from_code(Version::new(1, 0), vec![]);
217        let bytes = to_binary(&program);
218        assert_eq!(bytes.len(), HEADER_LEN);
219        assert_eq!(from_binary(&bytes).unwrap(), program);
220    }
221
222    #[test]
223    fn bad_magic_rejected() {
224        let mut bytes = to_binary(&sample());
225        bytes[0] = b'X';
226        assert_eq!(from_binary(&bytes), Err(BinaryError::BadMagic));
227    }
228
229    #[test]
230    fn short_buffer_rejected() {
231        assert_eq!(
232            from_binary(b"LAN"),
233            Err(BinaryError::Truncated {
234                expected: HEADER_LEN,
235                got: 3
236            })
237        );
238    }
239
240    #[test]
241    fn unaligned_code_rejected() {
242        let mut bytes = to_binary(&sample());
243        bytes.push(0); // one stray byte past the last full word
244        assert!(matches!(
245            from_binary(&bytes),
246            Err(BinaryError::UnalignedCode { .. })
247        ));
248    }
249
250    #[test]
251    fn decode_error_on_bad_opcode() {
252        // A well-formed header followed by one aligned word whose opcode byte
253        // (0xFF) names no instruction: length checks pass, so the failure must
254        // come from per-word decoding, tagged with its pc.
255        let mut bytes = Vec::new();
256        bytes.extend_from_slice(MAGIC);
257        let packed: u32 = Version::new(1, 0).into();
258        bytes.extend_from_slice(&packed.to_le_bytes());
259        bytes.extend_from_slice(&[0xFF; INSTRUCTION_WIDTH as usize]);
260        assert!(
261            matches!(from_binary(&bytes), Err(BinaryError::Decode { pc: 0, .. })),
262            "got {:?}",
263            from_binary(&bytes)
264        );
265    }
266
267    #[test]
268    fn binary_error_display_strings() {
269        assert_eq!(
270            BinaryError::BadMagic.to_string(),
271            "bad magic bytes (expected LANES)"
272        );
273        assert_eq!(
274            BinaryError::Truncated {
275                expected: 9,
276                got: 3
277            }
278            .to_string(),
279            "truncated: expected 9 bytes, got 3"
280        );
281        assert_eq!(
282            BinaryError::UnalignedCode { len: 5 }.to_string(),
283            format!("code length 5 is not a multiple of {INSTRUCTION_WIDTH}")
284        );
285        assert_eq!(
286            BinaryError::Decode {
287                pc: 2,
288                message: "boom".into()
289            }
290            .to_string(),
291            "decode error at instruction 2: boom"
292        );
293    }
294
295    #[test]
296    fn lanes_info_display() {
297        let info = LanesInfo {
298            version: Version::new(1, 4),
299        };
300        assert_eq!(info.to_string(), "version 1.4");
301    }
302}