Skip to content

feat: support #pragma statements and preserve braket verbatim boxes - #341

Open
ryanhill1 wants to merge 4 commits into
mainfrom
support-pragma-statements
Open

feat: support #pragma statements and preserve braket verbatim boxes#341
ryanhill1 wants to merge 4 commits into
mainfrom
support-pragma-statements

Conversation

@ryanhill1

@ryanhill1 ryanhill1 commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary of changes

Closes #340.

Problem

Any program containing a #pragma was rejected with ValidationError: Unsupported statement of type <class 'openqasm3.ast.Pragma'>, because ast.Pragma had no entry in the visitor's dispatch map. That blocks verbatim execution on Braket QPUs through the qBraid stack: #pragma braket verbatim is the only remaining mechanism for fixed physical qubits, and pyqasm rejected the program client-side before it could be submitted.

Pragmas now pass through loads/validate/unroll/dumps unchanged.

Verbatim boxes are not decomposed

Pass-through alone is not enough: unrolling still rewrote the gates inside the box, so a verbatim prx(...) $1 came out as an rz/rx/rz/rx/rz chain the device cannot accept. A braket verbatim pragma now marks the box that immediately follows it, and gates inside that box take the existing external-gate path — validated and counted for depth/qubits, but emitted as written. Nested boxes inherit it; the marker is consumed by the next statement, so each verbatim box needs its own pragma.

The verbatim check is a literal match on the pragma command (braket verbatim). Every other pragma is opaque text that pyqasm copies through.

#pragma vs pragma

openqasm3's printer emits the bare pragma keyword. Both forms parse, but Braket emits and documents the hashed form, and the point of this change is round-tripping to Braket — so Qasm3Module now prints via a Printer subclass that writes #pragma. QASM 2 output is untouched (Qasm2Module still rejects pragmas via its statement whitelist).

Limits of pass-through, documented

src/README.md gains a Pragmas section recording two things the design does not give you:

  • Qubit-renumbering passes do not rewrite pragma text. remove_idle_qubits(), reverse_qubit_order() and unroll(consolidate_qubits=True) renumber the program but not the opaque pragma body, so #pragma braket noise bit_flip(0.1) q[3] can end up on a different qubit than it was written for, or outside the register — silently, since the output is still valid QASM. Inherent to pass-through; parsing vendor pragma grammar is not on the table.
  • A verbatim box should contain only device-native gates. pyqasm does not enforce it, and a user-defined gate inside one is emitted as a call while unrolling drops its gate definition, so that output does not load back into pyqasm.

Known gap: depth of verbatim gates

#pragma braket verbatim box { crz(0.5) q[0], q[1]; } emits one instruction but reports depth == 12 — the depth of the decomposition that was deliberately skipped. Pre-existing on the external_gates path (unroll(external_gates=["crz"]) reports the same on main) and tracked in #352; this PR makes it reachable without opting in per gate name. Only gates with a decomposition rule are affected: verbatim box { rx; cz } reports 2 and verbatim box { ccx } reports 1, both correct.

Merge order

Merge after #344 and #345. Two caveats that would otherwise apply here are fixed by them: unroll(consolidate_qubits=True) on a verbatim box with physical qubits raises AttributeError on this branch alone and works merged (#344), and remove_idle_qubits() / reverse_qubit_order() correctly rewrite box bodies once merged (#345). Neither is on the path the issue needs (plain unroll() + dumps()).

Tests

tests/qasm3/test_pragma.py covers round-tripping, #pragma normalisation, verbatim preservation, and the positional scoping in all four arrangements (pragma → gate → box, one pragma with two boxes, nested box inheritance, stacked pragmas). Validation inside a verbatim box is asserted to still reject undefined gates, wrong arity, duplicate operands and out-of-range indices — verbatim is not an escape hatch.

tests/visualization/test_mpl_draw.py::test_draw_qasm3_pragma covers the Pragma branch in the moment builder; Pragma subclasses neither Statement nor QuantumStatement, so without that branch a pragma reaches raise ValueError(f"Unsupported statement: ..."). Verified to fail when the branch is removed.

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.
@ryanhill1
ryanhill1 requested a review from TheGupta2012 as a code owner August 4, 2026 17:12
@argus-eye

argus-eye Bot commented Aug 4, 2026

Copy link
Copy Markdown

Argus review

Auto-review is off for this repo. Tick the box below to run a review on this PR.

Running Argus review...

Estimated cost

  • Files changed: 6
  • Diff lines (±): 272
  • Historical avg: ~161.1k tokens · ~$1.16 · across last 2 review(s)

Tip: you can also comment @argus-eye review at any time.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes support pragma preservation, verbatim box handling, serialization, validation, unrolling, and nested-box behavior required by issue #340.
Out of Scope Changes check ✅ Passed The changes are limited to pragma support, verbatim box handling, documentation, rendering, and focused tests.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary changes: adding pragma support and preserving Braket verbatim boxes.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

