Description
The negctrl modifier expansion in visitor.py places the same QuantumGate object at two positions in the emitted statement list, rather than two equal copies:
negs = [
qasm3_ast.QuantumGate([], qasm3_ast.Identifier("x"), [], [ctrl]) for ctrl in negctrls
]
result = negs + result + negs
Every transformation that rewrites qubit indices in place (remove_idle_qubits(), reverse_qubit_order()) therefore reaches the same operand node more than once.
Reproduction
from pyqasm import loads
qasm = """OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
negctrl @ x q[0], q[1];
"""
m = loads(qasm)
m.unroll()
print([id(s) for s in m._unrolled_ast.statements if hasattr(s, "qubits")])
# [4362757584, 4362758224, 4362757584] <- statements 0 and 2 are the same object
m.reverse_qubit_order() # KeyError: -2
remove_idle_qubits() on the same circuit survives only because of the visited_node_ids guard in Modules._remap_qubits; removing that guard raises KeyError: 0. Nothing in the test suite covers either path.
Scope
Pre-existing, not a regression. #335 fixed the equivalent aliasing for decomposition-emitted statements (maps/gates.py constructors and Decomposer), but deliberately did not touch the modifier expansion path.
Suggested fix
Build a second negs list for the trailing position rather than reusing the first, and run the control operand through fresh_qubits() so the two x gates own distinct operand nodes. Add a regression test asserting no shared statement or operand nodes after unroll() for negctrl @ x, and that reverse_qubit_order() succeeds on it.
Found in review of #335 (M2).
Description
The
negctrlmodifier expansion invisitor.pyplaces the sameQuantumGateobject at two positions in the emitted statement list, rather than two equal copies:Every transformation that rewrites qubit indices in place (
remove_idle_qubits(),reverse_qubit_order()) therefore reaches the same operand node more than once.Reproduction
remove_idle_qubits()on the same circuit survives only because of thevisited_node_idsguard inModules._remap_qubits; removing that guard raisesKeyError: 0. Nothing in the test suite covers either path.Scope
Pre-existing, not a regression. #335 fixed the equivalent aliasing for decomposition-emitted statements (
maps/gates.pyconstructors andDecomposer), but deliberately did not touch the modifier expansion path.Suggested fix
Build a second
negslist for the trailing position rather than reusing the first, and run the control operand throughfresh_qubits()so the twoxgates own distinct operand nodes. Add a regression test asserting no shared statement or operand nodes afterunroll()fornegctrl @ x, and thatreverse_qubit_order()succeeds on it.Found in review of #335 (M2).