From 89e28937130a0f6ce5da7c7f748958b418e59734 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Tue, 4 Aug 2026 12:12:17 -0500 Subject: [PATCH 1/4] feat: support #pragma statements, preserve braket verbatim boxes Pragmas were rejected outright (`Unsupported statement of type openqasm3.ast.Pragma`), which blocked verbatim execution on Braket QPUs since `#pragma braket verbatim` is the only way to pin physical qubits. Pragmas now pass through loads/validate/unroll/dumps unchanged, and a `braket verbatim` pragma marks the box that follows it so its gates are emitted as written instead of decomposed. --- CHANGELOG.md | 1 + src/README.md | 2 +- src/pyqasm/modules/qasm3.py | 27 +++++- src/pyqasm/printer.py | 2 + src/pyqasm/visitor.py | 55 ++++++++++- tests/qasm3/test_pragma.py | 185 ++++++++++++++++++++++++++++++++++++ 6 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 tests/qasm3/test_pragma.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fc340e88..0a9af1d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Types of changes: ## Unreleased ### Added +- Added support for `#pragma` statements, which previously raised `ValidationError: Unsupported statement of type ` and blocked any program carrying one. Pragmas are now passed through `loads`/`validate`/`unroll`/`dumps` unchanged, and are printed in their `#pragma` form (the upstream printer emits the bare `pragma` keyword). A `#pragma braket verbatim` additionally marks the `box` that immediately follows it: gates inside a verbatim box are emitted as written instead of being decomposed, so verbatim submissions to Braket QPUs keep the native gates they were built with. ([#341](https://github.com/qBraid/pyqasm/pull/341)) - Added support for the `c3x` (3-controlled X) and `rc3x`/`rcccx` (relative-phase 3-controlled X) gates, decomposed into basis gates following qiskit's `C3XGate`/`RC3XGate` definitions. Also extended the `ctrl @` modifier chain so that 3- and 4-control stacks on `x` (e.g. `ctrl @ ctrl @ ctrl @ x`, `ctrl(4) @ x`) resolve to `c3x`/`c4x`. ([#320](https://github.com/qBraid/pyqasm/pull/320)) ### Improved / Modified diff --git a/src/README.md b/src/README.md index ebe831be..acc38b1d 100644 --- a/src/README.md +++ b/src/README.md @@ -30,7 +30,7 @@ Source code for OpenQASM 3 program validator and semantic analyzer | QuantumGateModifier (ctrl) | ✅ | Completed | | WhileLoop | ✅ | Completed | | IODeclaration | 📋 | Planned | -| Pragma | 📋 | Planned | +| Pragma | ✅ | Preserved as-is | | Annotation | 📋 | Planned | | DurationType | ✅ | Completed | | StretchType | ✅ | Completed | diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index 8ed08d51..1b5bd60a 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -16,12 +16,35 @@ Defines a module for handling OpenQASM 3.0 programs. """ -from openqasm3.ast import Program -from openqasm3.printer import dumps +import io + +from openqasm3.ast import Pragma, Program, QASMNode +from openqasm3.printer import Printer, PrinterState from pyqasm.modules.base import QasmModule +class Qasm3Printer(Printer): + """OpenQASM 3 printer that writes pragmas in their '#pragma' form. + + The upstream printer emits the bare 'pragma' keyword. Both forms parse, but tools + consuming the output (e.g. Amazon Braket for '#pragma braket verbatim') expect the + hashed form, which is also what they emit. + """ + + def visit_Pragma(self, node: Pragma, context: PrinterState) -> None: + self._start_line(context) + self.stream.write(f"#pragma {node.command}") + self._end_line(context) + + +def dumps(node: QASMNode, **kwargs) -> str: + """Return the OpenQASM 3 string representation of ``node``.""" + out = io.StringIO() + Qasm3Printer(out, **kwargs).visit(node) + return out.getvalue() + + class Qasm3Module(QasmModule): """ A module representing an openqasm3 quantum program. diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py index eabeee66..5567ab16 100644 --- a/src/pyqasm/printer.py +++ b/src/pyqasm/printer.py @@ -134,6 +134,8 @@ def mpl_draw( # pylint: disable=too-many-locals for s in program._statements: if isinstance(s, ast.QuantumPhase): global_phase += Qasm3ExprEvaluator.evaluate_expression(s.argument)[0] + elif isinstance(s, ast.Pragma): + continue # pragmas carry no timing information else: statements.append(s) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index fd4d0dd6..07e2a138 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -143,6 +143,11 @@ def __init__( # pylint: disable=too-many-arguments # timeline within the box. Delays on disjoint qubits run in parallel, # so box durations are validated against the per-qubit maximum. self._box_delay_frames: list[dict[tuple[str, int], float]] = [] + # A 'braket verbatim' pragma applies to the box that immediately follows it. + # Gates inside such a box are emitted as written so that the device receives + # the exact native instructions the user asked for. + self._verbatim_pragma_pending: bool = False + self._in_verbatim_box: bool = False self._in_extern_function: bool = False self._openpulse_qubit_map: dict[str, set[str]] = {} self._total_pulse_qubits: int = 0 @@ -188,6 +193,7 @@ def _construct_visit_map(self): qasm3_ast.ContinueStatement: self._visit_continue, qasm3_ast.DelayInstruction: self._visit_delay_statement, qasm3_ast.Box: self._visit_box_statement, + qasm3_ast.Pragma: self._visit_pragma, qasm3_ast.CalibrationDefinition: self._visit_calibration_definition, qasm3_ast.CalibrationStatement: self._visit_calibration_statement, qasm3_ast.CalibrationGrammarDeclaration: self._visit_calibration_grammar_declaration, @@ -1577,7 +1583,7 @@ def _visit_generic_gate_operation( # pylint: disable=too-many-branches, too-man for _ in range(power_value): if isinstance(operation, qasm3_ast.QuantumPhase): result.extend(self._visit_phase_operation(operation, inverse_value, ctrls)) - elif operation.name.name in self._external_gates: + elif self._in_verbatim_box or operation.name.name in self._external_gates: result.extend(self._visit_external_gate_operation(operation, inverse_value, ctrls)) elif operation.name.name in self._custom_gates: result.extend(self._visit_custom_gate_operation(operation, inverse_value, ctrls)) @@ -2934,6 +2940,33 @@ def _visit_delay_statement( return [statement] + @staticmethod + def _is_verbatim_pragma(statement: qasm3_ast.Pragma) -> bool: + """Check whether a pragma marks the following box as verbatim.""" + return statement.command.split() == ["braket", "verbatim"] + + def _visit_pragma(self, statement: qasm3_ast.Pragma) -> list[qasm3_ast.Pragma]: + """ + Visit a Pragma statement. + + Pragmas carry vendor specific directives which pyqasm does not interpret, so they + are passed through unchanged. A 'braket verbatim' pragma additionally marks the box + that follows it, whose gates are then left as written instead of being decomposed. + + Args: + statement (qasm3_ast.Pragma): The Pragma node to visit. + Returns: + list[qasm3_ast.Pragma]: The pragma, unmodified. + """ + logger.debug("Visiting pragma '%s'", statement.command) + if self._is_verbatim_pragma(statement): + self._verbatim_pragma_pending = True + + if self._check_only: + return [] + + return [statement] + def _visit_box_statement(self, statement: qasm3_ast.Box) -> list[qasm3_ast.Statement]: """ Visit a Box statement. @@ -2943,6 +2976,9 @@ def _visit_box_statement(self, statement: qasm3_ast.Box) -> list[qasm3_ast.State list[qasm3_ast.Statement]: The list of statements generated by the Box statement. """ statements = [] + outer_verbatim = self._in_verbatim_box + self._in_verbatim_box = outer_verbatim or self._verbatim_pragma_pending + self._verbatim_pragma_pending = False _box_time_var = statement.duration box_duration_val = 0 if _box_time_var is not None: @@ -2976,6 +3012,7 @@ def _visit_box_statement(self, statement: qasm3_ast.Box) -> list[qasm3_ast.State self._scope_manager.restore_context() self._scope_manager.decrement_scope_level() self._scope_manager.pop_scope() + self._in_verbatim_box = outer_verbatim delay_frame = self._box_delay_frames.pop() if _box_time_var and box_duration_val and delay_frame: @@ -3265,11 +3302,13 @@ def _visit_include(self, include: qasm3_ast.Include) -> list[qasm3_ast.Statement return [include] - def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Statement]: + def visit_statement( + self, statement: qasm3_ast.Statement | qasm3_ast.Pragma + ) -> list[qasm3_ast.Statement]: """Visit a statement element. Args: - statement (qasm3_ast.Statement): The statement to visit. + statement (qasm3_ast.Statement | qasm3_ast.Pragma): The statement to visit. Returns: None @@ -3277,6 +3316,10 @@ def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Stat logger.debug("Visiting statement '%s'", str(statement)) result = [] + if not isinstance(statement, (qasm3_ast.Pragma, qasm3_ast.Box)): + # a pending verbatim pragma only carries over to a box directly following it + self._verbatim_pragma_pending = False + visitor_function = self._visit_map.get(type(statement)) if visitor_function: if isinstance(statement, qasm3_ast.ExpressionStatement): @@ -3293,11 +3336,13 @@ def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Stat ) return result - def visit_basic_block(self, stmt_list: list[qasm3_ast.Statement]) -> list[qasm3_ast.Statement]: + def visit_basic_block( + self, stmt_list: Sequence[qasm3_ast.Statement | qasm3_ast.Pragma] + ) -> list[qasm3_ast.Statement]: """Visit a basic block of statements. Args: - stmt_list (list[qasm3_ast.Statement]): The list of statements to visit. + stmt_list (Sequence[qasm3_ast.Statement | qasm3_ast.Pragma]): The statements to visit. Returns: list[qasm3_ast.Statement]: The list of unrolled statements. diff --git a/tests/qasm3/test_pragma.py b/tests/qasm3/test_pragma.py new file mode 100644 index 00000000..8e3733d1 --- /dev/null +++ b/tests/qasm3/test_pragma.py @@ -0,0 +1,185 @@ +# Copyright 2025 qBraid +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module containing unit tests for Pragma statements. +""" + +import openqasm3.ast as qasm3_ast + +from pyqasm.entrypoint import dumps, loads +from tests.utils import check_unrolled_qasm + + +def test_pragma_is_preserved(): + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + #pragma braket result probability + qubit[2] q; + h q[0]; + """ + expected_qasm = """ + OPENQASM 3.0; + include "stdgates.inc"; + #pragma braket result probability + qubit[2] q; + h q[0]; + """ + module = loads(qasm_str) + module.unroll() + check_unrolled_qasm(dumps(module), expected_qasm) + + +def test_pragma_validation(): + qasm_str = """ + OPENQASM 3.0; + #pragma braket noise bit_flip(0.1) q[0] + qubit[1] q; + """ + module = loads(qasm_str) + module.validate() + assert module.num_qubits == 1 + + +def test_pragma_round_trip(): + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + #pragma braket verbatim + box { + rx(1.5707963267948966) $0; + cz $0, $1; + } + """ + module = loads(qasm_str) + module.unroll() + reloaded = loads(dumps(module)) + reloaded.unroll() + check_unrolled_qasm(dumps(reloaded), dumps(module)) + + +def test_verbatim_box_is_not_decomposed(): + """Gates in a `#pragma braket verbatim` box must reach the device as written.""" + qasm_str = """ + OPENQASM 3.0; + bit[2] c; + #pragma braket verbatim + box { + prx(1.5707963267948966, 4.71238898038469) $1; + cz $2, $1; + } + c[0] = measure $2; + """ + expected_qasm = """ + OPENQASM 3.0; + bit[2] c; + #pragma braket verbatim + box { + prx(1.5707963267948966, 4.71238898038469) $1; + cz $2, $1; + } + c[0] = measure $2; + """ + module = loads(qasm_str) + module.unroll() + check_unrolled_qasm(dumps(module), expected_qasm) + + +def test_nested_box_in_verbatim_box_is_not_decomposed(): + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + #pragma braket verbatim + box { + box { + prx(0.1, 0.2) q[0]; + } + cz q[0], q[1]; + } + """ + expected_qasm = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + #pragma braket verbatim + box { + box { + prx(0.1, 0.2) q[0]; + } + cz q[0], q[1]; + } + """ + module = loads(qasm_str) + module.unroll() + check_unrolled_qasm(dumps(module), expected_qasm) + + +def test_non_verbatim_box_is_decomposed(): + """A pragma that is not `braket verbatim` leaves the following box untouched.""" + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + #pragma braket result probability + box { + prx(0.1, 0.2) q[0]; + } + """ + module = loads(qasm_str) + module.unroll() + box = module.unrolled_ast.statements[-1] + assert isinstance(box, qasm3_ast.Box) + assert [gate.name.name for gate in box.body] == ["rz", "rx", "rz", "rx", "rz"] + + +def test_verbatim_applies_only_to_the_box_that_follows(): + """The verbatim marker is consumed by the next statement, box or not.""" + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + #pragma braket verbatim + h q[0]; + box { + prx(0.1, 0.2) q[0]; + } + """ + module = loads(qasm_str) + module.unroll() + box = module.unrolled_ast.statements[-1] + assert isinstance(box, qasm3_ast.Box) + assert [gate.name.name for gate in box.body] == ["rz", "rx", "rz", "rx", "rz"] + + +def test_verbatim_box_after_verbatim_box(): + """Each verbatim box needs its own pragma.""" + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + #pragma braket verbatim + box { + prx(0.1, 0.2) q[0]; + } + box { + prx(0.1, 0.2) q[0]; + } + """ + module = loads(qasm_str) + module.unroll() + verbatim_box, plain_box = module.unrolled_ast.statements[-2:] + assert [gate.name.name for gate in verbatim_box.body] == ["prx"] + assert [gate.name.name for gate in plain_box.body] == ["rz", "rx", "rz", "rx", "rz"] From 993cd8b250ae5c6d0be1e88e8b6f5417f4a02dee Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Tue, 4 Aug 2026 12:46:19 -0500 Subject: [PATCH 2/4] review: strict verbatim adjacency, count verbatim custom gates once - clear the pending verbatim marker on any non-box statement, so an intervening pragma drops it, matching the documented rule that the pragma must immediately precede the box - record a verbatim custom gate's depth once, as the external-gate path does, instead of counting the decomposed body it does not emit - document the new pragma printer and _is_verbatim_pragma --- src/pyqasm/modules/qasm3.py | 19 +++++++++++++++++-- src/pyqasm/visitor.py | 20 +++++++++++++++----- tests/qasm3/test_pragma.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/pyqasm/modules/qasm3.py b/src/pyqasm/modules/qasm3.py index 1b5bd60a..d440fb29 100644 --- a/src/pyqasm/modules/qasm3.py +++ b/src/pyqasm/modules/qasm3.py @@ -17,6 +17,7 @@ """ import io +from typing import Any from openqasm3.ast import Pragma, Program, QASMNode from openqasm3.printer import Printer, PrinterState @@ -33,13 +34,27 @@ class Qasm3Printer(Printer): """ def visit_Pragma(self, node: Pragma, context: PrinterState) -> None: + """Write a pragma node, keeping the '#' that the upstream printer drops. + + Args: + node (Pragma): The pragma to write. + context (PrinterState): The printer state, carrying the current indent. + """ self._start_line(context) self.stream.write(f"#pragma {node.command}") self._end_line(context) -def dumps(node: QASMNode, **kwargs) -> str: - """Return the OpenQASM 3 string representation of ``node``.""" +def dumps(node: QASMNode, **kwargs: Any) -> str: + """Return the OpenQASM 3 string representation of ``node``. + + Args: + node (QASMNode): The node to print, usually a Program. + **kwargs (Any): Printer options, forwarded to `openqasm3.printer.Printer`. + + Returns: + str: The printed program. + """ out = io.StringIO() Qasm3Printer(out, **kwargs).visit(node) return out.getvalue() diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 07e2a138..d5d48999 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -1263,8 +1263,9 @@ def _visit_custom_gate_operation( self._scope_manager.push_context(Context.GATE) # Pause recording the depth of new gates because we are processing the - # definition of a custom gate here - handle the depth separately afterwards - self._recording_ext_gate_depth = gate_name in self._external_gates + # definition of a custom gate here - handle the depth separately afterwards. + # A verbatim gate is emitted as written, so it counts once, like an external gate. + self._recording_ext_gate_depth = self._in_verbatim_box or gate_name in self._external_gates result = [] for gate_op in gate_definition_ops: @@ -2942,7 +2943,14 @@ def _visit_delay_statement( @staticmethod def _is_verbatim_pragma(statement: qasm3_ast.Pragma) -> bool: - """Check whether a pragma marks the following box as verbatim.""" + """Check whether a pragma marks the following box as verbatim. + + Args: + statement (qasm3_ast.Pragma): The pragma to inspect. + + Returns: + bool: True for a 'braket verbatim' pragma, False for any other. + """ return statement.command.split() == ["braket", "verbatim"] def _visit_pragma(self, statement: qasm3_ast.Pragma) -> list[qasm3_ast.Pragma]: @@ -3316,8 +3324,10 @@ def visit_statement( logger.debug("Visiting statement '%s'", str(statement)) result = [] - if not isinstance(statement, (qasm3_ast.Pragma, qasm3_ast.Box)): - # a pending verbatim pragma only carries over to a box directly following it + if not isinstance(statement, qasm3_ast.Box): + # a pending verbatim pragma only carries over to a box directly following it. + # Clearing before dispatch lets a verbatim pragma re-arm the flag for itself, + # while any other statement - another pragma included - drops it. self._verbatim_pragma_pending = False visitor_function = self._visit_map.get(type(statement)) diff --git a/tests/qasm3/test_pragma.py b/tests/qasm3/test_pragma.py index 8e3733d1..a036d506 100644 --- a/tests/qasm3/test_pragma.py +++ b/tests/qasm3/test_pragma.py @@ -183,3 +183,39 @@ def test_verbatim_box_after_verbatim_box(): verbatim_box, plain_box = module.unrolled_ast.statements[-2:] assert [gate.name.name for gate in verbatim_box.body] == ["prx"] assert [gate.name.name for gate in plain_box.body] == ["rz", "rx", "rz", "rx", "rz"] + + +def test_verbatim_marker_dropped_by_an_intervening_pragma(): + """Only the pragma immediately preceding a box marks that box verbatim.""" + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + #pragma braket verbatim + #pragma braket result probability + box { + prx(0.1, 0.2) q[0]; + } + """ + module = loads(qasm_str) + module.unroll() + box = module.unrolled_ast.statements[-1] + assert isinstance(box, qasm3_ast.Box) + assert [gate.name.name for gate in box.body] == ["rz", "rx", "rz", "rx", "rz"] + + +def test_verbatim_custom_gate_counts_once_towards_depth(): + """A verbatim gate is emitted as written, so its depth is that of one gate.""" + qasm_str = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + gate my_gate(a) p, r { rx(a) p; cx p, r; rx(a) r; } + #pragma braket verbatim + box { + my_gate(0.3) q[0], q[1]; + } + """ + module = loads(qasm_str) + module.unroll() + assert module.depth() == 1 From b3bf10046ba2fa05ee6ba438c9ee4ea2cd2a03e5 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Tue, 4 Aug 2026 16:23:15 -0500 Subject: [PATCH 3/4] review: clear the verbatim marker when a box closes A pragma at the end of a box body left the marker armed, so the next box was emitted verbatim without one of its own. The text parser keeps pragmas global, but loads() also accepts a hand-built program, which reaches the same path. --- src/pyqasm/visitor.py | 2 ++ tests/qasm3/test_pragma.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index d5d48999..c18fda72 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -3021,6 +3021,8 @@ def _visit_box_statement(self, statement: qasm3_ast.Box) -> list[qasm3_ast.State self._scope_manager.decrement_scope_level() self._scope_manager.pop_scope() self._in_verbatim_box = outer_verbatim + # a marker left behind by the body must not reach the next box + self._verbatim_pragma_pending = False delay_frame = self._box_delay_frames.pop() if _box_time_var and box_duration_val and delay_frame: diff --git a/tests/qasm3/test_pragma.py b/tests/qasm3/test_pragma.py index a036d506..b1a57981 100644 --- a/tests/qasm3/test_pragma.py +++ b/tests/qasm3/test_pragma.py @@ -219,3 +219,36 @@ def test_verbatim_custom_gate_counts_once_towards_depth(): module = loads(qasm_str) module.unroll() assert module.depth() == 1 + + +def test_verbatim_marker_does_not_escape_a_box(): + """A pragma at the end of a box body must not mark the next box verbatim. + + The parser keeps pragmas global, so this reaches the visitor through a hand-built + program - which `loads` accepts just the same. + """ + + def prx_gate(): + return qasm3_ast.QuantumGate( + [], + qasm3_ast.Identifier("prx"), + [qasm3_ast.FloatLiteral(0.1), qasm3_ast.FloatLiteral(0.2)], + [ + qasm3_ast.IndexedIdentifier( + qasm3_ast.Identifier("q"), [[qasm3_ast.IntegerLiteral(0)]] + ) + ], + ) + + program = qasm3_ast.Program( + statements=[ + qasm3_ast.QubitDeclaration(qasm3_ast.Identifier("q"), qasm3_ast.IntegerLiteral(1)), + qasm3_ast.Box(duration=None, body=[prx_gate(), qasm3_ast.Pragma("braket verbatim")]), + qasm3_ast.Box(duration=None, body=[prx_gate()]), + ], + version="3.0", + ) + module = loads(program) + module.unroll() + trailing_box = module.unrolled_ast.statements[-1] + assert [gate.name.name for gate in trailing_box.body] == ["rz", "rx", "rz", "rx", "rz"] From 456e03bb8fc729ee9d6db4cd920b79a3ae4dfe66 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Wed, 5 Aug 2026 08:43:38 -0500 Subject: [PATCH 4/4] review: document pragma opacity and verbatim scope, cover the draw path - README gains a Pragmas section: pragma bodies are opaque and not rewritten by qubit-renumbering passes, and verbatim boxes should hold only device-native gates (M2, M3) - _visit_pragma docstring states the honour-vs-forward split for a verbatim pragma not followed by a box (L2) - test_draw_qasm3_pragma covers the Pragma branch in the moment builder; verified to fail when the branch is removed (L1) --- src/README.md | 19 +++++++++++++++++++ src/pyqasm/visitor.py | 2 ++ tests/visualization/test_mpl_draw.py | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/src/README.md b/src/README.md index acc38b1d..fe470714 100644 --- a/src/README.md +++ b/src/README.md @@ -41,3 +41,22 @@ Source code for OpenQASM 3 program validator and semantic analyzer | ComplexType | ✅ | Completed | | AngleType | ✅ | Completed | | ExternDeclaration | ✅ | Completed | + +## Pragmas + +A pragma body is opaque text. pyqasm copies it to the output unchanged and never parses +it, with one exception: `#pragma braket verbatim` marks the `box` that immediately +follows it, and gates inside that box are emitted as written rather than decomposed. + +Two consequences worth knowing before relying on them: + +- **Qubit-renumbering passes do not rewrite pragmas.** `remove_idle_qubits()`, + `reverse_qubit_order()` and `unroll(consolidate_qubits=True)` renumber qubits in the + program but not inside pragma text, so a pragma naming qubits by index — say + `#pragma braket noise bit_flip(0.1) q[3]` — can end up on a different qubit than the + one it was written for, or outside the declared register. The output is still valid + QASM, so nothing raises. +- **A verbatim box should contain only device-native gates.** That is what Braket + verbatim boxes are for, and pyqasm does not enforce it: a user-defined gate inside a + verbatim box is emitted as a call while unrolling drops its `gate` definition, so the + output does not load back into pyqasm. diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index c18fda72..09b05fbd 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -2960,6 +2960,8 @@ def _visit_pragma(self, statement: qasm3_ast.Pragma) -> list[qasm3_ast.Pragma]: Pragmas carry vendor specific directives which pyqasm does not interpret, so they are passed through unchanged. A 'braket verbatim' pragma additionally marks the box that follows it, whose gates are then left as written instead of being decomposed. + A verbatim pragma not followed by a box is honoured by nothing in pyqasm but is + still copied to the output, where the consumer applies it to whatever comes next. Args: statement (qasm3_ast.Pragma): The Pragma node to visit. diff --git a/tests/visualization/test_mpl_draw.py b/tests/visualization/test_mpl_draw.py index bd281f49..1703c3d1 100644 --- a/tests/visualization/test_mpl_draw.py +++ b/tests/visualization/test_mpl_draw.py @@ -59,6 +59,25 @@ def test_draw_qasm3_simple(): _check_fig(circ, fig) +def test_draw_qasm3_pragma(): + """Test drawing a circuit carrying a pragma. Pragma is neither a Statement nor a + QuantumStatement, so nothing downstream filters it out of the moment builder.""" + qasm = """ + OPENQASM 3.0; + include "stdgates.inc"; + + qubit[2] q; + + #pragma braket noise bit_flip(0.1) q[0] + h q[0]; + cx q[0], q[1]; + """ + circ = loads(qasm) + circ.unroll() + fig = mpl_draw(circ) + _check_fig(circ, fig) + + def test_draw_qasm3_custom_gate(): qasm = """ OPENQASM 3.0;