codecov-commenter commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/pyqasm/modules/qasm3.py`:
- Around line 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.

In `@src/pyqasm/visitor.py`:
- Around line 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.
- Around line 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.
- Around line 3319-3321: Update the statement check in the visitor logic to
clear _verbatim_pragma_pending for every statement that is not a qasm3_ast.Box,
including intervening Pragma nodes. Add a regression test covering a verbatim
pragma followed by a non-verbatim pragma and then a box, ensuring the box is not
marked verbatim.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c7b5f8a-5695-4e1c-bd5c-252d9072bb5d

📥 Commits

Reviewing files that changed from the base of the PR and between 5dcae69 and 89e2893.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/README.md
  • src/pyqasm/modules/qasm3.py
  • src/pyqasm/printer.py
  • src/pyqasm/visitor.py
  • tests/qasm3/test_pragma.py

Comment on lines +27 to +45
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()

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

Comment thread src/pyqasm/visitor.py
Comment on lines +1586 to 1587
elif self._in_verbatim_box or operation.name.name in self._external_gates:
result.extend(self._visit_external_gate_operation(operation, inverse_value, ctrls))

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.

Comment thread src/pyqasm/visitor.py
Comment on lines +2943 to +2946
@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"]

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

Comment thread src/pyqasm/visitor.py Outdated
Comment on lines +3319 to +3321
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

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 | 🟠 Major | ⚡ Quick win

Clear the marker when any intervening pragma occurs.

The condition excludes every Pragma, so this sequence marks the box as verbatim even though #pragma braket verbatim does not immediately precede it:

`#pragma` braket verbatim
`#pragma` braket result probability
box { prx(0.1, 0.2) q[0]; }

Clear _verbatim_pragma_pending for every non-Box statement. Add a regression test for an intervening non-verbatim pragma.

🤖 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 3319 - 3321, Update the statement check
in the visitor logic to clear _verbatim_pragma_pending for every statement that
is not a qasm3_ast.Box, including intervening Pragma nodes. Add a regression
test covering a verbatim pragma followed by a non-verbatim pragma and then a
box, ensuring the box is not marked verbatim.

- 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
@ryanhill1

Copy link
Copy Markdown
Member Author

Reviewed all four findings; three were valid and are fixed in 993cd8b.

Intervening pragma (major) — valid. The condition excluded every Pragma, so a verbatim marker survived one. It is now cleared for any non-Box statement; because the clear happens before dispatch, a verbatim pragma still re-arms the flag for itself, and anything else — another pragma included — drops it. That matches the documented "the box that immediately follows it" rule. Regression test: test_verbatim_marker_dropped_by_an_intervening_pragma.

Verbatim custom gate depth — valid. Confirmed before the fix: a custom gate in a verbatim box was emitted as my_gate(0.3) q[0], q[1]; but reported depth == 3, the depth of the body it does not emit; the same gate via external_gates=["my_gate"] reported 1. _recording_ext_gate_depth now also covers verbatim gates, so it reports 1. Pinned by test_verbatim_custom_gate_counts_once_towards_depth.

I did not add the preserve/restore of the previous state. The existing set-then-clear protocol already yields exactly one depth update for a nested custom gate (the inner call clears the flag, so the outer block skips); restoring the prior value would make both levels record, double-counting a single emitted gate. Verified: nested verbatim custom gate reports depth == 1, two sibling verbatim gates report 2.

Both docstring findings — valid, added.

One thing deliberately unchanged: a basic gate in a verbatim box still contributes its decomposition's depth (prx + cz reports 6). That is the pre-existing behaviour of the external-gate path — unroll(external_gates=["prx"]) on the same program reports 6 as well — so it is not specific to verbatim and is out of scope here.

@argus-eye argus-eye Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔎 Argus · 9/10 — Adds pragma and Braket verbatim-box support cleanly, with one validation edge case to address

🔍 PR intent vs diff (LLM analysis)

Argus read the diff against the stated intent. This is not an execution log — reviewer still needs to test behavior.

Goal: Support OpenQASM #pragma statements and preserve braket verbatim boxes through parsing, validation, unrolling, and serialization.
Not in scope:

  • Fix remove_idle_qubits() recursion into box bodies.
  • Fix unroll(consolidate_qubits=True) for physical qubits inside boxes.
    Stated acceptance criteria (from PR/issue — not independently verified):
  • Pragmas pass unchanged through loads, validate, unroll, and dumps.
  • #pragma braket verbatim marks the immediately following box as verbatim.
  • Gates inside verbatim boxes are validated and counted but emitted without decomposition.
  • Nested boxes inherit verbatim behavior, while each successive box requires its own pragma.
  • Non-braket pragmas are preserved as opaque text.
  • Qasm3Module prints pragmas with the #pragma form.
  • Qasm2Module continues to reject pragmas.
  • Circuit visualizations ignore pragmas when calculating timing and moments.

