Summary
The module transformation passes iterate _unrolled_ast.statements at the top level only, so any quantum statement nested inside a box or an if block is invisible to them. remove_idle_qubits() and reverse_qubit_order() therefore emit programs that are silently wrong — and in the if case, invalid QASM.
Found while working on #340 (verbatim boxes make nested statements the normal case), but this is independent of pragmas: every repro below is on main with plain box / if.
Repro (pyqasm 1.0.4)
import pyqasm
qasm = """OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit c;
h q[2];
box {
cx q[2], q[1];
}
c = measure q[1];
"""
m = pyqasm.loads(qasm); m.unroll(); m.remove_idle_qubits()
print(pyqasm.dumps(m))
qubit[2] q; // q[0] dropped, register collapsed to 2
h q[1]; // top-level operands remapped
box {
cx q[2], q[1]; // <- untouched: q[2] is out of range now
}
c[0] = measure q[0];
reverse_qubit_order() on the same program remaps h q[2] -> h q[0] and leaves the cx inside the box pointing at the old indices.
With an if block the result is not just wrong but unparseable, and a second problem shows up — a qubit used only inside a branch is classified as idle and removed:
qasm = """OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit c;
h q[2];
c = measure q[2];
if (c == 1) {
x q[1];
}
"""
m = pyqasm.loads(qasm); m.unroll(); m.remove_idle_qubits()
qubit[1] q;
h q[0];
c[0] = measure q[0];
if (c[0] == true) {
x q[1]; // <- q[1] both un-remapped and no longer declared
}
Reloading that output fails with Index 1 out of range for register of size 1.
remove_measurements() / has_measurements() / remove_barriers() have the same blind spot: they filter the top-level statement list, so a measure or barrier inside a box or if block is neither reported nor removed.
Root cause
QasmModule._remap_qubits and QasmModule.reverse_qubit_order loop over self._unrolled_ast.statements and test isinstance(operation, QUANTUM_STATEMENTS). Box and BranchingStatement are not in QUANTUM_STATEMENTS, and their bodies are never descended into.
remove_measurements / remove_barriers rebuild the top-level list only.
- Separately,
_visit_basic_gate_operation records gates applied inside a branch in _is_branch_qubits (for depth) but never increments QubitDepthNode.num_gates, and is_idle() is defined as _total_ops() == 0. So a qubit touched only inside an if block reads as idle.
Expected
Nested quantum statements should be walked like top-level ones: operand indices remapped by remove_idle_qubits / reverse_qubit_order, and measurements/barriers found and removed wherever they live. A qubit used only inside a branch is not idle and must not be removed.
Summary
The module transformation passes iterate
_unrolled_ast.statementsat the top level only, so any quantum statement nested inside aboxor anifblock is invisible to them.remove_idle_qubits()andreverse_qubit_order()therefore emit programs that are silently wrong — and in theifcase, invalid QASM.Found while working on #340 (verbatim boxes make nested statements the normal case), but this is independent of pragmas: every repro below is on
mainwith plainbox/if.Repro (pyqasm 1.0.4)
reverse_qubit_order()on the same program remapsh q[2]->h q[0]and leaves thecxinside the box pointing at the old indices.With an
ifblock the result is not just wrong but unparseable, and a second problem shows up — a qubit used only inside a branch is classified as idle and removed:Reloading that output fails with
Index 1 out of range for register of size 1.remove_measurements()/has_measurements()/remove_barriers()have the same blind spot: they filter the top-level statement list, so ameasureorbarrierinside aboxorifblock is neither reported nor removed.Root cause
QasmModule._remap_qubitsandQasmModule.reverse_qubit_orderloop overself._unrolled_ast.statementsand testisinstance(operation, QUANTUM_STATEMENTS).BoxandBranchingStatementare not inQUANTUM_STATEMENTS, and their bodies are never descended into.remove_measurements/remove_barriersrebuild the top-level list only._visit_basic_gate_operationrecords gates applied inside a branch in_is_branch_qubits(for depth) but never incrementsQubitDepthNode.num_gates, andis_idle()is defined as_total_ops() == 0. So a qubit touched only inside anifblock reads as idle.Expected
Nested quantum statements should be walked like top-level ones: operand indices remapped by
remove_idle_qubits/reverse_qubit_order, and measurements/barriers found and removed wherever they live. A qubit used only inside a branch is not idle and must not be removed.