Skip to content

negctrl expansion emits the same QuantumGate object twice, aliasing operand nodes #350

Description

@ryanhill1

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions