Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Types of changes:
## Unreleased

### Added
- Added support for `#pragma` statements, which previously raised `ValidationError: Unsupported statement of type <class 'openqasm3.ast.Pragma'>` 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
Expand Down
21 changes: 20 additions & 1 deletion src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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.
42 changes: 40 additions & 2 deletions src/pyqasm/modules/qasm3.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,50 @@
Defines a module for handling OpenQASM 3.0 programs.
"""

from openqasm3.ast import Program
from openqasm3.printer import dumps
import io
from typing import Any

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:
"""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: 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()
Comment on lines +28 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Complete the new API documentation and annotations.

Qasm3Printer.visit_Pragma() has no docstring. dumps() does not document node, kwargs, or its return value in the required format. Annotate kwargs as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pyqasm/modules/qasm3.py` around lines 27 - 45, Complete the API
documentation for Qasm3Printer.visit_Pragma and dumps: add a concise docstring
to visit_Pragma, document dumps’ node and kwargs parameters and its return value
in the project’s required format, and annotate kwargs with the appropriate type
while preserving the existing behavior.

Source: Coding guidelines



class Qasm3Module(QasmModule):
"""
A module representing an openqasm3 quantum program.
Expand Down
2 changes: 2 additions & 0 deletions src/pyqasm/printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[L1] This branch is load-bearing but untested — Implementation · Low

Rationale: without this continue, the pragma reaches the moment builder and hits raise ValueError(f"Unsupported statement: {statement}")Pragma subclasses neither Statement nor QuantumStatement, so it is not filtered downstream. Correct fix, but there is no pragma case anywhere in tests/visualization/, so a regression here would surface as a hard error for anyone drawing a Braket verbatim program.

Change requested: add one mpl_draw test over a program containing a pragma.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 456e03btest_draw_qasm3_pragma added to tests/visualization/test_mpl_draw.py.

Verified it is load-bearing: deleting the continue makes it fail with ValueError: Unsupported statement: Pragma(...), exactly as you described.

One thing worth knowing about the motivation: a Braket verbatim program cannot be drawn today regardless of this branch — Box has no moment-builder case either, so mpl_draw on any boxed program raises ValueError: Unsupported statement: Box(...), on main as well. So the test uses a bare pragma with plain gates to isolate the branch you flagged. Happy to file the Box drawing gap separately if you want it tracked.

else:
statements.append(s)

Expand Down
73 changes: 66 additions & 7 deletions src/pyqasm/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1257,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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[M1] Verbatim gate with a decomposition rule reports decomposed depth — Implementation · Medium (pre-existing; follow-up, not a blocker)

Rationale: #pragma braket verbatim box { crz(0.5) q[0], q[1]; } emits exactly one instruction but reports depth == 12 — the depth of the decomposition that was deliberately not performed. (verbatim box { rx; cz } → 2 and verbatim box { ccx } → 1 are both correct, so this is specific to gates that have a decomposition rule.)

Root cause: _visit_external_gate_operation calls self._visit_basic_gate_operation(operation) "just for validation" at src/pyqasm/visitor.py:1351, and that call still runs _update_qubit_depth_for_gate. The _recording_ext_gate_depth suppression this line extends guards only the custom gate path, not the basic-gate path.

This is pre-existing — on main, unroll(external_gates=['crz']) also reports depth=12 while emitting one crz — so it is not a blocker here. It is worth flagging because this PR makes the defect reachable without the user opting in per gate name: any Braket verbatim program containing a decomposable gate now silently reports an inflated depth.

Change requested: file a follow-up against _visit_external_gate_operation (suppress depth recording around the validation-only call) and link it here. The "Not fixed here" section already sets the precedent.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up filed as #352 and linked from the description. No code change here, per your call that it is not worth growing the diff.

Reproduced both halves first: verbatim box { crz(0.5) q[0], q[1]; } reports depth=12 while emitting one statement, and unroll(external_gates=['crz']) reports the same 12 on main. verbatim box { rx; cz } → 2 and verbatim box { ccx } → 1, so it is specific to gates with a decomposition rule as you said. #352 points at the validation-only _visit_basic_gate_operation call and the _recording_ext_gate_depth asymmetry.

The description now carries a "Known gap" section instead of leaving it unmentioned.


result = []
for gate_op in gate_definition_ops:
Expand Down Expand Up @@ -1577,7 +1584,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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[M2] Custom gate in a verbatim box emits output that does not round-trip — Design / Maintenance · Medium (pre-existing in kind)

Rationale: this branch makes every gate take the external path when inside a verbatim box, user-defined ones included. Given gate mygate a { h a; } and a verbatim box containing mygate q[0];, the emitted program keeps the mygate q[0]; call while unrolling drops the gate mygate definition. Feeding that output back through pyqasm fails:

ValidationError: Unsupported / undeclared QASM operation: mygate

Pre-existing in kind — the same non-round-tripping output reproduces on main via unroll(external_gates=['mygate']). The difference that matters: external_gates is an explicit per-name opt-in, whereas _in_verbatim_box makes every gate external implicitly, including user-defined ones a device cannot accept as native instructions. Braket verbatim boxes exist specifically for native hardware gates, so this combination is meaningless in the first place.

Change requested: either reject a non-native, non-basis custom gate inside a verbatim box with a clear error, or document that verbatim bodies must contain only device-native gates. At minimum a line in the docs added by this PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 456e03b — took the documentation option.

Confirmed the round-trip failure first: gate mygate a { h a; } plus a verbatim box containing mygate q[0]; emits the call while unrolling drops the definition, and feeding that back gives ValidationError: Unsupported / undeclared QASM operation: mygate.

src/README.md now has a Pragmas section stating that a verbatim box should contain only device-native gates, that pyqasm does not enforce it, and what the output does if you ignore that. Went with documenting rather than rejecting since the combination is meaningless rather than dangerous, and rejecting would make pyqasm the arbiter of what a device considers native.

result.extend(self._visit_external_gate_operation(operation, inverse_value, ctrls))
Comment on lines +1587 to 1588

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Count a custom verbatim gate as one emitted gate.

When a custom gate is inside a verbatim box, _visit_external_gate_operation() validates it through _visit_custom_gate_operation(). That method sets _recording_ext_gate_depth only for configured external gates. It therefore records the decomposed gate body, although this path emits the original custom gate.

Set the depth-recording state for verbatim custom gates too. Preserve and restore the previous state for nested custom gates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pyqasm/visitor.py` around lines 1586 - 1587, Update the verbatim
custom-gate path in the visitor branch using _in_verbatim_box and
_visit_external_gate_operation so _recording_ext_gate_depth is set while
validating and emitting the original custom gate, not its decomposed body.
Preserve the prior depth-recording state and restore it after the nested
custom-gate operation, including nested verbatim custom gates.

