From f793e1eccc6b7847a8f685417bc6386d5f7f2c4c Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Wed, 11 Mar 2026 13:17:10 +0530 Subject: [PATCH 1/4] Fix double unroll with consolidate_qubits=True corrupting original AST `consolidate_qubit_registers` mutates AST nodes in-place (renaming qubit identifiers to `__PYQASM_QUBITS__`). Since `self._statements` holds a reference to the original program statements, these mutations corrupt the source AST. A subsequent `unroll(consolidate_qubits=True)` then encounters barrier qubits named `__PYQASM_QUBITS__[...]` that do not exist in the fresh scope, raising a `ValidationError`. Deep-copy `unrolled_stmts` at the entry of `consolidate_qubit_registers` so the transformer operates on isolated copies, leaving the original AST intact for re-processing. Closes #296 Co-Authored-By: Claude Opus 4.6 --- src/pyqasm/transformer.py | 4 ++++ 1 file changed, 4 insertions(+) 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] ): From c286a7a16d45095ec0231217fe0e96dc9ecb925d Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Wed, 11 Mar 2026 13:18:48 +0530 Subject: [PATCH 2/4] Update CHANGELOG for double-unroll consolidate_qubits fix (#296) Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86b217a..6ec6463 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`. ([#296](https://github.com/qBraid/pyqasm/pull/296)) - 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)) From a042969db459f171df320b4ca265c21ad0dc3886 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Wed, 11 Mar 2026 13:20:49 +0530 Subject: [PATCH 3/4] Update CHANGELOG PR link to #297 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec6463..21bd794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +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`. ([#296](https://github.com/qBraid/pyqasm/pull/296)) +- 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)) From 8b132eaefee01e83e6fb5f19e26f2eda8d6e4908 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Wed, 11 Mar 2026 13:23:53 +0530 Subject: [PATCH 4/4] Add test for double unroll with consolidate_qubits=True Verifies that calling unroll(consolidate_qubits=True) twice on the same module produces identical output without raising a ValidationError. Co-Authored-By: Claude Opus 4.6 --- tests/qasm3/test_device_qubits.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) 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"):