diff --git a/CHANGELOG.md b/CHANGELOG.md index 86b217a..21bd794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Types of changes: ### Removed ### Fixed +- Fixed `consolidate_qubit_registers` mutating AST nodes in-place, causing a `ValidationError` when `unroll(consolidate_qubits=True)` is called more than once on the same `QasmModule`. ([#297](https://github.com/qBraid/pyqasm/pull/297)) - Fixed barrier unrolling to preserve multi-qubit barrier statements instead of splitting into individual per-qubit barriers. ([#295](https://github.com/qBraid/pyqasm/pull/295)) - Added support for physical qubit identifiers (`$0`, `$1`, …) in plain QASM 3 programs, including gates, barriers, measurements, and duplicate-qubit detection. ([#291](https://github.com/qBraid/pyqasm/pull/291)) - Updated CI to use `macos-15-intel` image due to deprecation of `macos-13` image. ([#283](https://github.com/qBraid/pyqasm/pull/283)) diff --git a/src/pyqasm/transformer.py b/src/pyqasm/transformer.py index 6f0fe2a..8ab2916 100644 --- a/src/pyqasm/transformer.py +++ b/src/pyqasm/transformer.py @@ -469,6 +469,10 @@ def consolidate_qubit_registers( # pylint: disable=too-many-branches, too-many- if device_qubits is None: device_qubits = sum(global_qreg_size_map.values()) + # Deep-copy so that in-place mutations below never corrupt the + # original AST nodes (which may be re-visited on a subsequent unroll). + unrolled_stmts = deepcopy(unrolled_stmts) + def _get_pyqasm_device_qubit_index( reg: str, idx: int, qubit_reg_offsets: dict[str, int], global_qreg: dict[str, int] ): diff --git a/tests/qasm3/test_device_qubits.py b/tests/qasm3/test_device_qubits.py index d3c0eec..1c08271 100644 --- a/tests/qasm3/test_device_qubits.py +++ b/tests/qasm3/test_device_qubits.py @@ -223,6 +223,34 @@ def test_gates(): check_unrolled_qasm(dumps(result), expected_qasm) +def test_double_unroll_with_consolidate_qubits(): + """Test that calling unroll(consolidate_qubits=True) twice on the same + module does not raise due to in-place AST mutation from the first call.""" + qasm = """OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + qreg q2[3]; + barrier q2; + barrier q, q2; + """ + expected_qasm = """OPENQASM 3.0; + qubit[5] __PYQASM_QUBITS__; + include "stdgates.inc"; + barrier __PYQASM_QUBITS__[2], __PYQASM_QUBITS__[3], __PYQASM_QUBITS__[4]; + barrier __PYQASM_QUBITS__[0], __PYQASM_QUBITS__[1], __PYQASM_QUBITS__[2], __PYQASM_QUBITS__[3], __PYQASM_QUBITS__[4]; + """ + mod = loads(qasm) + mod.unroll(consolidate_qubits=True) + first_result = dumps(mod) + + # Second unroll should produce identical output without raising + mod.unroll(consolidate_qubits=True) + second_result = dumps(mod) + + check_unrolled_qasm(first_result, expected_qasm) + check_unrolled_qasm(second_result, expected_qasm) + + def test_validate(caplog): with pytest.raises(ValidationError, match=r"Total qubits '4' exceed device qubits '3'."): with caplog.at_level("ERROR"):