elif operation.name.name in self._custom_gates:
result.extend(self._visit_custom_gate_operation(operation, inverse_value, ctrls))
Expand Down Expand Up @@ -2934,6 +2941,42 @@ 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.

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"]
Comment on lines +2944 to +2954

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add parameter and return documentation.

_is_verbatim_pragma() has type annotations, but its docstring does not describe statement or the boolean result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pyqasm/visitor.py` around lines 2943 - 2946, Update the
_is_verbatim_pragma() docstring to document the statement parameter and the
boolean return value, while preserving its existing behavior and type
annotations.

Source: Coding guidelines


def _visit_pragma(self, statement: qasm3_ast.Pragma) -> list[qasm3_ast.Pragma]:

@TheGupta2012 TheGupta2012 Aug 5, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[M3] Qubit-referencing pragmas silently desync from renumbering passes — Implementation · Medium

Rationale: pass-through means no transform can rewrite pragma text, so a pragma that names qubits by index goes stale as soon as a renumbering pass runs. Two verified cases:

qubit[4] q;
#pragma braket noise bit_flip(0.1) q[3]
h q[3];

After unroll() + remove_idle_qubits() the register shrinks to qubit[1] q and the gate is rewritten to h q[0], but the pragma still reads q[3] — now pointing outside the declared register. Symmetrically, reverse_qubit_order() rewrites h q[0]h q[3] while the pragma stays on q[0], so the noise channel lands on a different qubit than the gate it was written for. Relatedly, #pragma braket noise bit_flip(0.1) q[9] against qubit[1] q passes validate() cleanly.

This is inherent to the pass-through design, and parsing vendor pragma grammar is not the suggestion. It is newly reachable, though — before this PR such programs were rejected outright — and it fails silently, producing a program that is valid QASM and wrong. Note this is distinct from the box recursion gap that #345 fixes: no amount of body-walking fixes opaque text.

Change requested: document in the docs added here that pragma bodies are opaque and are not rewritten by qubit-renumbering passes (remove_idle_qubits, reverse_qubit_order, consolidate_qubits), so pragmas referencing qubits by index may go stale. A warning when a renumbering pass runs on a module carrying pragmas would be a reasonable follow-up, not required here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 456e03b — documented in the same Pragmas section.

Verified both directions on this branch:

remove_idle_qubits() : qubit[4] q -> qubit[1] q, h q[3] -> h q[0], pragma still q[3]
reverse_qubit_order(): h q[3] -> h q[0],          pragma still q[3]

The README now records that remove_idle_qubits(), reverse_qubit_order() and unroll(consolidate_qubits=True) renumber the program but not pragma text, that an index-referencing pragma can therefore land on the wrong qubit or outside the register, and that nothing raises because the output is still valid QASM. Left the warning-on-renumbering idea as a follow-up, per your note that it is not required here.

"""
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.
Comment on lines +2960 to +2962

@TheGupta2012 TheGupta2012 Aug 5, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[L2] Docstring the honour-vs-forward asymmetry — Maintenance · Low

Rationale: #pragma braket verbatim followed by something other than a box is a no-op for pyqasm, but it is still copied to the output, where Braket would apply it to whatever follows. That is defensible given "the marker is consumed by the next statement", but the split between what pyqasm honours and what it merely forwards is currently implicit.

Change requested:

Suggested change
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.
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.

Separately, in this file: the module-level dumps added in modules/qasm3.py shadows both openqasm3.printer.dumps and the public pyqasm.dumps (different signature). The shadowing is contained to that one file, so this is readability only — worth a glance, not a change request.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 456e03b — suggestion applied verbatim to the _visit_pragma docstring.

Left the dumps shadowing in modules/qasm3.py alone, per your note that it is readability only and not a change request.

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.
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.
Expand All @@ -2943,6 +2986,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:
Expand Down Expand Up @@ -2976,6 +3022,9 @@ 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
Comment thread
argus-eye[bot] marked this conversation as resolved.
# 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:
Expand Down Expand Up @@ -3265,18 +3314,26 @@ 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
"""
logger.debug("Visiting statement '%s'", str(statement))
result = []

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))
if visitor_function:
if isinstance(statement, qasm3_ast.ExpressionStatement):
Expand All @@ -3293,11 +3350,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.
Expand Down
Loading
Loading