Skip to main content

bloqade_lanes_bytecode_core/isa/
def.rs

1//! The Bloqade Lanes [`Instruction`] enum and its fixed encoding width.
2//!
3//! The instruction set is defined once as a `#[derive(Instruction, Parse)]`
4//! enum; vihaco's derive macros generate the binary codec
5//! ([`vihaco::instruction::WriteBytes`] / [`FromBytes`](vihaco::instruction::FromBytes))
6//! and the text (`.sst`) parser ([`vihaco_parser_core::Parse`]). See the
7//! [`super`] module docs for the design rationale (CPU-op reuse via
8//! [`Cpu`](Instruction::Cpu), native byte layout).
9
10use vihaco::Instruction;
11
12/// Fixed width, in bytes, of every encoded instruction word: 1 opcode byte
13/// plus a payload up to the nested [`vihaco_cpu::Instruction`] word (16 bytes),
14/// zero-padded. Decoding consumes exactly this many bytes per instruction, so a
15/// flat program decodes without desync.
16///
17/// The `17` is forced entirely by nesting the 16-byte vihaco-cpu word (1 + 16);
18/// every lanes-native variant needs at most 13 bytes (`NewArray` = 1 + 3×u32).
19/// It is *not* an alignment choice. This is entangled with the array /
20/// measurement-result representation (today a bespoke `ARRAY_REF` +
21/// `new_array`/`get_item`), which is slated to move onto vihaco-cpu's heap
22/// allocator as a nested `IList` — see
23/// <https://github.com/QuEraComputing/bloqade-lanes/issues/776>. That refactor
24/// is deliberately deferred; the width stays 17 until it lands.
25pub const INSTRUCTION_WIDTH: u32 = 17;
26
27/// The Bloqade Lanes instruction set, defined on the vihaco framework.
28///
29/// Device operands use only the scalar types vihaco implements byte traits for
30/// (`u32`, `u64`, `i64`, `f64`); the legacy `u8`/`u16` array operands are
31/// widened to `u32`. CPU ops are reused from [`vihaco_cpu`] via the nested
32/// [`Cpu`](Instruction::Cpu) variant (see module docs).
33///
34/// **Variant order is significant.** It is both the encoded opcode order and
35/// the text parser's try-order; a token that is a prefix of another must be
36/// declared *after* the longer token (hence `*_rz` precedes `*_r`), and the
37/// `#[delegate]` [`Cpu`](Instruction::Cpu) variant is declared **last** so
38/// device-specific tokens (e.g. `get_item <n>`) win over any vihaco-cpu token
39/// they would otherwise shadow.
40#[derive(Debug, Clone, PartialEq, Instruction, vihaco_parser::Parse)]
41#[instruction(width = 17)]
42pub enum Instruction {
43    // ---- Lanes-native stack ops (no round-trippable vihaco-cpu equivalent) ----
44    #[token = "pop"]
45    Pop,
46    #[token = "swap"]
47    Swap,
48    #[token = "return"]
49    Return,
50
51    // ---- Lane constants (hex operands) ----
52    #[token = "const_loc"]
53    #[delimiters(open = "", close = "", separator = "")]
54    ConstLoc(#[parse_with = "crate::isa::parse_helpers::hex_u64"] u64),
55
56    #[token = "const_lane"]
57    #[delimiters(open = "", close = "", separator = "")]
58    ConstLane(#[parse_with = "crate::isa::parse_helpers::hex_u64"] u64),
59
60    #[token = "const_zone"]
61    #[delimiters(open = "", close = "", separator = "")]
62    ConstZone(#[parse_with = "crate::isa::parse_helpers::hex_u32"] u32),
63
64    // ---- Atom arrangement ----
65    #[token = "initial_fill"]
66    #[delimiters(open = "", close = "", separator = "")]
67    InitialFill(u32),
68    #[token = "fill"]
69    #[delimiters(open = "", close = "", separator = "")]
70    Fill(u32),
71    #[token = "move"]
72    #[delimiters(open = "", close = "", separator = "")]
73    Move(u32),
74
75    // ---- Quantum gates (`*_rz` before `*_r`: token-prefix ordering) ----
76    #[token = "local_rz"]
77    #[delimiters(open = "", close = "", separator = "")]
78    LocalRz(u32),
79    #[token = "local_r"]
80    #[delimiters(open = "", close = "", separator = "")]
81    LocalR(u32),
82    #[token = "global_rz"]
83    GlobalRz,
84    #[token = "global_r"]
85    GlobalR,
86    #[token = "cz"]
87    Cz,
88
89    // ---- Measurement ----
90    #[token = "measure"]
91    #[delimiters(open = "", close = "", separator = "")]
92    Measure(u32),
93    #[token = "await_measure"]
94    AwaitMeasure,
95
96    // ---- Arrays ----
97    // `new_array <type_tag> <dim0> <dim1>` — all three operands required
98    // (1-D arrays use `dim1 = 0`). Legacy `u8`/`u16` widened to `u32`.
99    #[token = "new_array"]
100    #[delimiters(open = "", close = "", separator = " ")]
101    NewArray(u32, u32, u32),
102    #[token = "get_item"]
103    #[delimiters(open = "", close = "", separator = "")]
104    GetItem(u32),
105
106    // ---- Detectors / observables ----
107    #[token = "set_detector"]
108    SetDetector,
109    #[token = "set_observable"]
110    SetObservable,
111
112    // ---- CPU / stack ops, reused wholesale from vihaco-cpu ----
113    // Declared LAST: parsing is `#[delegate]`d to vihaco-cpu's parser, so
114    // device tokens above are tried first and win on any shared prefix.
115    #[delegate]
116    Cpu(vihaco_cpu::Instruction),
117}
118
119impl Instruction {
120    /// Canonical opcode name used for decode dispatch (the Python decoder
121    /// calls `_visit_{op_name}`) and introspection.
122    ///
123    /// For lanes-native ops this equals the parser `#[token]` / `Display`
124    /// mnemonic. For nested vihaco-cpu ops it returns the **decode-handler**
125    /// name (`const_float`/`const_int`/`dup`/`halt`), which deliberately
126    /// differs from the vihaco-cpu *text* syntax (`const.f64`, …) because
127    /// decode handler method names cannot contain `.`.
128    pub fn op_name(&self) -> &'static str {
129        match self {
130            Instruction::Pop => "pop",
131            Instruction::Swap => "swap",
132            Instruction::Return => "return",
133            Instruction::ConstLoc(_) => "const_loc",
134            Instruction::ConstLane(_) => "const_lane",
135            Instruction::ConstZone(_) => "const_zone",
136            Instruction::InitialFill(_) => "initial_fill",
137            Instruction::Fill(_) => "fill",
138            Instruction::Move(_) => "move",
139            Instruction::LocalRz(_) => "local_rz",
140            Instruction::LocalR(_) => "local_r",
141            Instruction::GlobalRz => "global_rz",
142            Instruction::GlobalR => "global_r",
143            Instruction::Cz => "cz",
144            Instruction::Measure(_) => "measure",
145            Instruction::AwaitMeasure => "await_measure",
146            Instruction::NewArray(..) => "new_array",
147            Instruction::GetItem(_) => "get_item",
148            Instruction::SetDetector => "set_detector",
149            Instruction::SetObservable => "set_observable",
150            Instruction::Cpu(cpu) => cpu_op_name(cpu),
151        }
152    }
153}
154
155/// Decode-handler dispatch name for a nested vihaco-cpu instruction. NOT the
156/// text mnemonic (see [`Instruction::op_name`] docs).
157fn cpu_op_name(cpu: &vihaco_cpu::Instruction) -> &'static str {
158    use vihaco::value::Value;
159    use vihaco_cpu::Instruction as Cpu;
160    match cpu {
161        Cpu::Const(Value::F64(_)) => "const_float",
162        Cpu::Const(Value::I64(_)) => "const_int",
163        Cpu::Const(_) => "const",
164        Cpu::Dup => "dup",
165        Cpu::Halt => "halt",
166        _ => "cpu",
167    }
168}
169
170impl std::fmt::Display for Instruction {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        match self {
173            Instruction::Pop => f.write_str("pop"),
174            Instruction::Swap => f.write_str("swap"),
175            Instruction::Return => f.write_str("return"),
176            Instruction::ConstLoc(v) => write!(f, "const_loc 0x{v:016x}"),
177            Instruction::ConstLane(v) => write!(f, "const_lane 0x{v:016x}"),
178            Instruction::ConstZone(v) => write!(f, "const_zone 0x{v:08x}"),
179            Instruction::InitialFill(a) => write!(f, "initial_fill {a}"),
180            Instruction::Fill(a) => write!(f, "fill {a}"),
181            Instruction::Move(a) => write!(f, "move {a}"),
182            Instruction::LocalRz(a) => write!(f, "local_rz {a}"),
183            Instruction::LocalR(a) => write!(f, "local_r {a}"),
184            Instruction::GlobalRz => f.write_str("global_rz"),
185            Instruction::GlobalR => f.write_str("global_r"),
186            Instruction::Cz => f.write_str("cz"),
187            Instruction::Measure(a) => write!(f, "measure {a}"),
188            Instruction::AwaitMeasure => f.write_str("await_measure"),
189            Instruction::NewArray(t, d0, d1) => write!(f, "new_array {t} {d0} {d1}"),
190            Instruction::GetItem(n) => write!(f, "get_item {n}"),
191            Instruction::SetDetector => f.write_str("set_detector"),
192            Instruction::SetObservable => f.write_str("set_observable"),
193            // vihaco-cpu owns its own Display (const.f64 / dup / halt / …).
194            Instruction::Cpu(cpu) => write!(f, "{cpu}"),
195        }
196    }
197}
198
199#[cfg(test)]
200#[allow(clippy::approx_constant)] // sample floats are illustrative, not math constants
201mod tests {
202    use super::*;
203    use chumsky::Parser as _;
204    use vihaco::instruction::{FromBytes, OpCode, WriteBytes};
205    use vihaco::value::Value;
206    use vihaco_cpu::Instruction as Cpu;
207    use vihaco_parser_core::Parse;
208
209    fn parse(input: &str) -> Instruction {
210        Instruction::parser()
211            .parse(input)
212            .into_result()
213            .unwrap_or_else(|e| panic!("parse({input:?}) failed: {e:?}"))
214    }
215
216    /// A representative instruction of every shape (unit, scalar, hex,
217    /// multi-field, and nested vihaco-cpu).
218    fn sample_program() -> Vec<Instruction> {
219        vec![
220            Instruction::Cpu(Cpu::Const(Value::F64(3.14159))),
221            Instruction::Cpu(Cpu::Const(Value::I64(-42))),
222            Instruction::Cpu(Cpu::Dup),
223            Instruction::ConstLoc(0x0000_0000_0100_0000),
224            Instruction::ConstLane(0x0000_0000_0000_0001),
225            Instruction::ConstZone(0x0000_0007),
226            Instruction::InitialFill(2),
227            Instruction::Fill(1),
228            Instruction::Move(2),
229            Instruction::LocalRz(1),
230            Instruction::LocalR(3),
231            Instruction::GlobalRz,
232            Instruction::GlobalR,
233            Instruction::Cz,
234            Instruction::Measure(1),
235            Instruction::AwaitMeasure,
236            Instruction::NewArray(1, 3, 0),
237            Instruction::GetItem(1),
238            Instruction::SetDetector,
239            Instruction::SetObservable,
240            Instruction::Pop,
241            Instruction::Swap,
242            Instruction::Cpu(Cpu::Halt),
243            Instruction::Return,
244        ]
245    }
246
247    #[test]
248    fn every_instruction_encodes_to_fixed_width() {
249        assert_eq!(Instruction::width(), INSTRUCTION_WIDTH);
250        for inst in sample_program() {
251            let mut buf = Vec::new();
252            inst.write_bytes(&mut buf).unwrap();
253            assert_eq!(
254                buf.len(),
255                INSTRUCTION_WIDTH as usize,
256                "{inst:?} did not encode to a full {INSTRUCTION_WIDTH}-byte word"
257            );
258        }
259    }
260
261    #[test]
262    fn binary_round_trips_a_flat_program() {
263        let program = sample_program();
264
265        // Encode every instruction back-to-back into one buffer.
266        let mut bytes = Vec::new();
267        for inst in &program {
268            inst.write_bytes(&mut bytes).unwrap();
269        }
270        assert_eq!(bytes.len(), program.len() * INSTRUCTION_WIDTH as usize);
271
272        // Decode the stream and confirm it matches, proving fixed-width words
273        // stay aligned (no desync from padding).
274        let mut cursor = std::io::Cursor::new(bytes);
275        let mut decoded = Vec::new();
276        for _ in 0..program.len() {
277            decoded.push(Instruction::from_bytes(&mut cursor).unwrap());
278        }
279        assert_eq!(decoded, program);
280    }
281
282    #[test]
283    fn text_parses_each_shape() {
284        assert_eq!(
285            parse("const_loc 0x0000000001000000"),
286            Instruction::ConstLoc(0x0000_0000_0100_0000)
287        );
288        assert_eq!(
289            parse("const_lane 0x0000000000000001"),
290            Instruction::ConstLane(1)
291        );
292        assert_eq!(parse("const_zone 0x00000007"), Instruction::ConstZone(7));
293        assert_eq!(parse("initial_fill 2"), Instruction::InitialFill(2));
294        assert_eq!(parse("move 2"), Instruction::Move(2));
295        assert_eq!(parse("new_array 1 3 0"), Instruction::NewArray(1, 3, 0));
296        assert_eq!(parse("get_item 1"), Instruction::GetItem(1));
297
298        for (text, inst) in [
299            ("pop", Instruction::Pop),
300            ("swap", Instruction::Swap),
301            ("return", Instruction::Return),
302            ("global_rz", Instruction::GlobalRz),
303            ("global_r", Instruction::GlobalR),
304            ("cz", Instruction::Cz),
305            ("await_measure", Instruction::AwaitMeasure),
306            ("set_detector", Instruction::SetDetector),
307            ("set_observable", Instruction::SetObservable),
308        ] {
309            assert_eq!(parse(text), inst, "text {text:?}");
310        }
311    }
312
313    #[test]
314    fn cpu_ops_delegate_to_vihaco_cpu() {
315        // CPU mnemonics use vihaco-cpu's syntax and route through the nested
316        // `Cpu` variant.
317        assert_eq!(
318            parse("const.i64 42"),
319            Instruction::Cpu(Cpu::Const(Value::I64(42)))
320        );
321        assert_eq!(
322            parse("const.f64 1.5"),
323            Instruction::Cpu(Cpu::Const(Value::F64(1.5)))
324        );
325        assert_eq!(parse("dup"), Instruction::Cpu(Cpu::Dup));
326        assert_eq!(parse("halt"), Instruction::Cpu(Cpu::Halt));
327    }
328
329    #[test]
330    fn device_token_wins_over_delegated_cpu() {
331        // vihaco-cpu also defines `get_item` (unit), but the lanes array
332        // `get_item <n>` is declared first and must win on a full line.
333        assert_eq!(parse("get_item 2"), Instruction::GetItem(2));
334    }
335
336    #[test]
337    fn prefix_tokens_disambiguate() {
338        // `local_r` is a prefix of `local_rz`; `global_r` of `global_rz`.
339        assert_eq!(parse("local_rz 1"), Instruction::LocalRz(1));
340        assert_eq!(parse("local_r 3"), Instruction::LocalR(3));
341        assert_eq!(parse("global_rz"), Instruction::GlobalRz);
342        assert_eq!(parse("global_r"), Instruction::GlobalR);
343    }
344
345    #[test]
346    fn text_then_binary_agree() {
347        let from_text = parse("const_loc 0x0000000001000000");
348        let mut bytes = Vec::new();
349        from_text.write_bytes(&mut bytes).unwrap();
350        let decoded = Instruction::from_bytes(&mut std::io::Cursor::new(bytes)).unwrap();
351        assert_eq!(from_text, decoded);
352    }
353
354    #[test]
355    fn op_name_matches_display_leading_token_for_native_ops() {
356        // For lanes-native ops, op_name() == the first whitespace token of Display
357        // (Display is itself pinned to the parser #[token] by the round-trip test),
358        // so op_name / Display / parser token cannot drift apart.
359        let native = [
360            Instruction::Pop,
361            Instruction::Swap,
362            Instruction::Return,
363            Instruction::ConstLoc(0),
364            Instruction::ConstLane(0),
365            Instruction::ConstZone(0),
366            Instruction::InitialFill(1),
367            Instruction::Fill(1),
368            Instruction::Move(1),
369            Instruction::LocalRz(1),
370            Instruction::LocalR(1),
371            Instruction::GlobalRz,
372            Instruction::GlobalR,
373            Instruction::Cz,
374            Instruction::Measure(1),
375            Instruction::AwaitMeasure,
376            Instruction::NewArray(1, 1, 0),
377            Instruction::GetItem(1),
378            Instruction::SetDetector,
379            Instruction::SetObservable,
380        ];
381        for inst in native {
382            let display_head = inst.to_string();
383            let head = display_head.split_whitespace().next().unwrap();
384            assert_eq!(inst.op_name(), head, "op_name/Display drift for {inst:?}");
385        }
386    }
387
388    #[test]
389    fn cpu_op_name_uses_decode_handler_names() {
390        assert_eq!(
391            Instruction::Cpu(Cpu::Const(Value::F64(0.0))).op_name(),
392            "const_float"
393        );
394        assert_eq!(
395            Instruction::Cpu(Cpu::Const(Value::I64(0))).op_name(),
396            "const_int"
397        );
398        assert_eq!(Instruction::Cpu(Cpu::Dup).op_name(), "dup");
399        assert_eq!(Instruction::Cpu(Cpu::Halt).op_name(), "halt");
400    }
401
402    #[test]
403    fn cpu_op_name_falls_back_for_other_variants() {
404        // A `const.<type>` that is neither float nor int uses the generic
405        // "const" handler name (the `Cpu::Const(_)` catch-all arm)...
406        assert_eq!(
407            Instruction::Cpu(Cpu::Const(Value::Bool(true))).op_name(),
408            "const"
409        );
410        // ...and any other reused vihaco-cpu op falls through to "cpu".
411        assert_eq!(Instruction::Cpu(Cpu::Print).op_name(), "cpu");
412    }
413
414    #[test]
415    fn display_matches_parser_tokens() {
416        use std::string::ToString;
417        // Every non-CPU variant's Display must re-parse to itself.
418        let samples = [
419            Instruction::ConstLoc(0x0100_0000),
420            Instruction::Move(2),
421            Instruction::LocalRz(1),
422            Instruction::GlobalR,
423            Instruction::NewArray(1, 3, 0),
424            Instruction::Pop,
425            Instruction::Return,
426            Instruction::Cpu(Cpu::Const(Value::F64(1.5))),
427            Instruction::Cpu(Cpu::Halt),
428        ];
429        for inst in samples {
430            let text = inst.to_string();
431            assert_eq!(parse(&text), inst, "Display/parse mismatch for {text:?}");
432        }
433    }
434}