fix: emit OpenQASM 2 syntax for classical conditionals - #338
Conversation
Failing tests for the qasm2 serializer emitting OpenQASM 3 syntax for
classical conditionals:
- braced if-blocks instead of `if (creg == int) <statement>`;
- after `unroll()`, the per-bit chain the unroller ravels a register
comparison into (`if (m[0] == true) { if (m[1] == false) { ... } }`),
which additionally uses creg indexing and a `true` literal that
OpenQASM 2 has no syntax for;
- a body writing to the register under test, which cannot be expanded
into one guarded statement per body statement without re-evaluating
the condition mid-body.
`Qasm2Module._qasm_ast_to_str` serialized with the stock openqasm3 printer and patched the result with regexes covering only declarations, so branching statements were emitted verbatim in OpenQASM 3 form under an `OPENQASM 2.0;` header. Downstream QASM 2 parsers reject the result. Adds `Qasm2Printer`, an `openqasm3.printer.Printer` subclass overriding `visit_BranchingStatement` to emit `if (creg == int) <statement>`: - a body that unrolled into several statements becomes one guarded statement each, QASM 2 having no braced blocks; - the nested per-bit chain `unroll()` ravels a register comparison into is walked back into that comparison, inverting the unroller's MSB-first bit ordering so `if(m==2)` round-trips as `if (m == 2)`; - branches QASM 2 cannot express — `else` blocks, nested `if`s, branches spanning registers, partial-register conditions (reachable via `>=` and `<=` unrolling) — raise a `ValidationError`. A multi-statement body is guarded per statement, which re-tests the register before each one. That is only faithful while the body leaves the register alone, so a body assigning to the register under test is rejected rather than silently given new semantics. Output verified against qiskit's OpenQASM 2 parser.
Argus reviewAuto-review is off for this repo. Tick the box below to run a review on this PR. Running Argus review... Estimated cost
Tip: you can also comment |
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🔎 Argus · 9/10 — Cleanly ships OpenQASM 2 classical conditional syntax with solid coverage
🔍 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: Emit valid OpenQASM 2 classical conditional syntax (if (creg == int) ) instead of OpenQASM 3 braced if-blocks.
Not in scope:
- Fix if(m==1) barrier q; input-validation gap (handled separately in #339)
Stated acceptance criteria (from PR/issue — not independently verified): - Branching emits brace-free if (creg == int) QASM 2 syntax
- Unrolled multibit nested per-bit chains collapse back to whole-register comparison
- Multi-statement bodies expand to one guarded conditional per statement
- else blocks, nested ifs, cross-register and partial-register conditions raise errors
- Multi-statement body writing the tested register raises ValidationError
- Emitted programs parse with qiskit.qasm2.loads
- Tests cover issue repro, body expansion, multibit unroll collapse, statement types, and write hazard
✅ Intent delivered
Verdict: This PR correctly emits brace-free if (creg == int) <statement> QASM 2 syntax, collapses multibit unrolls, expands multi-statement bodies, and guards the stated error cases. Ready to merge; the two warnings are minor polish.
🟡 2 P1 · 3 files reviewed
Architecture: Serialization and validation paths stay well separated; the write-hazard check and collapse logic keep semantic risk contained without overreaching scope.
Simulation Results
Tested 1 scenarios, 1 potential issues found:
Scenario: src/pyqasm/modules/qasm2.py: Conditional-body filtering blacklists only barrier instead of whitelisting the three statement kinds the QASM 2 grammar allows as an if body
Verdict: Broken (82% sure)
Why: If-body filter only bans barrier instead of allowing gate, measure, reset.
Fix: Whitelist only QuantumGate, Measure, and Reset as QASM2 if bodies.
Minor notes (1)
Low-confidence or minor observations — safe to ignore.
tests/qasm2/test_branching.py:L50[suggestion] Could we add one skip-guarded test that feeds the emitted program to qiskit.qasm2.loads, so the dialect contract is validated by a real QASM 2 parser and not only by string comparison?
2 findings · 2 inline · 0 folded
🔢 120.7k tokens · $0.9119 total
| Stage | Tokens | Cost |
|---|---|---|
| Intent | 4.7k | $0.0133 |
| Triage | 3.7k | $0.0087 |
| Lead agent | 1.9k | $0.0070 |
| Review · security | 63.7k | $0.5181 |
| Review | 31.1k | $0.3137 |
| Acceptance | 2.3k | $0.0087 |
| Simulation | 9.6k | $0.0322 |
| Scoring | 2.4k | $0.0066 |
| Synthesis | 1.3k | $0.0036 |
Contract: production/full · checked: bug_hunter, security, architecture, regression · review took 17m40s
Dashboard → · React 👎 to dismiss · Reply to any inline comment or use @argus-eye help to chat
Review feedback on #338. The chain walk in _flatten_branch collapses nested branches into one comparison, but nothing checks that the constraints it collects can coexist. Three shapes are silently reduced to whichever constraint is walked last, emitting a valid-looking QASM 2 program that triggers on different values than the source: if(m==1) if(m==0) x q[0]; -> if (m == 0) x q[0]; if(m[0]==1) if(m[0]==0) x q[0]; -> if (m == 0) x q[0]; if(m[0]==2) x q[0]; -> if (m == 1) x q[0]; The first drops the outer condition entirely, so the PR's claim that nested ifs raise is false for a chain of them. Also adds coverage for the four rejection paths the PR does introduce but never exercised -- else blocks, cross-register nesting, partial-register conditions and non-equality operators -- which passed already and now stay pinned.
Review feedback on #338. The chain walk collected each nested branch's constraint but never checked they could coexist, so three shapes were silently reduced to whichever constraint happened to be walked last and emitted as a valid-looking QASM 2 program triggering on different values: if(m==1) if(m==0) x q[0]; -> if (m == 0) x q[0]; if(m[0]==1) if(m[0]==0) x q[0]; -> if (m == 0) x q[0]; if(m[0]==2) x q[0]; -> if (m == 1) x q[0]; The first dropped the outer condition outright, so the guard that was supposed to reject nested ifs never fired for a chain of them -- the walk consumed the nesting before the check could see it. Each is now rejected where the constraint is folded in. The legitimate case the collapse exists for, a per-bit chain from unrolling, is unaffected: those carry distinct bit indices and boolean values. Constraint folding is extracted to _add_chain_constraint to keep _flatten_branch within the branch limit.
Summary of changes
Closes #337.
Qasm2Module._qasm_ast_to_strserialized with the stockopenqasm3printer and then patched the result with regexes that only cover declarations (qubit[n] q→qreg q[n]). Branching statements went through the OpenQASM 3 printer untouched, so a valid QASM 2 conditional round-tripped as a braced block under anOPENQASM 2.0;header:Confirmed rejected by a real QASM 2 parser:
The unrolled path was worse
The issue's repro does not call
unroll(). Once it does,unroll()ravels a register comparison into a nested chain of per-bit tests:That is invalid QASM 2 three times over — braces, creg indexing in the condition, and a
trueliteral. Since the reported impact path is the qBraid SDK normalization pass, which unrolls, fixing only the braces would have left it broken.Fix
Qasm2Printer, anopenqasm3.printer.Printersubclass overridingvisit_BranchingStatementto emit QASM 2'sif (creg == int) <statement>:if(m==2)round-trips asif (m == 2).elseblocks, nestedifs, branches spanning registers, partial-register conditions (reachable through>=/<=unrolling), non-equality operators, and a bit compared against a non-bit value.The chain walk also has to reject constraints that cannot coexist in the single comparison QASM 2 allows. Without that, three shapes collapsed to whichever constraint was walked last and emitted a valid-looking program triggering on different values than the source —
if(m==1) if(m==0) x q[0];becameif (m == 0) x q[0];, dropping the outer condition outright. Caught in review; see 65984ac and 14858df.One semantic hazard the per-statement expansion introduces is guarded rather than shipped: guarding each statement re-tests the register before every one, which is only faithful while the body leaves that register alone.
if(m==1) measure q -> m;would silently change meaning, so a multi-statement body assigning to the register under test raises aValidationErrorexplaining why.Tests
Written first and committed failing (278f394), then made to pass by the fix (5b27fb1).
test_branch_emits_qasm2_syntax— the issue repro, checked against the full expected program.test_branch_body_expands_to_one_conditional_per_statement— a broadcast body becomes one guarded statement per qubit.test_multibit_branch_survives_unrolling— parametrized over every value of a 2-bit register, asserting the per-bit chain collapses back to the original comparison.test_branch_body_statement_types— gate,reset, andmeasurebodies each emit brace-free.test_branch_body_writing_tested_register_raises— the semantic hazard above is rejected.test_conflicting_chain_constraints_raise— the three shapes that previously collapsed to the wrong condition.test_inexpressible_branches_raise— one case per rejection path:else, cross-register nesting, partial-register conditions, non-equality operators.Every emitted program in the cases above was additionally verified to parse with
qiskit.qasm2.loads.Related bug, not fixed here
if(m==1) barrier q;is accepted on input but is not valid QASM 2 —barrieris not a<qop>, so it cannot be a conditional body. That is an input-validation gap rather than a serializer gap, so it is fixed separately in #339.