Skip to main content

bloq_vm/
instruction.rs

1//! Backend-independent instructions executed by the Bloq VM.
2//!
3//! The types in this module deliberately contain no Bloq IR node, graph, or
4//! expression types. [`crate::lower()`] resolves those into dense identifiers,
5//! executable quantum streams, and structured control flow once up front.
6#![allow(
7    missing_docs,
8    reason = "the ISA containers and gate vocabulary are documented at their owning types"
9)]
10
11/// Dense task identifier in [`Program::tasks`].
12pub type TaskId = u32;
13/// Dense classical bit register.
14pub type BitId = u32;
15/// Dense simulator qubit.
16pub type QubitId = u32;
17/// Dense measurement record.
18pub type RecordId = u32;
19/// Runtime readiness signal, normally produced by an accepted RUS region.
20pub type ResourceId = u32;
21
22/// Default mock streaming-decoder latency, in physical memory rounds.
23pub const DEFAULT_DECODER_LATENCY_ROUNDS: u32 = 10;
24/// A lowered executable program.
25#[derive(Debug, Clone, PartialEq, serde::Serialize)]
26pub struct Program {
27    /// Number of simulator qubits.
28    pub qubit_count: u32,
29    /// Number of classical bit registers.
30    pub bit_count: u32,
31    /// Number of statically named measurement records.
32    pub record_count: u32,
33    /// All tasks, addressed by [`TaskId`]. Nested streams index this same arena.
34    pub tasks: Vec<Task>,
35    /// Backend-independent source geometry, indexed like `tasks`. Hand-built
36    /// programs may leave this empty.
37    pub task_origins: Vec<TaskOrigin>,
38    /// Top-level executable stream.
39    pub entry: Stream,
40    /// External logical patches injected when their source task is released.
41    pub inputs: Vec<LogicalInput>,
42    /// Terminal logical operators and their Pauli-frame registers.
43    pub outputs: Vec<LogicalOutput>,
44    /// Configured mock-decoder latency, in physical memory rounds.
45    pub decoder_latency_rounds: u32,
46}
47
48impl Default for Program {
49    fn default() -> Self {
50        Self {
51            qubit_count: 0,
52            bit_count: 0,
53            record_count: 0,
54            tasks: Vec::new(),
55            task_origins: Vec::new(),
56            entry: Stream::default(),
57            inputs: Vec::new(),
58            outputs: Vec::new(),
59            decoder_latency_rounds: DEFAULT_DECODER_LATENCY_ROUNDS,
60        }
61    }
62}
63
64/// Source geometry and function of one lowered task.
65#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
66pub struct TaskOrigin {
67    pub function: TaskFunction,
68    /// Reserved logical patch sites in source-lattice `(x, y)` coordinates.
69    pub sites: Box<[[i32; 2]]>,
70    /// Full source-lattice members retained for audit.
71    pub members: Box<[[i32; 3]]>,
72}
73
74/// Plot-facing function classification derived from compiled IR provenance.
75#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum TaskFunction {
78    Classical,
79    Cube,
80    PreparedY,
81    Factory,
82    FactoryBody,
83    TemporalHadamard,
84    Memory,
85    AdaptiveMeasurement,
86    IdealBoundary,
87    Control,
88    #[default]
89    Synthetic,
90}
91
92/// One external logical input in dense VM coordinates.
93#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
94pub struct LogicalInput {
95    pub port: [i32; 3],
96    /// Source task released when the encoded state arrives.
97    pub task: TaskId,
98    pub data_qubits: Box<[QubitId]>,
99    pub stabilizers: Box<[PauliProduct]>,
100    pub x: PauliProduct,
101    pub z: PauliProduct,
102}
103
104/// One terminal logical output in dense VM coordinates.
105#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
106pub struct LogicalOutput {
107    /// Source block-graph port coordinate.
108    pub port: [i32; 3],
109    /// Logical X observable at the output cut.
110    pub x: PauliProduct,
111    /// Logical Z observable at the output cut.
112    pub z: PauliProduct,
113    /// Whether an X frame correction is required.
114    pub frame_x: BitId,
115    /// Whether a Z frame correction is required.
116    pub frame_z: BitId,
117}
118
119impl Program {
120    /// Every instruction, including instructions owned by nested streams.
121    pub fn instructions(&self) -> impl Iterator<Item = &Instruction> {
122        self.tasks.iter().map(|task| &task.instruction)
123    }
124
125    /// Independently released sources in dense task order.
126    #[must_use]
127    pub fn sources(&self) -> Vec<SourceSchedule> {
128        self.tasks
129            .iter()
130            .enumerate()
131            .filter_map(|(task, source)| {
132                source.source.map(|role| SourceSchedule {
133                    task: task as TaskId,
134                    role,
135                    release: source.release,
136                    duration: source.duration,
137                })
138            })
139            .collect()
140    }
141
142    /// Render the complete lowered instruction stream as compact JSON.
143    ///
144    /// This is an inspection format, not a stable persistence protocol.
145    ///
146    /// # Errors
147    ///
148    /// Propagates JSON serialization errors.
149    pub fn to_json(&self) -> Result<String, serde_json::Error> {
150        serde_json::to_string(self)
151    }
152
153    /// Render the complete lowered instruction stream as readable JSON.
154    ///
155    /// This is an inspection format, not a stable persistence protocol.
156    ///
157    /// # Errors
158    ///
159    /// Propagates JSON serialization errors.
160    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
161        serde_json::to_string_pretty(self)
162    }
163}
164
165/// A set of tasks scheduled together. Dependencies, rather than vector order,
166/// define readiness; order only makes inspection deterministic.
167#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
168pub struct Stream {
169    pub tasks: Box<[TaskId]>,
170    /// Declared bit result of a region body. `None` is constant false.
171    pub value: Option<BitId>,
172    /// Tasks whose boundary bindings the region exports.
173    pub bindings: Box<[TaskId]>,
174}
175
176/// One schedulable unit.
177#[derive(Debug, Clone, PartialEq, serde::Serialize)]
178pub struct Task {
179    /// Stable inspection label derived from the IR scope, node id, and role.
180    pub label: String,
181    /// Absolute earliest start time.
182    pub release: f64,
183    /// Source role, present only on independently released roots.
184    pub source: Option<SourceRole>,
185    /// All predecessors, including Bloq value, order, and quantum edges and
186    /// sequencing added for physical qubit reuse.
187    pub dependencies: Box<[TaskId]>,
188    /// Optional classical gate. A false gate suppresses physical work and
189    /// decoder requests, clears bindings, and writes `false` to `output`.
190    pub activation: Option<BitId>,
191    /// Classical result register, when this task produces one.
192    pub output: Option<BitId>,
193    /// Physical support retained for scheduling and wait events.
194    pub qubits: Box<[QubitId]>,
195    /// Nominal active duration. Dynamic instructions may extend it.
196    pub duration: f64,
197    pub instruction: Instruction,
198}
199
200/// Why an independently released task exists.
201#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
202#[serde(rename_all = "snake_case")]
203pub enum SourceRole {
204    Factory,
205    LogicalInput,
206    PreparedY,
207    Clifford,
208}
209
210/// Stable schedule summary for one independently released source.
211#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
212pub struct SourceSchedule {
213    pub task: TaskId,
214    pub role: SourceRole,
215    pub release: f64,
216    pub duration: f64,
217}
218
219/// Epoch from which decoder latency is measured.
220#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
221pub enum DecodeTiming {
222    Measurement,
223    FactoryCompletion,
224}
225
226/// One decoder query against an already-lowered `Observable`.
227///
228/// Corrected and flip outputs share one sampled decision for a request.
229/// The shared payload keeps their query fields aligned. The runtime caches
230/// decisions by observable task and attempt epoch, and rejects inconsistent
231/// inputs for that causal key.
232#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
233pub struct DecodeRequest {
234    /// Boolean result of this causal solve.
235    pub output: bloq_ir::ObservableOutput,
236    /// Task id of the `Observable` this estimates.
237    pub observable: TaskId,
238    /// The observable's program-level index.
239    pub index: u32,
240    /// Register holding the raw measured parity.
241    pub raw: BitId,
242    /// Duration of one physical memory round, the unit of decoder latency.
243    pub round_duration: f64,
244    /// Epoch decoder latency is measured from.
245    pub timing: DecodeTiming,
246}
247
248/// VM instruction set. Structured bodies refer to the shared task arena.
249#[derive(Debug, Clone, PartialEq, serde::Serialize)]
250pub enum Instruction {
251    Quantum(QuantumTask),
252    Eval(BoolOp),
253    Accumulate(RecordParity),
254    Bind(Box<[BoundaryBinding]>),
255    /// Shared bit parity and boundary bindings; only a complete Observable reads them.
256    ReadoutRecipe {
257        bits: Box<[BitId]>,
258        bindings: Box<[TaskId]>,
259    },
260    Observable {
261        index: u32,
262        bits: Box<[BitId]>,
263        bindings: Box<[TaskId]>,
264    },
265    /// Publish one observable's corrected parity or decoder flip.
266    Decode(DecodeRequest),
267    Discard(BoolOp),
268    /// Repeat a source-isolated body without rolling back global state or time.
269    Rus {
270        body: Stream,
271        restart: BoolOp,
272        owned_qubits: Box<[QubitId]>,
273        attempt_bits: Box<[BitId]>,
274        attempt_records: Box<[RecordId]>,
275        resource: ResourceId,
276        /// Optional explicit preparation before attempts after the first. Compiled
277        /// factories replay authored body resets and leave this empty.
278        retry_prepare: QuantumStream,
279        /// Exact terminal seam cycles used while attempt decoders are pending.
280        decoder_hold: Box<[MemoryCycle]>,
281        /// Physical cultivation exits after which GAP rounds begin.
282        cultivation_exits: Box<[TaskId]>,
283    },
284    /// Hold a live patch until every task in `until` is ready. Execute as many
285    /// whole syndrome rounds as fit, then idle only for a sub-round remainder.
286    WaitFor {
287        /// Sibling producer exits whose active work must finish.
288        until: Box<[TaskId]>,
289        memory: Box<[MemoryCycle]>,
290    },
291    SignalReady {
292        resource: ResourceId,
293    },
294    /// Statically requested memory rounds. The stream already contains the
295    /// exact full `rounds` circuit and executes once; `rounds` is inspection
296    /// metadata.
297    MemoryRounds {
298        rounds: u32,
299        stream: QuantumStream,
300        detectors: Box<[RecordParity]>,
301    },
302    /// A physical hold shorter than one available syndrome round.
303    Idle {
304        duration: f64,
305        noise_ratio: f64,
306        operations: Box<[QuantumOp]>,
307    },
308}
309
310/// Boolean bytecode with absolute register operands, or argument slots within a
311/// shared [`BoolOp::Call`] body.
312///
313/// Evaluators must load every
314/// listed operand, including unselected `Select` arms and duplicate parity
315/// inputs.
316#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
317pub enum BoolOp {
318    Const(bool),
319    Copy(BitId),
320    Not(Box<BoolOp>),
321    Parity {
322        inputs: Box<[BitId]>,
323        constant: bool,
324    },
325    Xor(Box<[BoolOp]>),
326    And(Box<[BoolOp]>),
327    Or(Box<[BoolOp]>),
328    Select {
329        condition: Box<BoolOp>,
330        when_false: Box<BoolOp>,
331        when_true: Box<BoolOp>,
332    },
333    /// Evaluate a shared pure body. Copies in `body` name positions in `inputs`;
334    /// the invocation binds those positions to registers in its enclosing scope.
335    /// Every argument is required, including arguments unused by the body.
336    /// Values and timestamps are evaluated independently per call.
337    Call {
338        body: std::sync::Arc<BoolOp>,
339        inputs: Box<[BitId]>,
340    },
341}
342
343/// Affine measurement parity. `constant` is the expected-sign term.
344#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
345pub struct RecordParity {
346    pub records: std::sync::Arc<[RecordId]>,
347    pub constant: bool,
348}
349
350/// A conditional quantum stage is lowered to its finite executable choices.
351#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)]
352pub struct QuantumTask {
353    pub alternatives: Box<[QuantumAlternative]>,
354}
355
356#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)]
357pub struct QuantumAlternative {
358    /// Exact input assignment selecting this alternative.
359    pub when: Box<[(BitId, bool)]>,
360    pub stream: QuantumStream,
361    /// Records produced by this selected membership.
362    pub selected_records: Box<[RecordId]>,
363    /// Records deliberately unavailable under this selected membership.
364    pub excluded_records: Box<[RecordId]>,
365    pub detectors: Box<[RecordParity]>,
366    pub restarts: Box<[RecordParity]>,
367}
368
369#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)]
370pub struct QuantumStream {
371    pub moments: Box<[Moment]>,
372}
373
374#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)]
375pub struct Moment {
376    pub kind: Option<MomentKind>,
377    pub duration: f64,
378    pub operations: Box<[QuantumOp]>,
379}
380
381/// Physical moment class used by deterministic lane alignment.
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
383pub enum MomentKind {
384    Reset,
385    Rotation,
386    Interaction,
387    Measurement,
388}
389
390/// One dynamically repeatable memory cycle on a seam. Boundary flows carry
391/// the last-round evidence needed to compose detectors across extra cycles.
392#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)]
393pub struct MemoryCycle {
394    pub stream: QuantumStream,
395    pub round_duration: f64,
396    pub detectors: Box<[RecordParity]>,
397    pub boundary_flows: Box<[BoundaryFlow]>,
398    /// Zero-cycle values for the padding frontier, derived from the exact
399    /// source-to-padding detector rows materialized by IR seam composition.
400    pub initializers: Box<[FrontierInitializer]>,
401}
402
403/// Set `record` to `parity` before a dynamic wait executes its first cycle.
404/// A cycle overwrites the same frontier record with its measured value.
405#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
406pub struct FrontierInitializer {
407    pub record: RecordId,
408    pub parity: RecordParity,
409}
410
411#[derive(Debug, Clone, PartialEq, serde::Serialize)]
412pub struct BoundaryFlow {
413    pub start: PauliProduct,
414    pub end: PauliProduct,
415    pub parity: RecordParity,
416    /// Record overwritten by the next cycle for this open frontier, when the
417    /// authored flow carries a single measurement.
418    pub frontier_record: Option<RecordId>,
419}
420
421/// Runtime boundary operator. `input` faces are declarative source endpoints;
422/// `output` faces are measured when their observable closes.
423#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
424pub struct BoundaryBinding {
425    pub input: bool,
426    pub operator: PauliProduct,
427}
428
429#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
430pub enum Pauli {
431    X,
432    Y,
433    Z,
434}
435
436/// Hermitian Pauli product. `negative` represents the real sign; ±i products
437/// are rejected during lowering.
438#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
439pub struct PauliProduct {
440    pub negative: bool,
441    pub terms: Box<[(QubitId, Pauli)]>,
442}
443
444/// Named single-qubit Clifford gates. Paulis and resets have dedicated ops.
445#[expect(
446    non_camel_case_types,
447    reason = "names match the source gate vocabulary"
448)]
449#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
450pub enum Clifford1 {
451    H,
452    H_XY,
453    H_YZ,
454    H_NXY,
455    H_NXZ,
456    H_NYZ,
457    SQRT_X,
458    SQRT_X_DAG,
459    SQRT_Y,
460    SQRT_Y_DAG,
461    S,
462    S_DAG,
463    C_XYZ,
464    C_ZYX,
465    C_NXYZ,
466    C_XNYZ,
467    C_XYNZ,
468    C_NZYX,
469    C_ZNYX,
470    C_ZYNX,
471}
472
473#[derive(Debug, Clone, PartialEq, serde::Serialize)]
474pub enum QuantumOp {
475    Gate1 {
476        gate: Clifford1,
477        qubit: QubitId,
478    },
479    Gate2 {
480        control_basis: Pauli,
481        target_basis: Pauli,
482        control: QubitId,
483        target: QubitId,
484    },
485    Pauli {
486        basis: Pauli,
487        qubit: QubitId,
488    },
489    T {
490        basis: Pauli,
491        qubit: QubitId,
492        adjoint: bool,
493    },
494    Measure {
495        observable: PauliProduct,
496        /// Aliased instance records all receive the same outcome.
497        records: Box<[RecordId]>,
498        flip_probability: f64,
499    },
500    Reset {
501        basis: Pauli,
502        qubit: QubitId,
503    },
504    ConditionalPauli {
505        basis: Pauli,
506        qubit: QubitId,
507        control: RecordId,
508    },
509    Depolarize1 {
510        probability: f64,
511        qubits: Box<[QubitId]>,
512    },
513    Depolarize2 {
514        probability: f64,
515        pairs: Box<[(QubitId, QubitId)]>,
516    },
517    PauliError {
518        probability: f64,
519        basis: Pauli,
520        qubits: Box<[QubitId]>,
521    },
522}
523
524#[cfg(test)]
525mod tests {
526    use serde_json::json;
527
528    use super::*;
529
530    #[test]
531    fn program_json_exposes_physical_stream_and_schedule_metadata() {
532        let quantum = Task {
533            label: "source".into(),
534            release: 2.0,
535            source: Some(SourceRole::Clifford),
536            dependencies: Box::new([]),
537            activation: None,
538            output: None,
539            qubits: Box::new([0]),
540            duration: 1.0,
541            instruction: Instruction::Quantum(QuantumTask {
542                alternatives: Box::new([QuantumAlternative {
543                    stream: QuantumStream {
544                        moments: Box::new([Moment {
545                            kind: Some(MomentKind::Rotation),
546                            duration: 1.0,
547                            operations: Box::new([QuantumOp::Gate1 {
548                                gate: Clifford1::H,
549                                qubit: 0,
550                            }]),
551                        }]),
552                    },
553                    ..QuantumAlternative::default()
554                }]),
555            }),
556        };
557        let dependent = Task {
558            label: "ready".into(),
559            release: 0.0,
560            source: None,
561            dependencies: Box::new([0]),
562            activation: None,
563            output: None,
564            qubits: Box::new([]),
565            duration: 0.0,
566            instruction: Instruction::SignalReady { resource: 0 },
567        };
568        let decode = Task {
569            label: "decode".into(),
570            release: 0.0,
571            source: None,
572            dependencies: Box::new([0]),
573            activation: None,
574            output: Some(0),
575            qubits: Box::new([]),
576            duration: 0.0,
577            instruction: Instruction::Decode(DecodeRequest {
578                output: bloq_ir::ObservableOutput::Corrected,
579                observable: 1,
580                index: 0,
581                raw: 1,
582                round_duration: 1.5,
583                timing: DecodeTiming::Measurement,
584            }),
585        };
586        let product = PauliProduct {
587            negative: false,
588            terms: Box::new([(0, Pauli::Z)]),
589        };
590        let program = Program {
591            qubit_count: 1,
592            bit_count: 2,
593            tasks: vec![quantum, dependent, decode],
594            task_origins: vec![
595                TaskOrigin {
596                    function: TaskFunction::Cube,
597                    sites: Box::new([[3, 4]]),
598                    members: Box::new([[3, 4, 5]]),
599                },
600                TaskOrigin::default(),
601                TaskOrigin::default(),
602            ],
603            entry: Stream {
604                tasks: Box::new([0, 1, 2]),
605                ..Stream::default()
606            },
607            inputs: vec![LogicalInput {
608                port: [3, 4, 5],
609                task: 0,
610                data_qubits: Box::new([0]),
611                stabilizers: Box::new([]),
612                x: product.clone(),
613                z: product.clone(),
614            }],
615            outputs: vec![LogicalOutput {
616                port: [3, 4, 6],
617                x: product.clone(),
618                z: product,
619                frame_x: 0,
620                frame_z: 1,
621            }],
622            ..Program::default()
623        };
624
625        let value: serde_json::Value = serde_json::from_str(&program.to_json().unwrap()).unwrap();
626        assert_eq!(value["entry"]["tasks"], json!([0, 1, 2]));
627        assert_eq!(value["tasks"][0]["source"], "clifford");
628        assert_eq!(value["tasks"][1]["dependencies"], json!([0]));
629        assert_eq!(
630            value["tasks"][0]["instruction"]["Quantum"]["alternatives"][0]["stream"]["moments"][0]
631                ["operations"][0]["Gate1"],
632            json!({ "gate": "H", "qubit": 0 })
633        );
634        // `DecodeRequest` is a newtype payload, so its fields stay flat under
635        // the variant tag exactly as the old struct variant serialized them.
636        assert_eq!(
637            value["tasks"][2]["instruction"]["Decode"],
638            json!({
639                "output": "Corrected",
640                "observable": 1,
641                "index": 0,
642                "raw": 1,
643                "round_duration": 1.5,
644                "timing": "Measurement",
645            })
646        );
647        assert_eq!(value["task_origins"][0]["function"], "cube");
648        assert_eq!(value["inputs"][0]["task"], 0);
649        assert_eq!(value["outputs"][0]["port"], json!([3, 4, 6]));
650    }
651}