Skip to content

fix(visitor): pair register operands element by element when broadcasting a gate - #462

Open
ryanhill1 wants to merge 2 commits into
mainfrom
fix/register-broadcasting
Open

ryanhill1 wants to merge 2 commits into
mainfrom
fix/register-broadcasting

Conversation

@ryanhill1

Copy link
Copy Markdown
Member

Closes #384

Problem

unroll() flattened every operand of a gate call into one list and chunked it by the gate's arity. With register operands this silently produced a different circuit:

Input Before After
cx a, b; (two qubit[2]) cx a[0], a[1]; cx b[0], b[1]; cx a[0], b[0]; cx a[1], b[1];
cx q[0:1], q[2:3]; cx q[0], q[1]; cx q[2], q[3]; cx q[0], q[2]; cx q[1], q[3];
let a = q[0:1]; let b = q[2:3]; cx a, b; same as above same as above
def f(qubit[2] a, qubit[2] b) { cx a, b; } f(r, s); cx r[0], r[1]; cx s[0], s[1]; cx r[0], s[0]; cx r[1], s[1];
ctrl @ x a, b; cx a[0], a[1]; cx a[0], b[0]; cx a[0], b[1]; cx a[0], b[0]; cx a[1], b[1];
cx q, r[0]; (qubit[3] q) ValidationError cx q[0], r[0]; cx q[1], r[0]; cx q[2], r[0];
spec g4 qr0[0], qr1, qr2[0], qr3; ValidationError three g4 calls, one per index of qr1/qr3

Change

_visit_generic_gate_operation now broadcasts first, in a new _broadcast_operands:

  • It resolves each operand on its own, keeping the operand boundaries the old path lost. Inside a subroutine it keeps the formal names, so the existing argument mapping still applies.
  • When a call has one operand per gate qubit (controls included) and at least one operand names several qubits, register operands pair by index and single qubits repeat. Each application then goes through the normal path, so modifiers, custom gates, external gates and subroutines need no other change.
  • Register operands of different lengths raise ValidationError: Cannot broadcast 'cx' over registers of different lengths [2, 3].
  • Every other call keeps the existing behaviour. That includes calls with fewer operands than gate qubits (cx q2;, inv @ ccx q[0], q2;, ctrl (2) @ x a, b[0];, which existing tests rely on), the cx q[0], q[1], q[2], q[3] form, opaque gates, and non-constant modifier arguments. The issue left it to the maintainers whether to keep that register-fills-several-operands extension, so this PR leaves it alone.

Testing

  • New tests/qasm3/test_broadcast.py: two registers, slices, aliases, subroutine register arguments, swap, ctrl @, the mixed register/qubit form, the spec's g4 example, and the length mismatch. 9 of its 10 tests fail on main.
  • Full suite: 1024 passed, 3 skipped. black, isort, pylint (10.00), headers pass. mypy reports only the typer/rich import errors that come from the tox environment lacking the CLI extra.
  • Also checked by hand: negctrl @, a parametrised custom two-qubit gate on registers, and cx kept with unroll(external_gates=["cx"]) all pair by index.

Found while testing qBraid/qBraid#1468, whose qasm3 -> qasm2 conversion currently detects register broadcasts and falls back to another path to avoid this bug.

…ting a gate

cx a, b over two registers unrolled to cx a[0], a[1]; cx b[0], b[1]
because every operand was flattened into one list and chunked by the
gate's arity. When a call has one operand per gate qubit, register
operands now pair by index and single qubits repeat, as the spec
defines. Slices, aliases, subroutine register arguments and ctrl @ take
the same path. Calls with fewer operands than gate qubits, such as
cx q2, keep filling operands from the register as before.

Closes #384
@argus-eye

argus-eye Bot commented Oct 23, 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: 2
  • Diff lines (±): 187
  • Historical avg: ~347.1k tokens · ~$1.28 · across last 15 review(s)

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

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.87755% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/pyqasm/visitor.py 93.87% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@argus-eye

This comment has been minimized.

@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 — Empty broadcast operands raise IndexError

Check 1 warning before you merge. Argus reviewed 3 files.

What this PR does · 9 author checks
  • Fix gate broadcasting so register operands pair element by element and single-qubit operands repeat, rather than silently unrolling to a different circuit.
  • Excludes: Rejecting the existing register-fills-several-operands extension or the four-single-qubit cx calling form.
  • Excludes: Changing existing behavior for opaque gates or non-constant modifier arguments.

Author's checks

  • _visit_generic_gate_operation broadcasts first through _broadcast_operands, resolving operands separately and preserving operand boundaries.
  • Inside subroutines, operand resolution preserves formal names so existing argument mapping still applies.
  • Calls with one operand per gate qubit, including controls, pair register operands by index and repeat single-qubit operands.
  • Registers of different lengths raise ValidationError; for cx with lengths 2 and 3, report Cannot broadcast 'cx' over registers of different lengths [2, 3].
  • Each broadcast application uses the normal path, retaining support for modifiers, custom gates, external gates and subroutines.
  • Calls with fewer operands than gate qubits and the cx q[0], q[1], q[2], q[3] form retain existing behavior.
  • Opaque gates and non-constant modifier arguments retain existing behavior.
  • tests/qasm3/test_broadcast.py covers registers, slices, aliases, subroutine arguments, swap, ctrl @, mixed operands, g4 and length mismatch.
  • The spec's mixed-operand g4 example emits three applications, pairing qr1 and qr3 by index while repeating qr0[0] and qr2[0].
usage: 282.3k tokens · 17 stages

Contract: production/full · checked: bug_hunter, security, architecture, regression, single-pass review, ponytail-review · review took 2m44s

Stage Model Tokens Cost
Intent openai/gpt-6.1-sol 6.5k $0.0135
Triage openai/gpt-6.1-sol 6.4k $0.0070
Lead agent openai/gpt-6.1-sol 1.3k $0.0067
Review specialists (10) openai/gpt-6.1-sol 237.3k $0.5146
Review openai/gpt-6.1-sol 14.2k $0.0295
Acceptance openai/gpt-6.1-sol 984 $0.0026
Simulation openai/gpt-6.1-sol 12.6k $0.0363
Scoring openai/gpt-6.1-sol 3.0k $0.0038

Dashboard → · React 👎 on an inline comment to dismiss it · Reply to an inline comment or use @argus-eye help

🧠 Learned: 1 pattern · 1 PR summary · 1 finding memory · 1 file memory · …

Cross-Repo PR Coverage

No conflict found with qBraid/qBraid#1468.

Updated at 10:26 UTC on 2026-10-24

Comment thread src/pyqasm/visitor.py
span=operation.span,
)
size = lengths.pop()
return [[bits[i] if len(bits) > 1 else bits[0] for bits in resolved] for i in range(size)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Warning · Bug: Empty broadcast operands raise IndexError

Invalid empty operands bypass QASM error handling and crash unroll with an uncaught IndexError.

Same issue at src/pyqasm/visitor.py:L1643.

Suggested change
return [[bits[i] if len(bits) > 1 else bits[0] for bits in resolved] for i in range(size)]
if any(not bits for bits in resolved):
raise_qasm3_error(
f"Cannot broadcast '{operation.name.name}' over an empty operand",
error_node=operation,
span=operation.span,
)
return [[bits[i] if len(bits) > 1 else bits[0] for bits in resolved] for i in range(size)]

Argus did not compile or run this fix · React 👎 to dismiss

This branch has not been deployed

No deployments
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.

Gate broadcasting over multiple registers unrolls to the wrong circuit

2 participants