-
Notifications
You must be signed in to change notification settings - Fork 27
fix: walk nested box and if bodies in module transformation passes #345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,7 @@ | |
| from abc import ABC, abstractmethod | ||
| from collections import Counter | ||
| from copy import deepcopy | ||
| from typing import Optional | ||
| from typing import Iterator, Optional, Sequence, TypeVar | ||
|
|
||
| import openqasm3.ast as qasm3_ast | ||
| from openqasm3.ast import BranchingStatement, Program, QuantumGate | ||
|
|
@@ -35,6 +35,60 @@ | |
| from pyqasm.maps.decomposition_rules import DECOMPOSITION_RULES | ||
| from pyqasm.visitor import QasmVisitor, ScopeManager | ||
|
|
||
| StatementT = TypeVar("StatementT", bound=qasm3_ast.QASMNode) | ||
|
|
||
|
|
||
| def iter_quantum_statements( | ||
| statements: Sequence[qasm3_ast.QASMNode], | ||
| ) -> Iterator[qasm3_ast.Statement]: | ||
| """Yield the quantum statements in a statement list, nested ones included. | ||
|
|
||
| ``box`` and ``if`` blocks survive unrolling with their bodies intact, so a pass that | ||
| rewrites qubit operands has to reach the statements inside them too. | ||
|
|
||
| Args: | ||
| statements (Sequence[qasm3_ast.QASMNode]): The statements to walk. | ||
|
|
||
| Yields: | ||
| qasm3_ast.Statement: Each quantum statement, in program order. | ||
| """ | ||
| for stmt in statements: | ||
| if isinstance(stmt, qasm3_ast.Box): | ||
| yield from iter_quantum_statements(stmt.body) | ||
| elif isinstance(stmt, BranchingStatement): | ||
| yield from iter_quantum_statements(stmt.if_block) | ||
| yield from iter_quantum_statements(stmt.else_block) | ||
| elif isinstance(stmt, QUANTUM_STATEMENTS): | ||
| yield stmt | ||
|
|
||
|
|
||
| def drop_statements(statements: list[StatementT], unwanted: type) -> list[StatementT]: | ||
| """Return the statements without those of the unwanted type, nested ones included. | ||
|
|
||
| Bodies of ``box`` and ``if`` blocks are filtered in place. A box left with an empty | ||
| body is dropped, since pyqasm rejects a box that holds no statement. | ||
|
|
||
| Args: | ||
| statements (list[StatementT]): The statements to filter. | ||
| unwanted (type): The statement type to remove. | ||
|
|
||
| Returns: | ||
| list[StatementT]: The remaining statements. | ||
| """ | ||
| kept: list[StatementT] = [] | ||
| for stmt in statements: | ||
| if isinstance(stmt, qasm3_ast.Box): | ||
| stmt.body = drop_statements(stmt.body, unwanted) | ||
| if not stmt.body: | ||
| continue | ||
| elif isinstance(stmt, BranchingStatement): | ||
| stmt.if_block = drop_statements(stmt.if_block, unwanted) | ||
| stmt.else_block = drop_statements(stmt.else_block, unwanted) | ||
| elif isinstance(stmt, unwanted): | ||
| continue | ||
| kept.append(stmt) | ||
| return kept | ||
|
|
||
|
|
||
| def track_user_operation(func): | ||
| """Decorator to track user operations on a QasmModule.""" | ||
|
|
@@ -183,7 +237,7 @@ def has_measurements(self) -> bool: | |
| if len(self._unrolled_ast.statements) > 0 | ||
| else self._statements | ||
| ) | ||
| for stmt in stmts_to_check: | ||
| for stmt in iter_quantum_statements(stmts_to_check): | ||
| if isinstance(stmt, qasm3_ast.QuantumMeasurementStatement): | ||
| self._has_measurements = True | ||
| break | ||
|
|
@@ -199,20 +253,15 @@ def remove_measurements(self, in_place: bool = True) -> Optional["QasmModule"]: | |
| Returns: | ||
| QasmModule: The module with the measurements removed if in_place is False | ||
| """ | ||
| # copy first: the filtering rewrites nested box and if bodies in place, so it | ||
| # has to run on the module that is being returned | ||
| curr_module = self if in_place else self.copy() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [L2] The description's aliasing caveat is contradicted by this line — Maintenance · Low Rationale: the PR description's last paragraph states that This hoisted This PR's own tests assert it too: Change requested: drop that paragraph from the description before merge, so it does not leave a future reader hunting for a latent aliasing bug that this hoist already rules out.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved — paragraph dropped from the description. You are right that the hoisted |
||
| stmt_list = ( | ||
| self._statements | ||
| if len(self._unrolled_ast.statements) == 0 | ||
| else self._unrolled_ast.statements | ||
| curr_module._statements | ||
| if len(curr_module._unrolled_ast.statements) == 0 | ||
| else curr_module._unrolled_ast.statements | ||
| ) | ||
| stmts_without_meas = [ | ||
| stmt | ||
| for stmt in stmt_list | ||
| if not isinstance(stmt, qasm3_ast.QuantumMeasurementStatement) | ||
| ] | ||
| curr_module = self | ||
|
|
||
| if not in_place: | ||
| curr_module = self.copy() | ||
| stmts_without_meas = drop_statements(stmt_list, qasm3_ast.QuantumMeasurementStatement) | ||
|
|
||
| for qubit in curr_module._qubit_depths.values(): | ||
| qubit.num_measurements = 0 | ||
|
|
@@ -243,7 +292,7 @@ def has_barriers(self) -> bool: | |
| if len(self._unrolled_ast.statements) > 0 | ||
| else self._statements | ||
| ) | ||
| for stmt in stmts_to_check: | ||
| for stmt in iter_quantum_statements(stmts_to_check): | ||
| if isinstance(stmt, qasm3_ast.QuantumBarrier): | ||
| self._has_barriers = True | ||
| break | ||
|
|
@@ -259,17 +308,14 @@ def remove_barriers(self, in_place: bool = True) -> Optional["QasmModule"]: | |
| Returns: | ||
| QasmModule: The module with the barriers removed if in_place is False | ||
| """ | ||
| # copy first, as in remove_measurements: nested bodies are filtered in place | ||
| curr_module = self if in_place else self.copy() | ||
| stmt_list = ( | ||
| self._statements | ||
| if len(self._unrolled_ast.statements) == 0 | ||
| else self._unrolled_ast.statements | ||
| curr_module._statements | ||
| if len(curr_module._unrolled_ast.statements) == 0 | ||
| else curr_module._unrolled_ast.statements | ||
| ) | ||
| stmts_without_barriers = [ | ||
| stmt for stmt in stmt_list if not isinstance(stmt, qasm3_ast.QuantumBarrier) | ||
| ] | ||
| curr_module = self | ||
| if not in_place: | ||
| curr_module = self.copy() | ||
| stmts_without_barriers = drop_statements(stmt_list, qasm3_ast.QuantumBarrier) | ||
|
|
||
| for qubit in curr_module._qubit_depths.values(): | ||
| qubit.num_barriers = 0 | ||
|
|
@@ -380,17 +426,17 @@ def _remap_qubits(self, reg_name: str, size: int, idle_indices: list[int]): | |
| # gate decompositions can reuse the same operand node across multiple statements, | ||
| # so track visited nodes to avoid remapping a shared node more than once | ||
| visited_node_ids = set() | ||
| for operation in self._unrolled_ast.statements: | ||
| if isinstance(operation, QUANTUM_STATEMENTS): | ||
| bit_list = Qasm3Analyzer.get_op_bit_list(operation) | ||
| for bit in bit_list: | ||
| assert isinstance(bit, qasm3_ast.IndexedIdentifier) | ||
| if bit.name.name == reg_name: | ||
| index_node = bit.indices[0][0] # type: ignore[index] | ||
| if id(index_node) in visited_node_ids: | ||
| continue | ||
| visited_node_ids.add(id(index_node)) | ||
| index_node.value = idx_map[index_node.value] # type: ignore[union-attr] | ||
| for operation in iter_quantum_statements(self._unrolled_ast.statements): | ||
| bit_list = Qasm3Analyzer.get_op_bit_list(operation) | ||
| for bit in bit_list: | ||
| if not isinstance(bit, qasm3_ast.IndexedIdentifier): | ||
| continue # physical qubit ("$n"): not part of any register | ||
| if bit.name.name == reg_name: | ||
| index_node = bit.indices[0][0] # type: ignore[index] | ||
| if id(index_node) in visited_node_ids: | ||
| continue | ||
| visited_node_ids.add(id(index_node)) | ||
| index_node.value = idx_map[index_node.value] # type: ignore[union-attr] | ||
|
|
||
| def _get_idle_qubit_indices(self) -> dict[str, list[int]]: | ||
| """Get the indices of the idle qubits in the module | ||
|
|
@@ -536,29 +582,31 @@ def reverse_qubit_order(self, in_place=True): | |
| # the depth maps here | ||
|
|
||
| # 2. replace each qubit index in the Quantum Operations with the new index | ||
| for operation in qasm_module._unrolled_ast.statements: | ||
| if isinstance(operation, QUANTUM_STATEMENTS): | ||
| bit_list = Qasm3Analyzer.get_op_bit_list(operation) | ||
| for bit in bit_list: | ||
| curr_reg_name = bit.name.name | ||
| curr_reg_idx = bit.indices[0][0].value | ||
| new_reg_idx = new_qubit_mappings[curr_reg_name][curr_reg_idx] | ||
|
|
||
| # make the idx -ve so that this is not touched | ||
| # while updating the same index later | ||
|
|
||
| # idx -> -1 * idx - 1 as we also have to look at index 0 | ||
| # which will remain 0 if we just multiply by -1 | ||
| bit.indices[0][0].value = -1 * new_reg_idx - 1 | ||
| for operation in iter_quantum_statements(qasm_module._unrolled_ast.statements): | ||
| bit_list = Qasm3Analyzer.get_op_bit_list(operation) | ||
| for bit in bit_list: | ||
| if not isinstance(bit, qasm3_ast.IndexedIdentifier): | ||
| continue # physical qubit ("$n"): not part of any register | ||
| curr_reg_name = bit.name.name | ||
| curr_reg_idx = bit.indices[0][0].value | ||
| new_reg_idx = new_qubit_mappings[curr_reg_name][curr_reg_idx] | ||
|
|
||
| # make the idx -ve so that this is not touched | ||
| # while updating the same index later | ||
|
|
||
| # idx -> -1 * idx - 1 as we also have to look at index 0 | ||
| # which will remain 0 if we just multiply by -1 | ||
| bit.indices[0][0].value = -1 * new_reg_idx - 1 | ||
|
|
||
| # remove the -ve marker | ||
| for operation in qasm_module._unrolled_ast.statements: | ||
| if isinstance(operation, QUANTUM_STATEMENTS): | ||
| bit_list = Qasm3Analyzer.get_op_bit_list(operation) | ||
| for bit in bit_list: | ||
| if bit.indices[0][0].value < 0: | ||
| bit.indices[0][0].value += 1 | ||
| bit.indices[0][0].value *= -1 | ||
| for operation in iter_quantum_statements(qasm_module._unrolled_ast.statements): | ||
| bit_list = Qasm3Analyzer.get_op_bit_list(operation) | ||
| for bit in bit_list: | ||
| if not isinstance(bit, qasm3_ast.IndexedIdentifier): | ||
| continue | ||
| if bit.indices[0][0].value < 0: | ||
| bit.indices[0][0].value += 1 | ||
| bit.indices[0][0].value *= -1 | ||
|
|
||
| # 3. update the original AST with the unrolled AST | ||
| qasm_module._statements = qasm_module._unrolled_ast.statements | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -628,11 +628,13 @@ def _visit_measurement( # pylint: disable=too-many-locals,too-many-branches,too | |||||||||||||||||||
| ) | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| # if measurement gate is not in branching statement | ||||||||||||||||||||
| src_name, src_idx = QasmVisitor._get_qubit_name_and_id(src_id) | ||||||||||||||||||||
| if not self._in_branching_statement: | ||||||||||||||||||||
| src_name, src_id = src_id.name.name, src_id.indices[0][0].value # type: ignore | ||||||||||||||||||||
| qubit_node = self._module._qubit_depths[(src_name, src_id)] | ||||||||||||||||||||
| qubit_node = self._module._qubit_depths[(src_name, src_idx)] | ||||||||||||||||||||
| qubit_node.depth += 1 | ||||||||||||||||||||
| qubit_node.num_measurements += 1 | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| self._mark_branch_qubit(src_name, src_idx) | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| target_name: str = ( | ||||||||||||||||||||
| target.name if isinstance(target, qasm3_ast.Identifier) else target.name.name | ||||||||||||||||||||
|
|
@@ -661,12 +663,12 @@ def _visit_measurement( # pylint: disable=too-many-locals,too-many-branches,too | |||||||||||||||||||
| target=tgt_id if target else None, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| # if measurement gate is not in branching statement | ||||||||||||||||||||
| src_name, src_idx = QasmVisitor._get_qubit_name_and_id(src_id) | ||||||||||||||||||||
| if not self._in_branching_statement: | ||||||||||||||||||||
| src_name, src_id = src_id.name.name, src_id.indices[0][0].value # type: ignore | ||||||||||||||||||||
| tgt_name, tgt_id = tgt_id.name.name, tgt_id.indices[0][0].value # type: ignore | ||||||||||||||||||||
|
|
||||||||||||||||||||
| qubit_node, clbit_node = ( | ||||||||||||||||||||
| self._module._qubit_depths[(src_name, src_id)], | ||||||||||||||||||||
| self._module._qubit_depths[(src_name, src_idx)], | ||||||||||||||||||||
| self._module._clbit_depths[(tgt_name, tgt_id)], | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| qubit_node.depth += 1 | ||||||||||||||||||||
|
|
@@ -682,6 +684,8 @@ def _visit_measurement( # pylint: disable=too-many-locals,too-many-branches,too | |||||||||||||||||||
| self._measurement_set.add(target.name) | ||||||||||||||||||||
| elif isinstance(target, qasm3_ast.IndexedIdentifier): | ||||||||||||||||||||
| self._measurement_set.add(target.name.name) | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| self._mark_branch_qubit(src_name, src_idx) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| unrolled_measurements.append(unrolled_measure) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -769,7 +773,7 @@ def _visit_reset(self, statement: qasm3_ast.QuantumReset) -> list[qasm3_ast.Quan | |||||||||||||||||||
| qubit_node.depth += 1 | ||||||||||||||||||||
| qubit_node.num_resets += 1 | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| self._is_branch_qubits.add((qubit_name, qubit_id)) | ||||||||||||||||||||
| self._mark_branch_qubit(qubit_name, qubit_id) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| unrolled_resets.append(unrolled_reset) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -1042,6 +1046,19 @@ def _register_physical_qubit(self, name: str) -> int: | |||||||||||||||||||
| self._module.num_qubits = max(self._module.num_qubits, phys_idx + 1) | ||||||||||||||||||||
| return phys_idx | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def _mark_branch_qubit(self, qubit_name: str, qubit_idx: int) -> None: | ||||||||||||||||||||
| """Record a qubit operated on inside an if/else block. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| The depth of such a qubit is settled once the branch closes, but it counts as | ||||||||||||||||||||
| used right away so that it is not mistaken for an idle qubit. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Args: | ||||||||||||||||||||
| qubit_name: The register name (or physical qubit identifier). | ||||||||||||||||||||
| qubit_idx: The index of the qubit within the register. | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| self._is_branch_qubits.add((qubit_name, qubit_idx)) | ||||||||||||||||||||
| self._module._qubit_depths[(qubit_name, qubit_idx)].used_in_branch = True | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||
| def _get_qubit_name_and_id( | ||||||||||||||||||||
| qubit: qasm3_ast.IndexedIdentifier | qasm3_ast.Identifier, | ||||||||||||||||||||
|
|
@@ -1182,7 +1199,7 @@ def _visit_basic_gate_operation( | |||||||||||||||||||
| for qubit_subset in unrolled_targets + [ctrls]: | ||||||||||||||||||||
| for qubit in qubit_subset: | ||||||||||||||||||||
| qubit_name, qubit_idx = QasmVisitor._get_qubit_name_and_id(qubit) | ||||||||||||||||||||
| self._is_branch_qubits.add((qubit_name, qubit_idx)) | ||||||||||||||||||||
| self._mark_branch_qubit(qubit_name, qubit_idx) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # check for duplicate bits | ||||||||||||||||||||
| for final_gate in result: | ||||||||||||||||||||
|
|
@@ -1299,7 +1316,7 @@ def _visit_custom_gate_operation( | |||||||||||||||||||
| for qubit_subset in [op_qubits] + [ctrls]: | ||||||||||||||||||||
| for qubit in qubit_subset: | ||||||||||||||||||||
| qubit_name, qubit_idx = QasmVisitor._get_qubit_name_and_id(qubit) | ||||||||||||||||||||
| self._is_branch_qubits.add((qubit_name, qubit_idx)) | ||||||||||||||||||||
| self._mark_branch_qubit(qubit_name, qubit_idx) | ||||||||||||||||||||
|
Comment on lines
1316
to
+1319
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 **P1 (7/10) · Bug:** Custom gates double-count branch operations after expansion (medium confidence)Operation metadata becomes inaccurate for every custom gate used in a branch, affecting consumers of
Suggested change
React 👎 to dismiss · Argus learns from feedback |
||||||||||||||||||||
|
|
||||||||||||||||||||
| self._scope_manager.pop_scope() | ||||||||||||||||||||
| self._scope_manager.restore_context() | ||||||||||||||||||||
|
|
||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[L3] Non-unrolled path still misses loop and switch bodies — Implementation · Low
Rationale: the walk closes the gap on the unrolled path, but
has_measurements/remove_measurements/has_barriers/remove_barriersfall back toself._statementswhen the module has not been unrolled, and that list can still containfor/while/switchbodies this walker does not descend into. Verified on this branch:Without
unroll(),has_measurements()andhas_barriers()both returnFalseandremove_measurements()is a no-op; afterunroll()both returnTrueand removal works.This is pre-existing —
mainbehaves identically — and the PR strictly improves the situation, so it is not worth holding the merge on. Flagged because this walker is the natural home for the remainder, and it is easy to read the PR as having closed the class of bug entirely.Change requested: either extend
iter_quantum_statementsto loop and switch bodies for the non-unrolled path, or document on those four methods that they are only meaningful afterunroll(). A follow-up issue is fine.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Follow-up filed as #354 and referenced in the description, per your note that an issue is fine here.
Reproduced your case on this branch:
for int i in [0:1] { c[i] = measure q[i]; barrier q; }giveshas_measurements() == has_barriers() == Falsewithoutunroll(),Trueafter. #354 records both remedies you offered (extenditer_quantum_statements, or document the four methods as post-unroll()only).The description now has a "Not closed here" section naming the gap, and carries your finding that
for/while/switchnever reach_unrolled_astwith bodies intact — so the unrolled path really is complete withBoxandBranchingStatement, and the remainder is non-unrolled only.