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 @@ -26,6 +26,7 @@ Types of changes:
### Removed

### Fixed
- Fixed the `negctrl @` expansion emitting the same `x` `QuantumGate` object at both the leading and trailing position, so in-place transformations mutated its operands twice — crashing `reverse_qubit_order()` (`KeyError`) on any unrolled `negctrl` gate. The two `x` gates are now distinct statements with fresh operand nodes. ([#350](https://github.com/qBraid/pyqasm/issues/350))
- Fixed inaccurate `device_qubits` entry in `QasmModule.unroll()` docstring ([#349](https://github.com/qBraid/pyqasm/pull/349))
- Fixed `remove_idle_qubits()` and `reverse_qubit_order()` ignoring statements nested inside `box` and `if` blocks. Top-level operands were rewritten while nested ones kept their old indices, so the result silently addressed the wrong qubits — and when a nested index fell outside the shrunken register, the output was not a loadable program at all. Both passes now walk nested bodies, as do `has_measurements()` / `remove_measurements()` and `has_barriers()` / `remove_barriers()`; a box left empty by a removal is dropped, since pyqasm rejects a box with no statements. Two consequences of the same blind spot are fixed alongside: a qubit operated on only inside an `if` block no longer counts as idle, and `remove_idle_qubits()` no longer raises `AssertionError` on a program that mixes physical qubits with declared registers. ([#345](https://github.com/qBraid/pyqasm/pull/345))
- Fixed `unroll(consolidate_qubits=True)` raising `AttributeError: 'str' object has no attribute 'name'` for any gate applied to a physical qubit, e.g. `h $1;`. Consolidation assumed every gate operand was an `IndexedIdentifier`, but a physical qubit survives unrolling as `Identifier("$1")`. Physical qubits are absolute hardware indices belonging to no declared register, so they are now left as written — matching how `measure`, `reset` and `barrier` already treat them. ([#344](https://github.com/qBraid/pyqasm/pull/344))
Expand Down
16 changes: 11 additions & 5 deletions src/pyqasm/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
MAX_ARRAY_DIMENSIONS,
)
from pyqasm.maps.gates import (
fresh_qubits,
map_qasm_ctrl_op_to_callable,
map_qasm_inv_op_to_callable,
map_qasm_op_num_params,
Expand Down Expand Up @@ -1608,11 +1609,16 @@ def _visit_generic_gate_operation( # pylint: disable=too-many-branches, too-man
else:
result.extend(self._visit_basic_gate_operation(operation, inverse_value, ctrls))

# negctrl -> ctrl conversion
negs = [
qasm3_ast.QuantumGate([], qasm3_ast.Identifier("x"), [], [ctrl]) for ctrl in negctrls
]
result = negs + result + negs # type: ignore
# negctrl -> ctrl conversion; build each x gate with fresh operand nodes so
# the leading and trailing statements share nothing (issue #350)
def _neg_x_gates() -> list[qasm3_ast.QuantumGate]:
"""Build an x gate with fresh operand nodes for each negative control."""
return [
qasm3_ast.QuantumGate([], qasm3_ast.Identifier("x"), [], fresh_qubits(ctrl))
for ctrl in negctrls
]
Comment on lines +1614 to +1619

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 a docstring to _neg_x_gates.

The new nested function has no docstring. Add a concise description of its purpose and return value.

As per coding guidelines, every Python function must have a docstring explaining its purpose, parameters, and return values.

Proposed fix
         def _neg_x_gates() -> list[qasm3_ast.QuantumGate]:
+            """Create X gates for the negative controls.
+
+            Returns:
+                list[qasm3_ast.QuantumGate]: The generated X gates.
+            """
             return [
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _neg_x_gates() -> list[qasm3_ast.QuantumGate]:
return [
qasm3_ast.QuantumGate([], qasm3_ast.Identifier("x"), [], fresh_qubits(ctrl))
for ctrl in negctrls
]
def _neg_x_gates() -> list[qasm3_ast.QuantumGate]:
"""Create X gates for the negative controls.
Returns:
list[qasm3_ast.QuantumGate]: The generated X gates.
"""
return [
qasm3_ast.QuantumGate([], qasm3_ast.Identifier("x"), [], fresh_qubits(ctrl))
for ctrl in negctrls
]
🤖 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 1614 - 1618, Update the nested
_neg_x_gates function with a concise docstring describing that it creates X
gates for the negative-control qubits and returns the resulting list of
qasm3_ast.QuantumGate objects; note that it takes no parameters.

Source: Coding guidelines


result = _neg_x_gates() + result + _neg_x_gates() # type: ignore
self._in_generic_gate_op_scope -= 1
if self._consolidate_qubits and not self._in_generic_gate_op_scope:
result = cast(
Expand Down
27 changes: 27 additions & 0 deletions tests/qasm3/test_transformations.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ def _assert_no_shared_operand_nodes(module):
"c4x q[0], q[1], q[2], q[3], q[4];",
"ecr q[0], q[1];",
"inv @ crz(0.5) q[1], q[2];",
"negctrl @ x q[0], q[1];",
"negctrl(2) @ x q[0], q[1], q[2];",
],
)
def test_unroll_emits_fresh_operand_nodes(operation):
Expand All @@ -244,6 +246,31 @@ def test_unroll_emits_fresh_operand_nodes(operation):
_assert_no_shared_operand_nodes(module)


def test_reverse_qubit_order_negctrl():
"""Test reverse_qubit_order on a negctrl gate whose leading and trailing x
statements previously were the same object (issue #350)"""
qasm3_str = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
negctrl @ x q[0], q[1];
"""

expected_qasm3_str = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
x q[2];
cx q[2], q[1];
x q[2];
"""

module = loads(qasm3_str)
module.unroll()
module.reverse_qubit_order()
check_unrolled_qasm(dumps(module), expected_qasm3_str)


@pytest.mark.parametrize(
"operation", ["crz(0.5) q[1], q[2];", "swap q[0], q[2];", "cz q[1], q[2];"]
)
Expand Down
Loading