✅ Intent delivered

Verdict: This PR preserves pragmas through the QASM3 pipeline and correctly carries Braket verbatim behavior through nested boxes. It is close to merge-ready; resolving the remaining validation edge case would make the behavior more robust.

🟡 1 P1 · 6 files reviewed

Architecture: The parser-to-printer flow is well integrated; we should ensure validation semantics remain consistent for verbatim-box contents.

1 finding · 1 inline · 0 folded

🔢 354.5k tokens · $1.1995 total
Stage Tokens Cost
Intent 3.6k $0.0021
Triage 3.7k $0.0006
Lead agent 3.7k $0.0094
Review · bug_hunter 82.2k $0.3078
Review · security 81.0k $0.2597
Review · architecture 80.6k $0.2872
Review · regression 95.3k $0.3220
Acceptance 1.9k $0.0051
Scoring 1.5k $0.0035
Synthesis 1.0k $0.0020

Contract: production/full · checked: bug_hunter, security, architecture, regression · review took 2m32s

Dashboard → · React 👎 to dismiss · Reply to any inline comment or use @argus-eye help to chat

Comment thread src/pyqasm/visitor.py
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.
@ryanhill1

Copy link
Copy Markdown
Member Author

Valid, and reachable — fixed in b3bf100.

The text parser keeps pragmas global, but loads() also accepts a hand-built ast.Program, and that reaches the same visitor path. Confirmed before the fix: a box whose body ends with a verbatim pragma, followed by a second box with no pragma of its own, emitted prx undecomposed in the second box. _visit_box_statement now clears _verbatim_pragma_pending alongside restoring _in_verbatim_box, and test_verbatim_marker_does_not_escape_a_box builds exactly that program and pins the second box to its decomposed form.

@TheGupta2012 TheGupta2012 left a comment

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.

Approved with comments — well-designed feature, and the hard part (positional scoping) is right. None of the findings block merge; the first two are inherited rather than introduced here.

Findings

All findings are posted inline on the relevant lines. Checklist:

  • M1 — Verbatim gate with a decomposition rule reports decomposed depth (inherited)
  • M2 — Custom gate in a verbatim box emits output that does not round-trip (inherited)
  • M3 — Qubit-referencing pragmas silently desync from renumbering passes
  • L1 — mpl_draw pragma branch is load-bearing but untested
  • L2 — Docstring the honour-vs-forward asymmetry

How it was tested

  • Pragma round-trip through loads/unroll/dumps; bare pragma normalised to #pragma.
  • Verbatim box preserves rx/cz as written (depth=2, num_qubits=2); plain box { crz(0.5) q[0], q[1]; } still decomposes to 12 statements, so no over-reach.
  • Scoping, all four positions: pragma → gate → box leaves the box non-verbatim; one pragma with two boxes marks only the first; a nested box inherits; two stacked pragmas count only the immediately preceding one. #pragma inside a box is rejected by the parser itself (pragmas must be global), so the global-only assumption in visit_statement holds.
  • Validation is preserved inside a verbatim box: undefined gate, wrong arity, duplicate operand and out-of-range index all still raise. Verbatim is not an escape hatch.
  • QASM 2 still rejects pragmas via the statement whitelist.
  • Suite on branch: 647 passed, 4 skipped.
  • Merged worktree of #341 + #344 + #345: 658 passing; all six open PRs merge cleanly (CHANGELOG.md only) at 697 passing.

Next steps

Merge after #344 and #345 — both "Not fixed here" caveats are fixed by them, verified on a merged worktree, so that section can be deleted from the description rather than filed as follow-ups. Specifically: consolidate_qubits=True on a verbatim box with physical qubits raises AttributeError on this branch alone and works merged (#344); remove_idle_qubits()/reverse_qubit_order() correctly rewrite box bodies merged (#345). Then file the M1 follow-up and add the doc lines from M2/M3. L2 is attached as an applicable suggestion.

Comment thread src/pyqasm/visitor.py
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.

Comment thread src/pyqasm/visitor.py
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.

Comment thread src/pyqasm/visitor.py
"""
return statement.command.split() == ["braket", "verbatim"]

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.

Comment thread src/pyqasm/printer.py
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.

Comment thread src/pyqasm/visitor.py
Comment on lines +2960 to +2962
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.

@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.

- 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)
@ryanhill1
ryanhill1 requested a review from TheGupta2012 August 5, 2026 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pyqasm rejects #pragma braket verbatim (Pragma statements), blocking verbatim compilation through qBraid

3 participants