Skip to main content

quantrs2_circuit/qasm/
exporter.rs

1//! Export `QuantRS2` circuits to `OpenQASM` 3.0 format
2
3use super::ast::{
4    ClassicalRef, Declaration, Expression, GateDefinition, Literal, Measurement, QasmGate,
5    QasmProgram, QasmRegister, QasmStatement, QubitRef,
6};
7use crate::builder::Circuit;
8use quantrs2_core::synthesis::decompose_single_qubit_zyz;
9use quantrs2_core::{gate::GateOp, qubit::QubitId};
10use scirs2_core::ndarray::Array2;
11use scirs2_core::Complex64;
12use std::collections::{HashMap, HashSet};
13use std::fmt::Write;
14use std::sync::Arc;
15use thiserror::Error;
16
17/// Export error types
18#[derive(Debug, Error)]
19pub enum ExportError {
20    #[error("Unsupported gate: {0}")]
21    UnsupportedGate(String),
22
23    #[error("Invalid circuit: {0}")]
24    InvalidCircuit(String),
25
26    #[error("Formatting error: {0}")]
27    FormattingError(#[from] std::fmt::Error),
28
29    #[error("Gate parameter error: {0}")]
30    ParameterError(String),
31
32    /// A custom gate could not be turned into a QASM `gate` definition because
33    /// no decomposition is available for it with the information at hand.
34    ///
35    /// This is an honest failure: returning it prevents the gate from being
36    /// silently dropped from the exported program.
37    #[error("Cannot decompose custom gate '{gate}' for QASM export: {reason}")]
38    UndecomposableGate { gate: String, reason: String },
39}
40
41/// Options for controlling QASM export
42#[derive(Debug, Clone)]
43pub struct ExportOptions {
44    /// Include standard gate library
45    pub include_stdgates: bool,
46    /// Use gate decomposition for non-standard gates
47    pub decompose_custom: bool,
48    /// Add comments with gate matrix representations
49    pub include_gate_comments: bool,
50    /// Optimize gate sequences
51    pub optimize: bool,
52    /// Pretty print with indentation
53    pub pretty_print: bool,
54}
55
56impl Default for ExportOptions {
57    fn default() -> Self {
58        Self {
59            include_stdgates: true,
60            decompose_custom: true,
61            include_gate_comments: false,
62            optimize: false,
63            pretty_print: true,
64        }
65    }
66}
67
68/// QASM exporter
69pub struct QasmExporter {
70    options: ExportOptions,
71    /// Track which gates need custom definitions
72    custom_gates: HashMap<String, GateInfo>,
73    /// Track qubit usage
74    qubit_usage: HashSet<usize>,
75    /// Track if measurements are used
76    needs_classical_bits: bool,
77}
78
79#[derive(Clone)]
80struct GateInfo {
81    name: String,
82    num_qubits: usize,
83    num_params: usize,
84    matrix: Option<scirs2_core::ndarray::Array2<Complex64>>,
85}
86
87impl QasmExporter {
88    /// Create a new exporter with options
89    #[must_use]
90    pub fn new(options: ExportOptions) -> Self {
91        Self {
92            options,
93            custom_gates: HashMap::new(),
94            qubit_usage: HashSet::new(),
95            needs_classical_bits: false,
96        }
97    }
98
99    /// Export a circuit to QASM 3.0
100    pub fn export<const N: usize>(&mut self, circuit: &Circuit<N>) -> Result<String, ExportError> {
101        // Analyze circuit
102        self.analyze_circuit(circuit)?;
103
104        // Generate QASM program
105        let program = self.generate_program(circuit)?;
106
107        // Convert to string
108        Ok(program.to_string())
109    }
110
111    /// Analyze circuit to determine requirements
112    fn analyze_circuit<const N: usize>(&mut self, circuit: &Circuit<N>) -> Result<(), ExportError> {
113        self.qubit_usage.clear();
114        self.custom_gates.clear();
115        self.needs_classical_bits = false;
116
117        // Analyze each gate
118        for gate in circuit.gates() {
119            // Track qubit usage
120            for qubit in gate.qubits() {
121                self.qubit_usage.insert(qubit.id() as usize);
122            }
123
124            // Check if gate is standard or custom
125            if !self.is_standard_gate(gate.as_ref()) {
126                self.register_custom_gate(gate.as_ref())?;
127            }
128
129            // Check for measurements
130            if gate.name().contains("measure") {
131                self.needs_classical_bits = true;
132            }
133        }
134
135        Ok(())
136    }
137
138    /// Check if a gate is in the standard library
139    fn is_standard_gate(&self, gate: &dyn GateOp) -> bool {
140        let name = gate.name();
141        matches!(
142            name,
143            "I" | "X"
144                | "Y"
145                | "Z"
146                | "H"
147                | "S"
148                | "S†"
149                | "Sdg"
150                | "T"
151                | "T†"
152                | "Tdg"
153                | "√X"
154                | "√X†"
155                | "SX"
156                | "SXdg"
157                | "RX"
158                | "RY"
159                | "RZ"
160                | "P"
161                | "Phase"
162                | "U"
163                | "U1"
164                | "U2"
165                | "U3"
166                | "CX"
167                | "CNOT"
168                | "CY"
169                | "CZ"
170                | "CH"
171                | "CRX"
172                | "CRY"
173                | "CRZ"
174                | "CPhase"
175                | "SWAP"
176                | "iSWAP"
177                | "ECR"
178                | "DCX"
179                | "RXX"
180                | "RYY"
181                | "RZZ"
182                | "RZX"
183                | "CU"
184                | "CCX"
185                | "Toffoli"
186                | "Fredkin"
187                | "measure"
188                | "reset"
189                | "barrier"
190        )
191    }
192
193    /// Register a custom gate
194    fn register_custom_gate(&mut self, gate: &dyn GateOp) -> Result<(), ExportError> {
195        let name = self.gate_qasm_name(gate);
196
197        if !self.custom_gates.contains_key(&name) {
198            let num_qubits = gate.qubits().len();
199            let matrix = Self::gate_matrix(gate, num_qubits);
200
201            let info = GateInfo {
202                name: name.clone(),
203                num_qubits,
204                num_params: self.count_gate_params(gate),
205                matrix,
206            };
207
208            self.custom_gates.insert(name, info);
209        }
210
211        Ok(())
212    }
213
214    /// Retrieve the unitary matrix of a gate as a square `Array2`.
215    ///
216    /// `GateOp::matrix` returns a flat row-major `Vec<Complex64>` of length
217    /// `(2^num_qubits)^2`. Returns `None` when the gate cannot produce a matrix
218    /// or the data does not form a square matrix of the expected dimension.
219    fn gate_matrix(gate: &dyn GateOp, num_qubits: usize) -> Option<Array2<Complex64>> {
220        let flat = gate.matrix().ok()?;
221        let dim = 1usize.checked_shl(num_qubits as u32)?;
222        if flat.len() != dim.checked_mul(dim)? {
223            return None;
224        }
225        Array2::from_shape_vec((dim, dim), flat).ok()
226    }
227
228    /// Get QASM name for a gate
229    fn gate_qasm_name(&self, gate: &dyn GateOp) -> String {
230        let name = gate.name();
231        match name {
232            "I" => "id".to_string(),
233            "X" => "x".to_string(),
234            "Y" => "y".to_string(),
235            "Z" => "z".to_string(),
236            "H" => "h".to_string(),
237            "S" | "S†" => "s".to_string(),
238            "Sdg" => "sdg".to_string(),
239            "T" => "t".to_string(),
240            "T†" | "Tdg" => "tdg".to_string(),
241            "√X" | "SX" => "sx".to_string(),
242            "√X†" | "SXdg" => "sxdg".to_string(),
243            "RX" => "rx".to_string(),
244            "RY" => "ry".to_string(),
245            "RZ" => "rz".to_string(),
246            "P" | "Phase" => "p".to_string(),
247            "U" => "u".to_string(),
248            "CX" | "CNOT" => "cx".to_string(),
249            "CY" => "cy".to_string(),
250            "CZ" => "cz".to_string(),
251            "CH" => "ch".to_string(),
252            "CRX" => "crx".to_string(),
253            "CRY" => "cry".to_string(),
254            "CRZ" => "crz".to_string(),
255            "CPhase" => "cp".to_string(),
256            "SWAP" => "swap".to_string(),
257            "iSWAP" => "iswap".to_string(),
258            "ECR" => "ecr".to_string(),
259            "DCX" => "dcx".to_string(),
260            "RXX" => "rxx".to_string(),
261            "RYY" => "ryy".to_string(),
262            "RZZ" => "rzz".to_string(),
263            "RZX" => "rzx".to_string(),
264            "CCX" | "Toffoli" => "ccx".to_string(),
265            "Fredkin" => "cswap".to_string(),
266            _ => name.to_lowercase(),
267        }
268    }
269
270    /// Count gate parameters
271    fn count_gate_params(&self, gate: &dyn GateOp) -> usize {
272        // This is a simplified version - would need gate trait extension
273        let name = gate.name();
274        match name {
275            "RX" | "RY" | "RZ" | "P" | "Phase" | "U1" => 1,
276            "U2" => 2,
277            "U" | "U3" => 3,
278            "CRX" | "CRY" | "CRZ" | "CPhase" => 1,
279            "RXX" | "RYY" | "RZZ" | "RZX" => 1,
280            _ => 0,
281        }
282    }
283
284    /// Generate QASM program
285    fn generate_program<const N: usize>(
286        &self,
287        circuit: &Circuit<N>,
288    ) -> Result<QasmProgram, ExportError> {
289        let mut declarations = Vec::new();
290        let mut statements = Vec::new();
291
292        // Calculate required register size
293        let max_qubit = self.qubit_usage.iter().max().copied().unwrap_or(0);
294        let num_qubits = max_qubit + 1;
295
296        // Add quantum register
297        declarations.push(Declaration::QuantumRegister(QasmRegister {
298            name: "q".to_string(),
299            size: num_qubits,
300        }));
301
302        // Add classical register if needed
303        if self.needs_classical_bits {
304            declarations.push(Declaration::ClassicalRegister(QasmRegister {
305                name: "c".to_string(),
306                size: num_qubits,
307            }));
308        }
309
310        // Add custom gate definitions
311        if self.options.decompose_custom {
312            for gate_info in self.custom_gates.values() {
313                if let Some(def) = self.generate_gate_definition(gate_info)? {
314                    declarations.push(Declaration::GateDefinition(def));
315                }
316            }
317        }
318
319        // Convert gates to statements
320        for gate in circuit.gates() {
321            statements.push(self.convert_gate(gate)?);
322        }
323
324        // Build includes
325        let includes = if self.options.include_stdgates {
326            vec!["stdgates.inc".to_string()]
327        } else {
328            vec![]
329        };
330
331        Ok(QasmProgram {
332            version: "3.0".to_string(),
333            includes,
334            declarations,
335            statements,
336        })
337    }
338
339    /// Generate a QASM `gate` definition for a custom (non-standard) gate.
340    ///
341    /// Returns `Ok(Some(def))` with a body expressed in standard-library gates
342    /// when the custom gate can be synthesized. Returns `Ok(None)` only when no
343    /// definition is required (there is nothing to emit for this entry).
344    /// Returns `Err(ExportError::UndecomposableGate)` when a definition *is*
345    /// required but cannot be produced from the available information — this is
346    /// an honest error so the gate is never silently dropped from the output.
347    fn generate_gate_definition(
348        &self,
349        gate_info: &GateInfo,
350    ) -> Result<Option<GateDefinition>, ExportError> {
351        // A parameterized custom gate would need a symbolic body in terms of its
352        // parameters. We only captured a single concrete matrix instance, which
353        // cannot represent the gate for arbitrary parameter values, so emitting a
354        // body from it would be a fabrication. Fail honestly instead.
355        if gate_info.num_params > 0 {
356            return Err(ExportError::UndecomposableGate {
357                gate: gate_info.name.clone(),
358                reason: format!(
359                    "parameterized custom gate with {} parameter(s); symbolic decomposition is not supported",
360                    gate_info.num_params
361                ),
362            });
363        }
364
365        match gate_info.num_qubits {
366            // Defensive: a gate acting on zero qubits has no meaningful body.
367            0 => Err(ExportError::UndecomposableGate {
368                gate: gate_info.name.clone(),
369                reason: "gate acts on zero qubits".to_string(),
370            }),
371            1 => self.single_qubit_gate_definition(gate_info),
372            n => Err(ExportError::UndecomposableGate {
373                gate: gate_info.name.clone(),
374                reason: format!(
375                    "no decomposition available for {n}-qubit custom gate (only single-qubit synthesis is implemented)"
376                ),
377            }),
378        }
379    }
380
381    /// Synthesize a single-qubit custom gate into an `rz · ry · rz` body using a
382    /// ZYZ Euler decomposition of its unitary matrix.
383    fn single_qubit_gate_definition(
384        &self,
385        gate_info: &GateInfo,
386    ) -> Result<Option<GateDefinition>, ExportError> {
387        let matrix = gate_info
388            .matrix
389            .as_ref()
390            .ok_or_else(|| ExportError::UndecomposableGate {
391                gate: gate_info.name.clone(),
392                reason: "matrix representation unavailable".to_string(),
393            })?;
394
395        let decomp = decompose_single_qubit_zyz(&matrix.view()).map_err(|e| {
396            ExportError::UndecomposableGate {
397                gate: gate_info.name.clone(),
398                reason: format!("ZYZ decomposition failed: {e}"),
399            }
400        })?;
401
402        // U = e^{i·global_phase} · Rz(theta2) · Ry(phi) · Rz(theta1)
403        //
404        // QASM statements execute in source order (first statement applied
405        // first, i.e. rightmost in the matrix product), so the matrix product
406        // Rz(θ₂)·Ry(φ)·Rz(θ₁) is emitted as rz(θ₁); ry(φ); rz(θ₂);.
407        //
408        // The scalar global phase is physically unobservable for a stand-alone
409        // gate and is intentionally not emitted (QASM `gate` bodies have no
410        // portable way to express it). The synthesized operator therefore equals
411        // the original up to global phase, which defines an equivalent gate.
412        let qubit_arg = "qb".to_string();
413
414        let make_rotation = |name: &str, angle: f64| -> QasmStatement {
415            QasmStatement::Gate(QasmGate {
416                name: name.to_string(),
417                params: vec![Expression::Literal(Literal::Float(angle))],
418                qubits: vec![QubitRef::Register(qubit_arg.clone())],
419                control: None,
420                inverse: false,
421                power: None,
422            })
423        };
424
425        let body = vec![
426            make_rotation("rz", decomp.theta1),
427            make_rotation("ry", decomp.phi),
428            make_rotation("rz", decomp.theta2),
429        ];
430
431        Ok(Some(GateDefinition {
432            name: gate_info.name.clone(),
433            params: Vec::new(),
434            qubits: vec![qubit_arg],
435            body,
436        }))
437    }
438
439    /// Convert gate to QASM statement
440    fn convert_gate(
441        &self,
442        gate: &Arc<dyn GateOp + Send + Sync>,
443    ) -> Result<QasmStatement, ExportError> {
444        let gate_name = gate.name();
445
446        match gate_name {
447            "measure" => {
448                // Convert measurement
449                let qubits: Vec<QubitRef> = gate
450                    .qubits()
451                    .iter()
452                    .map(|q| QubitRef::Single {
453                        register: "q".to_string(),
454                        index: q.id() as usize,
455                    })
456                    .collect();
457
458                let targets: Vec<ClassicalRef> = gate
459                    .qubits()
460                    .iter()
461                    .map(|q| ClassicalRef::Single {
462                        register: "c".to_string(),
463                        index: q.id() as usize,
464                    })
465                    .collect();
466
467                Ok(QasmStatement::Measure(Measurement { qubits, targets }))
468            }
469            "reset" => {
470                let qubits: Vec<QubitRef> = gate
471                    .qubits()
472                    .iter()
473                    .map(|q| QubitRef::Single {
474                        register: "q".to_string(),
475                        index: q.id() as usize,
476                    })
477                    .collect();
478
479                Ok(QasmStatement::Reset(qubits))
480            }
481            "barrier" => {
482                let qubits: Vec<QubitRef> = gate
483                    .qubits()
484                    .iter()
485                    .map(|q| QubitRef::Single {
486                        register: "q".to_string(),
487                        index: q.id() as usize,
488                    })
489                    .collect();
490
491                Ok(QasmStatement::Barrier(qubits))
492            }
493            _ => {
494                // Regular gate
495                let name = self.gate_qasm_name(gate.as_ref());
496
497                let qubits: Vec<QubitRef> = gate
498                    .qubits()
499                    .iter()
500                    .map(|q| QubitRef::Single {
501                        register: "q".to_string(),
502                        index: q.id() as usize,
503                    })
504                    .collect();
505
506                // Extract parameters - this is simplified
507                let params = self.extract_gate_params(gate.as_ref())?;
508
509                Ok(QasmStatement::Gate(QasmGate {
510                    name,
511                    params,
512                    qubits,
513                    control: None,
514                    inverse: false,
515                    power: None,
516                }))
517            }
518        }
519    }
520
521    /// Extract gate parameters as expressions
522    fn extract_gate_params(&self, gate: &dyn GateOp) -> Result<Vec<Expression>, ExportError> {
523        use quantrs2_core::gate::multi::{CRX, CRY, CRZ};
524        use quantrs2_core::gate::single::{RotationX, RotationY, RotationZ};
525        use std::any::Any;
526
527        let any_gate = gate.as_any();
528
529        // Single-qubit rotation gates
530        if let Some(rx) = any_gate.downcast_ref::<RotationX>() {
531            return Ok(vec![Expression::Literal(Literal::Float(rx.theta))]);
532        }
533        if let Some(ry) = any_gate.downcast_ref::<RotationY>() {
534            return Ok(vec![Expression::Literal(Literal::Float(ry.theta))]);
535        }
536        if let Some(rz) = any_gate.downcast_ref::<RotationZ>() {
537            return Ok(vec![Expression::Literal(Literal::Float(rz.theta))]);
538        }
539
540        // Controlled rotation gates
541        if let Some(crx) = any_gate.downcast_ref::<CRX>() {
542            return Ok(vec![Expression::Literal(Literal::Float(crx.theta))]);
543        }
544        if let Some(cry) = any_gate.downcast_ref::<CRY>() {
545            return Ok(vec![Expression::Literal(Literal::Float(cry.theta))]);
546        }
547        if let Some(crz) = any_gate.downcast_ref::<CRZ>() {
548            return Ok(vec![Expression::Literal(Literal::Float(crz.theta))]);
549        }
550
551        // No parameters for other gates
552        Ok(vec![])
553    }
554}
555
556/// Export a circuit to QASM 3.0 with default options
557pub fn export_qasm3<const N: usize>(circuit: &Circuit<N>) -> Result<String, ExportError> {
558    let mut exporter = QasmExporter::new(ExportOptions::default());
559    exporter.export(circuit)
560}
561
562#[cfg(test)]
563mod tests {
564    use super::*;
565    use quantrs2_core::error::QuantRS2Result;
566    use quantrs2_core::gate::multi::CNOT;
567    use quantrs2_core::gate::single::{Hadamard, PauliX};
568    use quantrs2_core::qubit::QubitId;
569    use std::any::Any;
570
571    /// A non-standard single-qubit gate whose unitary is the Hadamard matrix.
572    /// Its name is not in the standard library, so the exporter must synthesize
573    /// a real `gate` definition for it rather than dropping it.
574    #[derive(Debug, Clone)]
575    struct CustomHadamard {
576        target: QubitId,
577    }
578
579    impl GateOp for CustomHadamard {
580        fn name(&self) -> &'static str {
581            "myhad"
582        }
583        fn qubits(&self) -> Vec<QubitId> {
584            vec![self.target]
585        }
586        fn matrix(&self) -> QuantRS2Result<Vec<Complex64>> {
587            let s = 1.0 / 2.0_f64.sqrt();
588            Ok(vec![
589                Complex64::new(s, 0.0),
590                Complex64::new(s, 0.0),
591                Complex64::new(s, 0.0),
592                Complex64::new(-s, 0.0),
593            ])
594        }
595        fn as_any(&self) -> &dyn Any {
596            self
597        }
598        fn clone_gate(&self) -> Box<dyn GateOp> {
599            Box::new(self.clone())
600        }
601    }
602
603    /// A non-standard two-qubit gate (identity on two qubits). Two-qubit custom
604    /// synthesis is out of scope, so the exporter must fail honestly instead of
605    /// silently dropping it.
606    #[derive(Debug, Clone)]
607    struct CustomTwoQubit {
608        a: QubitId,
609        b: QubitId,
610    }
611
612    impl GateOp for CustomTwoQubit {
613        fn name(&self) -> &'static str {
614            "mytwo"
615        }
616        fn qubits(&self) -> Vec<QubitId> {
617            vec![self.a, self.b]
618        }
619        fn matrix(&self) -> QuantRS2Result<Vec<Complex64>> {
620            let mut m = vec![Complex64::new(0.0, 0.0); 16];
621            for i in 0..4 {
622                m[i * 4 + i] = Complex64::new(1.0, 0.0);
623            }
624            Ok(m)
625        }
626        fn as_any(&self) -> &dyn Any {
627            self
628        }
629        fn clone_gate(&self) -> Box<dyn GateOp> {
630            Box::new(self.clone())
631        }
632    }
633
634    #[test]
635    fn test_export_custom_single_qubit_gate_emits_definition() {
636        let mut circuit = Circuit::<1>::new();
637        circuit
638            .add_gate(CustomHadamard { target: QubitId(0) })
639            .expect("adding custom gate should succeed");
640
641        let qasm = export_qasm3(&circuit)
642            .expect("export should synthesize a definition for a single-qubit custom gate");
643
644        // The custom gate must produce a real, non-empty `gate` definition body
645        // expressed in standard rotations, and the gate must be referenced.
646        assert!(
647            qasm.contains("gate myhad"),
648            "missing custom gate definition: {qasm}"
649        );
650        assert!(qasm.contains("rz("), "definition body missing rz: {qasm}");
651        assert!(qasm.contains("ry("), "definition body missing ry: {qasm}");
652        assert!(
653            qasm.contains("myhad qb") || qasm.contains("myhad q[0]"),
654            "custom gate not applied: {qasm}"
655        );
656    }
657
658    #[test]
659    fn test_export_custom_multi_qubit_gate_errors_honestly() {
660        let mut circuit = Circuit::<2>::new();
661        circuit
662            .add_gate(CustomTwoQubit {
663                a: QubitId(0),
664                b: QubitId(1),
665            })
666            .expect("adding custom two-qubit gate should succeed");
667
668        let result = export_qasm3(&circuit);
669        // Must NOT silently drop the gate: an honest error is required.
670        assert!(
671            matches!(result, Err(ExportError::UndecomposableGate { ref gate, .. }) if gate == "mytwo"),
672            "expected UndecomposableGate error for two-qubit custom gate, got: {result:?}"
673        );
674    }
675
676    #[test]
677    fn test_custom_gate_definition_is_skipped_when_disabled() {
678        // With decompose_custom = false, no definition is emitted (and no error
679        // is raised) — the option legitimately suppresses definitions.
680        let mut circuit = Circuit::<2>::new();
681        circuit
682            .add_gate(CustomTwoQubit {
683                a: QubitId(0),
684                b: QubitId(1),
685            })
686            .expect("adding custom two-qubit gate should succeed");
687
688        let options = ExportOptions {
689            decompose_custom: false,
690            ..ExportOptions::default()
691        };
692        let mut exporter = QasmExporter::new(options);
693        let qasm = exporter
694            .export(&circuit)
695            .expect("export without decomposition should not error");
696        assert!(
697            !qasm.contains("gate mytwo"),
698            "unexpected definition: {qasm}"
699        );
700    }
701
702    #[test]
703    fn test_export_simple_circuit() {
704        let mut circuit = Circuit::<2>::new();
705        circuit
706            .add_gate(Hadamard { target: QubitId(0) })
707            .expect("adding Hadamard gate should succeed");
708        circuit
709            .add_gate(CNOT {
710                control: QubitId(0),
711                target: QubitId(1),
712            })
713            .expect("adding CNOT gate should succeed");
714
715        let result = export_qasm3(&circuit);
716        assert!(result.is_ok());
717
718        let qasm = result.expect("export_qasm3 should succeed for valid circuit");
719        assert!(qasm.contains("OPENQASM 3.0"));
720        assert!(qasm.contains("qubit[2] q"));
721        assert!(qasm.contains("h q[0]"));
722        assert!(qasm.contains("cx q[0], q[1]"));
723    }
724
725    #[test]
726    fn test_export_with_measurements() {
727        let mut circuit = Circuit::<2>::new();
728        circuit
729            .add_gate(Hadamard { target: QubitId(0) })
730            .expect("adding Hadamard gate should succeed");
731        // Note: measure gate would need to be implemented
732
733        let result = export_qasm3(&circuit);
734        assert!(result.is_ok());
735
736        let qasm = result.expect("export_qasm3 should succeed for measurement test");
737        // Basic check
738        assert!(qasm.contains("OPENQASM 3.0"));
739    }
740}