From 29806babcca0480d23eb05ecd98266fd5b1570cf Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Fri, 2 Oct 2026 15:55:25 -0500 Subject: [PATCH 1/3] Fix bit order of unrolled classical register conditions --- src/pyqasm/transformer.py | 69 ++++++++++++++- src/pyqasm/visitor.py | 97 ++++++++------------- tests/qasm3/test_if.py | 172 ++++++++++++++++++++++++++++++++----- tests/qasm3/test_rebase.py | 54 +++++++----- 4 files changed, 288 insertions(+), 104 deletions(-) diff --git a/src/pyqasm/transformer.py b/src/pyqasm/transformer.py index 296893fc..8402f295 100644 --- a/src/pyqasm/transformer.py +++ b/src/pyqasm/transformer.py @@ -25,6 +25,7 @@ BinaryExpression, BinaryOperator, BooleanLiteral, + BranchingStatement, DiscreteSet, Expression, FloatLiteral, @@ -315,8 +316,7 @@ def get_branch_params( condition.lhs.index[0].value, condition.lhs.collection.name, condition.op, - # evaluate to bool - Qasm3ExprEvaluator.evaluate_expression(condition.rhs)[0] != 0, + Qasm3ExprEvaluator.evaluate_expression(condition.rhs)[0], ) if isinstance(condition, IndexExpression): if isinstance(condition.index, DiscreteSet): @@ -341,6 +341,71 @@ def get_branch_params( # default case return BranchParams(None, "", None, None) + @staticmethod + def unroll_register_comparison( # pylint: disable=too-many-arguments, too-many-locals + reg_name: str, + bit_indices: Sequence[int], + op: BinaryOperator, + value: int, + if_block: list[Statement], + else_block: list[Statement], + ) -> list[Statement]: + """Rewrite the comparison ``reg op value`` as nested single-bit branches. + + ``bit_indices`` lists the bits of ``reg`` from least to most significant, as in + OpenQASM where ``c[0]`` is the LSB of ``c``. Bits are tested MSB-first. A + comparison that holds for every (or no) value of the register is folded into + ``if_block`` (or ``else_block``) without emitting a branch. + + Args: + reg_name (str): The classical register being compared. + bit_indices (Sequence[int]): The register bits, least significant first. + op (BinaryOperator): One of ``==``, ``>=``, ``<=``, ``>``, ``<``. + value (int): The integer the register is compared against. + if_block (list[Statement]): Statements to run when the comparison holds. + else_block (list[Statement]): Statements to run when it does not. + + Returns: + list[Statement]: The statements to emit in place of the branch. + """ + eq, ge, le = BinaryOperator["=="], BinaryOperator[">="], BinaryOperator["<="] + if op == BinaryOperator[">"]: + op, value = ge, value + 1 + elif op == BinaryOperator["<"]: + op, value = le, value - 1 + + max_value = (1 << len(bit_indices)) - 1 + if (op == ge and value <= 0) or (op == le and value >= max_value): + return deepcopy(if_block) + if not 0 <= value <= max_value: + return deepcopy(else_block) + + # built from the LSB outwards; ``block`` decides the comparison on the bits + # below ``pos`` once every bit above them has matched ``value`` + block = if_block + for pos, bit in enumerate(bit_indices): + low_mask = (1 << (pos + 1)) - 1 + if (op == ge and value & low_mask == 0) or (op == le and value & low_mask == low_mask): + block = if_block + continue + bit_set = bool(value >> pos & 1) + # first differing bit from the MSB decides the order of reg and value + mismatch = else_block if op == eq or (op == ge) == bit_set else if_block + block = [ + BranchingStatement( + condition=BinaryExpression( + op=eq, + lhs=IndexExpression( + collection=Identifier(name=reg_name), index=[IntegerLiteral(bit)] + ), + rhs=BooleanLiteral(bit_set), + ), + if_block=deepcopy(block), + else_block=deepcopy(mismatch), + ) + ] + return deepcopy(block) + @classmethod def transform_function_qubits( cls, diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index a1afb6f7..2e5efd2b 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -21,6 +21,7 @@ import copy import logging +import operator import re import sys from collections import OrderedDict, deque @@ -87,6 +88,16 @@ logger.propagate = False +# comparisons the unroller accepts between a classical bit and an integer +BRANCH_COMPARISONS: dict[str, Callable[[Any, Any], bool]] = { + "==": operator.eq, + ">=": operator.ge, + "<=": operator.le, + ">": operator.gt, + "<": operator.lt, +} + + # pylint: disable-next=too-many-instance-attributes class QasmVisitor: """A visitor for basic OpenQASM program elements. @@ -2377,7 +2388,7 @@ def _visit_branching_statement( span=statement.span, ) - assert isinstance(rhs_value, (bool, int)) + assert isinstance(rhs_value, (bool, int, float)) if_block = self.visit_basic_block(statement.if_block) else_block = self.visit_basic_block(statement.else_block) @@ -2391,72 +2402,40 @@ def _visit_branching_statement( # getting creg for depth counting self._is_branch_clbits.add((reg_name, reg_idx)) - new_if_block = qasm3_ast.BranchingStatement( - condition=qasm3_ast.BinaryExpression( - op=qasm3_ast.BinaryOperator["=="], - lhs=qasm3_ast.IndexExpression( - collection=qasm3_ast.Identifier(name=reg_name), - index=[qasm3_ast.IntegerLiteral(reg_idx)], - ), - rhs=( - qasm3_ast.BooleanLiteral(rhs_value) - if isinstance(rhs_value, bool) - else qasm3_ast.IntegerLiteral(rhs_value) - ), - ), - if_block=if_block, - else_block=else_block, - ) - result.append(new_if_block) + # a bit compares as the integer 0 or 1, so keep only the bit values + # that satisfy the condition + if op == qasm3_ast.UnaryOperator["!"]: + satisfying = [0] + else: + compare = BRANCH_COMPARISONS[op.name] # type: ignore[union-attr] + satisfying = [bit for bit in (0, 1) if compare(bit, rhs_value)] + if len(satisfying) == 2: + result.extend(if_block) + elif not satisfying: + result.extend(else_block) + else: + result.extend( + Qasm3Transformer.unroll_register_comparison( + reg_name, + [reg_idx], + qasm3_ast.BinaryOperator["=="], + satisfying[0], + if_block, + else_block, + ) + ) else: # unroll multi-bit branch assert isinstance(rhs_value, int) and op in [ qasm3_ast.BinaryOperator[o] for o in ["==", ">=", "<=", ">", "<"] ] - - if op == qasm3_ast.BinaryOperator[">"]: - op = qasm3_ast.BinaryOperator[">="] - rhs_value += 1 - elif op == qasm3_ast.BinaryOperator["<"]: - op = qasm3_ast.BinaryOperator["<="] - rhs_value -= 1 - size = self._global_creg_size_map[reg_name] # getting cregs for depth counting self._is_branch_clbits.update((reg_name, i) for i in range(size)) - rhs_value_str = bin(int(rhs_value))[2:].zfill(size) - else_block = self.visit_basic_block(statement.else_block) - - def ravel(bit_ind): - """Unravel if statement from MSB to LSB""" - r = rhs_value_str[bit_ind] == "1" - if (op == qasm3_ast.BinaryOperator[">="] and not r) or ( - op == qasm3_ast.BinaryOperator["<="] and r - ): - # skip if bit condition is irrelevant. - # ex. if op is >= and r = 0, both values reg[i]={0,1} satisfy the condition - return if_block if bit_ind == len(rhs_value_str) - 1 else ravel(bit_ind + 1) - - return [ - qasm3_ast.BranchingStatement( - condition=qasm3_ast.BinaryExpression( - op=qasm3_ast.BinaryOperator["=="], - lhs=qasm3_ast.IndexExpression( - collection=qasm3_ast.Identifier(name=reg_name), - index=[qasm3_ast.IntegerLiteral(bit_ind)], - ), - rhs=qasm3_ast.BooleanLiteral(r), - ), - if_block=( - if_block - if bit_ind == len(rhs_value_str) - 1 - else ravel(bit_ind + 1) - ), - else_block=else_block, - ) - ] - - result.extend(self.visit_basic_block(ravel(0))) # type: ignore[arg-type] + unrolled = Qasm3Transformer.unroll_register_comparison( + reg_name, range(size), op, rhs_value, if_block, else_block # type: ignore[arg-type] + ) + result.extend(self.visit_basic_block(unrolled)) # type: ignore[arg-type] else: # here we can unroll the block depending on the condition positive_branching = Qasm3ExprEvaluator.evaluate_expression(condition)[0] != 0 diff --git a/tests/qasm3/test_if.py b/tests/qasm3/test_if.py index a394ec74..894e3e7f 100644 --- a/tests/qasm3/test_if.py +++ b/tests/qasm3/test_if.py @@ -17,6 +17,7 @@ """ +import openqasm3.ast as qasm3_ast import pytest from pyqasm.entrypoint import dumps, loads @@ -167,39 +168,47 @@ def test_multi_bit_if(): include "stdgates.inc"; qubit[1] q; bit[4] c; - if (c[0] == false) { - if (c[1] == false) { - if (c[2] == true) { - if (c[3] == true) { + if (c[3] == false) { + if (c[2] == false) { + if (c[1] == true) { + if (c[0] == true) { h q[0]; } } } } - if (c[2] == true) { - if (c[3] == true) { - h q[0]; + if (c[3] == false) { + if (c[2] == false) { + if (c[1] == true) { + if (c[0] == true) { + h q[0]; + } else { + x q[0]; + } + } else { + x q[0]; + } } else { - x q[0]; + h q[0]; } } else { - x q[0]; + h q[0]; } - if (c[0] == false) { - if (c[1] == false) { - h q[0]; - } else { - x q[0]; - } + if (c[3] == false) { + if (c[2] == false) { + h q[0]; + } else { + x q[0]; + } } else { x q[0]; } - if (c[0] == false) { - if (c[1] == false) { - h q[0]; - } else { - x q[0]; - } + if (c[3] == false) { + if (c[2] == false) { + h q[0]; + } else { + x q[0]; + } } else { x q[0]; } @@ -212,6 +221,127 @@ def test_multi_bit_if(): check_unrolled_qasm(dumps(result), expected_qasm) +def test_register_condition_bit_order(): + """c[0] is the least significant bit, so c == 2 means c[1] = 1 and c[0] = 0.""" + qasm = """OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + bit[2] c; + if (c == 2) { + x q[0]; + } + """ + expected_qasm = """OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + bit[2] c; + if (c[1] == true) { + if (c[0] == false) { + x q[0]; + } + } + """ + result = loads(qasm) + result.unroll() + check_unrolled_qasm(dumps(result), expected_qasm) + + +def _branch_bits(statements): + """Collect ``{bit index: value}`` from an unrolled chain of single-bit branches.""" + bits = {} + while len(statements) == 1 and isinstance(statements[0], qasm3_ast.BranchingStatement): + condition = statements[0].condition + bits[condition.lhs.index[0].value] = condition.rhs.value + statements = statements[0].if_block + return bits + + +@pytest.mark.parametrize( + "width,value", + [(1, 0), (1, 1), (2, 1), (2, 2), (3, 1), (3, 4), (3, 6), (5, 1), (5, 19)], +) +def test_register_equality_bits(width, value): + qasm = f"""OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + bit[{width}] c; + if (c == {value}) {{ + x q[0]; + }} + """ + result = loads(qasm) + result.unroll() + branch = [s for s in result.unrolled_ast.statements if isinstance(s, qasm3_ast.Statement)] + assert _branch_bits(branch[-1:]) == {i: bool(value >> i & 1) for i in range(width)} + + +def _runs_body(statements, register_value): + """Whether ``x q[0]`` executes when the 1-qubit program's register holds ``register_value``.""" + for statement in statements: + if isinstance(statement, qasm3_ast.QuantumGate): + return True + if isinstance(statement, qasm3_ast.BranchingStatement): + condition = statement.condition + bit = register_value >> condition.lhs.index[0].value & 1 + taken = statement.if_block if bit == condition.rhs.value else statement.else_block + if _runs_body(taken, register_value): + return True + return False + + +@pytest.mark.parametrize("width", [1, 2, 3]) +@pytest.mark.parametrize("op", ["==", ">=", "<=", ">", "<"]) +def test_register_condition_matches_integer_comparison(width, op): + """Every unrolled comparison, including out-of-range values, agrees with integer comparison.""" + for value in range(-1, 2**width + 1): + qasm = f"""OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + bit[{width}] c; + if (c {op} {value}) {{ + x q[0]; + }} + """ + result = loads(qasm) + result.unroll() + for register_value in range(2**width): + expected = eval(f"{register_value} {op} {value}") # pylint: disable=eval-used + assert ( + _runs_body(result.unrolled_ast.statements, register_value) == expected + ), f"c {op} {value} with c = {register_value}" + + +@pytest.mark.parametrize( + "condition,expected_body", + [ + ("c[1] == 1", "if (c[1] == true) {\n x q[0];\n }"), + ("c[1] < 1", "if (c[1] == false) {\n x q[0];\n }"), + ("c[1] > 0", "if (c[1] == true) {\n x q[0];\n }"), + ("c[1] >= 0", "x q[0];"), + ("c[1] == 2", ""), + ("!c[1]", "if (c[1] == false) {\n x q[0];\n }"), + ], +) +def test_single_bit_condition(condition, expected_body): + qasm = f"""OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + bit[2] c; + if ({condition}) {{ + x q[0]; + }} + """ + expected_qasm = f"""OPENQASM 3.0; + include "stdgates.inc"; + qubit[1] q; + bit[2] c; + {expected_body} + """ + result = loads(qasm) + result.unroll() + check_unrolled_qasm(dumps(result), expected_qasm) + + @pytest.mark.parametrize( "qasm_code,error_message,line_num,col_num,err_line", [ diff --git a/tests/qasm3/test_rebase.py b/tests/qasm3/test_rebase.py index ecd41863..787c0eed 100644 --- a/tests/qasm3/test_rebase.py +++ b/tests/qasm3/test_rebase.py @@ -330,43 +330,53 @@ def test_rebase_if(): include "stdgates.inc"; qubit[1] q; bit[4] c; - if (c[0] == false) { - if (c[1] == false) { - if (c[2] == true) { - if (c[3] == true) { + if (c[3] == false) { + if (c[2] == false) { + if (c[1] == true) { + if (c[0] == true) { ry(1.5707963267948966) q[0]; rx(3.141592653589793) q[0]; } } } } - if (c[2] == true) { - if (c[3] == true) { - ry(1.5707963267948966) q[0]; - rx(3.141592653589793) q[0]; + if (c[3] == false) { + if (c[2] == false) { + if (c[1] == true) { + if (c[0] == true) { + ry(1.5707963267948966) q[0]; + rx(3.141592653589793) q[0]; + } else { + rx(3.141592653589793) q[0]; + } + } else { + rx(3.141592653589793) q[0]; + } } else { + ry(1.5707963267948966) q[0]; rx(3.141592653589793) q[0]; } } else { + ry(1.5707963267948966) q[0]; rx(3.141592653589793) q[0]; } - if (c[0] == false) { - if (c[1] == false) { - ry(1.5707963267948966) q[0]; - rx(3.141592653589793) q[0]; - } else { - rx(3.141592653589793) q[0]; - } + if (c[3] == false) { + if (c[2] == false) { + ry(1.5707963267948966) q[0]; + rx(3.141592653589793) q[0]; + } else { + rx(3.141592653589793) q[0]; + } } else { rx(3.141592653589793) q[0]; } - if (c[0] == false) { - if (c[1] == false) { - ry(1.5707963267948966) q[0]; - rx(3.141592653589793) q[0]; - } else { - rx(3.141592653589793) q[0]; - } + if (c[3] == false) { + if (c[2] == false) { + ry(1.5707963267948966) q[0]; + rx(3.141592653589793) q[0]; + } else { + rx(3.141592653589793) q[0]; + } } else { rx(3.141592653589793) q[0]; } From d3b609aa1c06d2443abe252f89108d7904f8fb89 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Fri, 2 Oct 2026 15:56:19 -0500 Subject: [PATCH 2/3] Add changelog entry for #444 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25cedc3b..80e23886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Types of changes: - Fixed qubit register ranges dropping their final qubit during unrolling. Endpoints are now inclusive for gates, aliases, barriers, and measurements. Code using an exclusive upper bound must lower it by one, for example `q[0:1]` instead of `q[0:2]` to select two qubits. ([#432](https://github.com/qBraid/pyqasm/issues/432)) - Fixed `cs` and `csdg` from `stdgates.inc` being rejected during unrolling. They now decompose to controlled phase shifts. ([#439](https://github.com/qBraid/pyqasm/issues/439)) - Fixed Clifford+T rebasing for exact `rx`, `ry`, and `rz` rotations at multiples of π/4. These gates now decompose instead of disappearing, while angles outside the exact basis raise `RebaseError` instead of producing an incorrect result. ([#428](https://github.com/qBraid/pyqasm/issues/428)) +- Fixed `unroll()` reading `c[0]` as the most significant bit of a classical register condition, so `if (c == 2)` ran its body when `c` was 1. `>=`, `<=`, `>`, `<`, out-of-range values, and single-bit conditions such as `c[0] < 1` now unroll correctly too. ([#444](https://github.com/qBraid/pyqasm/pull/444)) ### Dependencies From af60958746e68ee072eeb6b44bbc13209ed56311 Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Mon, 5 Oct 2026 12:00:31 +0530 Subject: [PATCH 3/3] Support != in register conditions and fix QASM 2 bit order Argus flagged that `!=` would raise KeyError in the single-bit path. It could not: get_branch_params rejected `!=` first. Support it end to end instead, as `==` with the branches swapped. The QASM 2 printer from #338 assumed the old MSB-first chain, so it printed `if (m == 2)` for `if (m == 1)`. Read c[0] as the LSB there too. The exhaustive comparison test now has an else branch, so it also checks which branch runs when a comparison folds. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- src/pyqasm/modules/qasm2.py | 4 ++-- src/pyqasm/transformer.py | 10 +++++++--- src/pyqasm/visitor.py | 13 +++++++++---- tests/qasm3/test_if.py | 26 +++++++++++++++----------- 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 237381ec..834a8f06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ Types of changes: - Fixed `cs` and `csdg` from `stdgates.inc` being rejected during unrolling. They now decompose to controlled phase shifts. ([#439](https://github.com/qBraid/pyqasm/issues/439)) - Fixed Clifford+T rebasing for exact `rx`, `ry`, and `rz` rotations at multiples of π/4. These gates now decompose instead of disappearing, while angles outside the exact basis raise `RebaseError` instead of producing an incorrect result. ([#428](https://github.com/qBraid/pyqasm/issues/428)) - Fixed the OpenQASM 2 serializer emitting OpenQASM 3 syntax for classical conditionals. `if(m==1) x q[1];` was written as a braced block and, after `unroll()`, as a per-bit `if (m[0] == true)` chain, which QASM 2 parsers such as pytket and Qiskit reject. Conditionals are now emitted as `if (creg == int) `, and `validate()` rejects branch shapes QASM 2 cannot express. ([#337](https://github.com/qBraid/pyqasm/issues/337)) -- Fixed `unroll()` reading `c[0]` as the most significant bit of a classical register condition, so `if (c == 2)` ran its body when `c` was 1. `>=`, `<=`, `>`, `<`, out-of-range values, and single-bit conditions such as `c[0] < 1` now unroll correctly too. ([#444](https://github.com/qBraid/pyqasm/pull/444)) +- Fixed `unroll()` reading `c[0]` as the most significant bit of a classical register condition, so `if (c == 2)` ran its body when `c` was 1. `>=`, `<=`, `>`, `<`, out-of-range values, and single-bit conditions such as `c[0] < 1` now unroll correctly too, and `!=` is now supported. ([#444](https://github.com/qBraid/pyqasm/pull/444)) ### Dependencies diff --git a/src/pyqasm/modules/qasm2.py b/src/pyqasm/modules/qasm2.py index 8561d913..44e63d1e 100644 --- a/src/pyqasm/modules/qasm2.py +++ b/src/pyqasm/modules/qasm2.py @@ -283,8 +283,8 @@ def _flatten_branch( f"{size}-bit register (bits {missing} are unconstrained); QASM 2.0 can only " "compare a classical register against an integer" ) - # unrolling ravels the comparison value MSB-first, so invert that ordering here - reg_value = sum(1 << (size - 1 - index) for index, set_ in bit_values.items() if set_) + # c[0] is the least significant bit of c + reg_value = sum(1 << index for index, set_ in bit_values.items() if set_) assert reg_value is not None diff --git a/src/pyqasm/transformer.py b/src/pyqasm/transformer.py index 8402f295..d31ba11f 100644 --- a/src/pyqasm/transformer.py +++ b/src/pyqasm/transformer.py @@ -293,9 +293,9 @@ def get_branch_params( False, ) if isinstance(condition, BinaryExpression): - if condition.op not in [BinaryOperator[o] for o in ["==", ">=", "<=", ">", "<"]]: + if condition.op not in [BinaryOperator[o] for o in ["==", "!=", ">=", "<=", ">", "<"]]: raise_qasm3_error( - message="Only {==, >=, <=, >, <} supported in branching condition " + message="Only {==, !=, >=, <=, >, <} supported in branching condition " "with classical register", error_node=condition, span=condition.span, @@ -360,7 +360,7 @@ def unroll_register_comparison( # pylint: disable=too-many-arguments, too-many- Args: reg_name (str): The classical register being compared. bit_indices (Sequence[int]): The register bits, least significant first. - op (BinaryOperator): One of ``==``, ``>=``, ``<=``, ``>``, ``<``. + op (BinaryOperator): One of ``==``, ``!=``, ``>=``, ``<=``, ``>``, ``<``. value (int): The integer the register is compared against. if_block (list[Statement]): Statements to run when the comparison holds. else_block (list[Statement]): Statements to run when it does not. @@ -369,6 +369,10 @@ def unroll_register_comparison( # pylint: disable=too-many-arguments, too-many- list[Statement]: The statements to emit in place of the branch. """ eq, ge, le = BinaryOperator["=="], BinaryOperator[">="], BinaryOperator["<="] + if op == BinaryOperator["!="]: + return Qasm3Transformer.unroll_register_comparison( + reg_name, bit_indices, eq, value, else_block, if_block + ) if op == BinaryOperator[">"]: op, value = ge, value + 1 elif op == BinaryOperator["<"]: diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 2e5efd2b..abf0f595 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -91,6 +91,7 @@ # comparisons the unroller accepts between a classical bit and an integer BRANCH_COMPARISONS: dict[str, Callable[[Any, Any], bool]] = { "==": operator.eq, + "!=": operator.ne, ">=": operator.ge, "<=": operator.le, ">": operator.gt, @@ -2426,14 +2427,18 @@ def _visit_branching_statement( ) else: # unroll multi-bit branch - assert isinstance(rhs_value, int) and op in [ - qasm3_ast.BinaryOperator[o] for o in ["==", ">=", "<=", ">", "<"] - ] + assert isinstance(rhs_value, int) + assert op is not None and op.name in BRANCH_COMPARISONS size = self._global_creg_size_map[reg_name] # getting cregs for depth counting self._is_branch_clbits.update((reg_name, i) for i in range(size)) unrolled = Qasm3Transformer.unroll_register_comparison( - reg_name, range(size), op, rhs_value, if_block, else_block # type: ignore[arg-type] + reg_name, + range(size), + op, # type: ignore[arg-type] + rhs_value, + if_block, + else_block, ) result.extend(self.visit_basic_block(unrolled)) # type: ignore[arg-type] else: diff --git a/tests/qasm3/test_if.py b/tests/qasm3/test_if.py index 894e3e7f..c054b4a6 100644 --- a/tests/qasm3/test_if.py +++ b/tests/qasm3/test_if.py @@ -275,22 +275,23 @@ def test_register_equality_bits(width, value): assert _branch_bits(branch[-1:]) == {i: bool(value >> i & 1) for i in range(width)} -def _runs_body(statements, register_value): - """Whether ``x q[0]`` executes when the 1-qubit program's register holds ``register_value``.""" +def _executed_gate(statements, register_value): + """The gate the 1-qubit program runs when its register holds ``register_value``.""" for statement in statements: if isinstance(statement, qasm3_ast.QuantumGate): - return True + return statement.name.name if isinstance(statement, qasm3_ast.BranchingStatement): condition = statement.condition bit = register_value >> condition.lhs.index[0].value & 1 taken = statement.if_block if bit == condition.rhs.value else statement.else_block - if _runs_body(taken, register_value): - return True - return False + gate = _executed_gate(taken, register_value) + if gate: + return gate + return None @pytest.mark.parametrize("width", [1, 2, 3]) -@pytest.mark.parametrize("op", ["==", ">=", "<=", ">", "<"]) +@pytest.mark.parametrize("op", ["==", "!=", ">=", "<=", ">", "<"]) def test_register_condition_matches_integer_comparison(width, op): """Every unrolled comparison, including out-of-range values, agrees with integer comparison.""" for value in range(-1, 2**width + 1): @@ -300,14 +301,16 @@ def test_register_condition_matches_integer_comparison(width, op): bit[{width}] c; if (c {op} {value}) {{ x q[0]; + }} else {{ + h q[0]; }} """ result = loads(qasm) result.unroll() for register_value in range(2**width): - expected = eval(f"{register_value} {op} {value}") # pylint: disable=eval-used - assert ( - _runs_body(result.unrolled_ast.statements, register_value) == expected + holds = eval(f"{register_value} {op} {value}") # pylint: disable=eval-used + assert _executed_gate(result.unrolled_ast.statements, register_value) == ( + "x" if holds else "h" ), f"c {op} {value} with c = {register_value}" @@ -316,6 +319,7 @@ def test_register_condition_matches_integer_comparison(width, op): [ ("c[1] == 1", "if (c[1] == true) {\n x q[0];\n }"), ("c[1] < 1", "if (c[1] == false) {\n x q[0];\n }"), + ("c[1] != 1", "if (c[1] == false) {\n x q[0];\n }"), ("c[1] > 0", "if (c[1] == true) {\n x q[0];\n }"), ("c[1] >= 0", "x q[0];"), ("c[1] == 2", ""), @@ -391,7 +395,7 @@ def test_single_bit_condition(condition, expected_body): cx q; } """, - r"Only {==, >=, <=, >, <} supported in branching condition with classical register", + r"Only {==, !=, >=, <=, >, <} supported in branching condition with classical register", 8, 15, "c[0] >> 1",