fix: reject barrier as an OpenQASM 2 conditional body - #339
Conversation
The QASM 2 grammar admits only a <qop> as the body of an `if`:
<if> := if ( <id> == <nninteger> ) <qop>
<qop> := <uop> | measure ... | reset ...
`barrier` is a separate production, so `if(m==1) barrier q;` is not a
valid QASM 2 program. pyqasm accepts it and emits it unchanged, which
downstream QASM 2 parsers reject.
Tests assert the rejection, alongside cases pinning down what must keep
validating: every operation that *is* a <qop> in a conditional body, and
an unconditional barrier.
`Qasm2Module._filter_statements` checked only the type of each top-level statement, so nothing inspected what a `BranchingStatement` carried in its body. A conditional barrier passed validation and was serialized verbatim as `if (m == 1) barrier q[0], q[1];`, which QASM 2 parsers reject — barrier is not a <qop>, and only a <qop> may follow an `if`. Filtering now recurses into conditional bodies (both blocks, at any depth) and raises a ValidationError naming the offending statement.
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 rejects barrier in OpenQASM 2 conditionals with solid nested 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: Reject barrier as an OpenQASM 2 conditional body, since the grammar allows only a qop (gate, measure, or reset) there.
Not in scope:
- Independent of #338 (printing/serialization of conditionals)
Stated acceptance criteria (from PR/issue — not independently verified): - Recurse into conditional if_block and else_block at any nesting depth and reject barrier there
- Raise ValidationError via raise_qasm3_error with source span for conditional barrier
- Accept conditional qops: gate, two-qubit gate, reset, measure
- Accept unconditional barriers outside conditionals
- Tests: test_conditional_barrier_rejected, test_conditional_barrier_rejected_when_nested, test_conditional_qop_accepted, test_unconditional_barrier_accepted
✅ Intent delivered
Verdict: This PR correctly enforces the qop-only grammar for OpenQASM 2 conditional bodies, including nested if/else, with clear ValidationError spans and good acceptance tests. Ready to merge; the two warnings are minor polish.
🟡 2 P1 · 3 files reviewed
Architecture: Validation is scoped tightly to conditional bodies without disturbing unconditional barriers or legitimate qops, which keeps the change easy to reason about.
2 findings · 2 inline · 0 folded
🔢 186.1k tokens · $1.2897 total
| Stage | Tokens | Cost |
|---|---|---|
| Intent | 4.1k | $0.0126 |
| Triage | 3.3k | $0.0087 |
| Lead agent | 1.8k | $0.0063 |
| Review · bug_hunter | 31.2k | $0.1673 |
| Review · security | 32.8k | $0.1885 |
| Review · architecture | 31.7k | $0.2841 |
| Review · regression | 58.9k | $0.4644 |
| Review | 18.7k | $0.1467 |
| Scoring | 2.4k | $0.0072 |
| Synthesis | 1.4k | $0.0039 |
Contract: production/full · checked: bug_hunter, security, architecture, regression · review took 14m4s
Dashboard → · React 👎 to dismiss · Reply to any inline comment or use @argus-eye help to chat
Review feedback on #339. `test_conditional_barrier_rejected_when_nested` claimed to exercise the recursive descent but its program had no nesting -- it duplicated the `barrier q;` parametrized case, so dropping the recursion left every test green. It now nests (`if(m==1) if(m==0) barrier q;`), verified to fail when the recursion is removed. Adds failing tests for the wider hole behind that one: filtering blacklists `barrier` alone, so any other non-qop body the parser accepts validates cleanly and is emitted as invalid QASM 2. `delay` and `box` bodies both do this today -- neither has any QASM 2 syntax, and `delay` is even rejected at the top level by the existing whitelist.
Review feedback on #339. Filtering blacklisted `barrier` alone, so every other non-qop body the parser accepts still validated and was emitted verbatim. `delay` and `box` bodies both did this -- neither has any QASM 2 syntax, and `delay` is already rejected by the top-level whitelist, so a conditional was the one place it could slip through. Conditional bodies are now checked against the <qop> production itself (QuantumGate, QuantumMeasurementStatement, QuantumReset), with branching statements recursed into as before. Barrier keeps its specific message; anything else is named by node type.
|
@TheGupta2012 flagging this one for a deliberate look before merge — it is a breaking behaviour change, and the scope grew during review, so I would rather you sign off on it than have it slip through as a routine bug fix. What breaksPrograms that validate today will start raising if(m==1) barrier q; // was: emitted as-is
if(m==1) delay[10ns] q; // was: emitted as-is
if(m==1) box { x q[0]; } // was: emitted as-isWhy I think it is rightNone of these are expressible in OpenQASM 2. The grammar admits only a
The program was already broken — this only moves the failure from a downstream parser's syntax error to a validation error with a source span and the offending statement named. The scope growth you should be aware ofThe PR originally rejected If you would rather not break usersTwo smaller options, happy to switch to either:
My recommendation is to keep it as-is: silently emitting invalid QASM 2 is the worst of the three, since the failure surfaces far from its cause. But this is a maintainer call, not mine. The sibling PR #338 (the #337 serializer fix) has no such concern — it only changes how conditionals are printed, and every program that validated before still validates. |
Summary of changes
Found while fixing #337 (see #338). Independent of that PR — this one is about what gets accepted, not how it is printed.
The OpenQASM 2 grammar admits only a
<qop>as the body of anif:barrieris a separate production, soif(m==1) barrier q;is not a valid QASM 2 program — anddelay/boxhave no QASM 2 syntax at all, yet both are accepted in a conditional body too (delayis even rejected by the existing top-level whitelist, so a conditional was the one place it slipped through).Qasm2Module._filter_statementschecked only the type of each top-level statement, so nothing ever inspected what aBranchingStatementcarried in its body — the conditional barrier validated cleanly and was serialized verbatim:which downstream parsers reject:
So the invalid construct entered unflagged and left as invalid output.
Fix
_filter_statementsnow recurses into conditional bodies — bothif_blockandelse_block, at any nesting depth — and checks each statement against the<qop>production itself (QuantumGate,QuantumMeasurementStatement,QuantumReset) rather than blacklistingbarrieralone, which would let through whatever kinds it had not enumerated:reported through
raise_qasm3_errorso the message carries the source span and the offending statement:This is a behavior change: programs that previously validated now raise. That is the point — they were being turned into invalid QASM 2, and failing at validation with a located error beats failing in a downstream parser with a syntax error.
Tests
Written first and committed failing (ddb79e4), then made to pass by the fix (3ca979d).
test_conditional_barrier_rejected— parametrized over broadcast, single-qubit, and multi-qubit barrier forms.test_conditional_barrier_rejected_when_nested— a barrier reached only through a nested conditional, verified to fail when the recursion is removed.test_conditional_non_qop_rejected—delayandboxbodies, the wider hole behind the barrier case.test_conditional_qop_accepted— every operation that is a<qop>(gate, two-qubit gate,reset,measure) still validates, guarding against over-rejection.test_unconditional_barrier_accepted— barriers outside conditionals are untouched.The last two passed before the fix as well, which is what makes them useful: they pin the boundary the fix must not cross.