Repository: Qiskit/qiskit

Issue:
Add support for `Expr` values to `SabreSwap` and `SabreLayout`
### What should we add?

Our Sabre implementation works by having Python-space pass a stripped-down, minimal version of the data-flow dependencies down to the Rust layer.  This involves a step where wires need to be copied into the new data structures.  For historical reasons, the way this is currently done is _not_ by inspecting the wires of the `DAGCircuit` data structure, but they are re-inferred from the nodes themselves.

The two passes need updating to ensure that any wires implied by the `Expr` values are also taken into account.  The work will be almost identical between the two passes.

Rather than having special handling for `Expr` values, an alternative implementation may be to infer the data-flow dependencies from the wires in the DAG structure, rather than the nodes.

Depends on #10223, #10224, #9419 and #9421.


Discussion:

Resolving patch:
diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py
--- a/qiskit/transpiler/passes/routing/sabre_swap.py
+++ b/qiskit/transpiler/passes/routing/sabre_swap.py
@@ -17,8 +17,9 @@
 
 import rustworkx
 
-from qiskit.circuit import ControlFlowOp
+from qiskit.circuit import SwitchCaseOp, ControlFlowOp, Clbit, ClassicalRegister
 from qiskit.circuit.library.standard_gates import SwapGate
+from qiskit.circuit.controlflow import condition_resources, node_resources
 from qiskit.converters import dag_to_circuit
 from qiskit.transpiler.basepasses import TransformationPass
 from qiskit.transpiler.coupling import CouplingMap
@@ -285,10 +286,18 @@ def process_dag(block_dag, wire_map):
         dag_list = []
         node_blocks = {}
         for node in block_dag.topological_op_nodes():
-            cargs = {block_dag.find_bit(x).index for x in node.cargs}
+            cargs_bits = set(node.cargs)
             if node.op.condition is not None:
-                for clbit in block_dag._bits_in_operation(node.op):
-                    cargs.add(block_dag.find_bit(clbit).index)
+                cargs_bits.update(condition_resources(node.op.condition).clbits)
+            if isinstance(node.op, SwitchCaseOp):
+                target = node.op.target
+                if isinstance(target, Clbit):
+                    cargs_bits.add(target)
+                elif isinstance(target, ClassicalRegister):
+                    cargs_bits.update(target)
+                else:  # Expr
+                    cargs_bits.update(node_resources(target).clbits)
+            cargs = {block_dag.find_bit(x).index for x in cargs_bits}
             if isinstance(node.op, ControlFlowOp):
                 node_blocks[node._node_id] = [
                     recurse(


Test patch:
diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py
--- a/test/python/transpiler/test_sabre_swap.py
+++ b/test/python/transpiler/test_sabre_swap.py
@@ -18,8 +18,9 @@
 import ddt
 import numpy.random
 
-from qiskit.circuit import Clbit, ControlFlowOp
+from qiskit.circuit import Clbit, ControlFlowOp, Qubit
 from qiskit.circuit.library import CCXGate, HGate, Measure, SwapGate
+from qiskit.circuit.classical import expr
 from qiskit.circuit.random import random_circuit
 from qiskit.compiler.transpiler import transpile
 from qiskit.converters import circuit_to_dag, dag_to_circuit
@@ -730,6 +731,44 @@ def test_pre_intra_post_if_else(self):
         expected.measure(qreg, creg[[1, 2, 0, 3, 4]])
         self.assertEqual(dag_to_circuit(cdag), expected)
 
+    def test_if_expr(self):
+        """Test simple if conditional with an `Expr` condition."""
+        coupling = CouplingMap.from_line(4)
+
+        body = QuantumCircuit(4)
+        body.cx(0, 1)
+        body.cx(0, 2)
+        body.cx(0, 3)
+        qc = QuantumCircuit(4, 2)
+        qc.if_test(expr.logic_and(qc.clbits[0], qc.clbits[1]), body, [0, 1, 2, 3], [])
+
+        dag = circuit_to_dag(qc)
+        cdag = SabreSwap(coupling, "lookahead", seed=58, trials=1).run(dag)
+        check_map_pass = CheckMap(coupling)
+        check_map_pass.run(cdag)
+        self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
+
+    def test_if_else_expr(self):
+        """Test simple if/else conditional with an `Expr` condition."""
+        coupling = CouplingMap.from_line(4)
+
+        true = QuantumCircuit(4)
+        true.cx(0, 1)
+        true.cx(0, 2)
+        true.cx(0, 3)
+        false = QuantumCircuit(4)
+        false.cx(3, 0)
+        false.cx(3, 1)
+        false.cx(3, 2)
+        qc = QuantumCircuit(4, 2)
+        qc.if_else(expr.logic_and(qc.clbits[0], qc.clbits[1]), true, false, [0, 1, 2, 3], [])
+
+        dag = circuit_to_dag(qc)
+        cdag = SabreSwap(coupling, "lookahead", seed=58, trials=1).run(dag)
+        check_map_pass = CheckMap(coupling)
+        check_map_pass.run(cdag)
+        self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
+
     def test_no_layout_change(self):
         """test controlflow with no layout change needed"""
         num_qubits = 5
@@ -841,6 +880,54 @@ def test_while_loop(self):
         expected.measure(qreg, creg)
         self.assertEqual(dag_to_circuit(cdag), expected)
 
+    def test_while_loop_expr(self):
+        """Test simple while loop with an `Expr` condition."""
+        coupling = CouplingMap.from_line(4)
+
+        body = QuantumCircuit(4)
+        body.cx(0, 1)
+        body.cx(0, 2)
+        body.cx(0, 3)
+        qc = QuantumCircuit(4, 2)
+        qc.while_loop(expr.logic_and(qc.clbits[0], qc.clbits[1]), body, [0, 1, 2, 3], [])
+
+        dag = circuit_to_dag(qc)
+        cdag = SabreSwap(coupling, "lookahead", seed=82, trials=1).run(dag)
+        check_map_pass = CheckMap(coupling)
+        check_map_pass.run(cdag)
+        self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
+
+    def test_switch_implicit_carg_use(self):
+        """Test that a switch statement that uses cargs only implicitly via its ``target`` attribute
+        and not explicitly in bodies of the cases is routed correctly, with the dependencies
+        fulfilled correctly."""
+        coupling = CouplingMap.from_line(4)
+        pass_ = SabreSwap(coupling, "lookahead", seed=82, trials=1)
+
+        body = QuantumCircuit([Qubit()])
+        body.x(0)
+
+        # If the classical wire condition isn't respected, then the switch would appear in the front
+        # layer and be immediately eligible for routing, which would produce invalid output.
+        qc = QuantumCircuit(4, 1)
+        qc.cx(0, 1)
+        qc.cx(1, 2)
+        qc.cx(0, 2)
+        qc.measure(2, 0)
+        qc.switch(expr.lift(qc.clbits[0]), [(False, body.copy()), (True, body.copy())], [3], [])
+
+        expected = QuantumCircuit(4, 1)
+        expected.cx(0, 1)
+        expected.cx(1, 2)
+        expected.swap(2, 1)
+        expected.cx(0, 1)
+        expected.measure(1, 0)
+        expected.switch(
+            expr.lift(expected.clbits[0]), [(False, body.copy()), (True, body.copy())], [3], []
+        )
+
+        self.assertEqual(pass_(qc), expected)
+
     def test_switch_single_case(self):
         """Test routing of 'switch' with just a single case."""
         qreg = QuantumRegister(5, "q")
@@ -923,6 +1010,88 @@ def test_switch_nonexhaustive(self):
 
         self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected))
 
+    def test_switch_expr_single_case(self):
+        """Test routing of 'switch' with an `Expr` target and just a single case."""
+        qreg = QuantumRegister(5, "q")
+        creg = ClassicalRegister(3, "c")
+        qc = QuantumCircuit(qreg, creg)
+
+        case0 = QuantumCircuit(qreg[[0, 1, 2]], creg[:])
+        case0.cx(0, 1)
+        case0.cx(1, 2)
+        case0.cx(2, 0)
+        qc.switch(expr.bit_or(creg, 5), [(0, case0)], qreg[[0, 1, 2]], creg)
+
+        coupling = CouplingMap.from_line(len(qreg))
+        pass_ = SabreSwap(coupling, "lookahead", seed=82, trials=1)
+        test = pass_(qc)
+
+        check = CheckMap(coupling)
+        check(test)
+        self.assertTrue(check.property_set["is_swap_mapped"])
+
+        expected = QuantumCircuit(qreg, creg)
+        case0 = QuantumCircuit(qreg[[0, 1, 2]], creg[:])
+        case0.cx(0, 1)
+        case0.cx(1, 2)
+        case0.swap(0, 1)
+        case0.cx(2, 1)
+        case0.swap(0, 1)
+        expected.switch(expr.bit_or(creg, 5), [(0, case0)], qreg[[0, 1, 2]], creg[:])
+
+        self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected))
+
+    def test_switch_expr_nonexhaustive(self):
+        """Test routing of 'switch' with an `Expr` target and several but nonexhaustive cases."""
+        qreg = QuantumRegister(5, "q")
+        creg = ClassicalRegister(3, "c")
+
+        qc = QuantumCircuit(qreg, creg)
+        case0 = QuantumCircuit(qreg, creg[:])
+        case0.cx(0, 1)
+        case0.cx(1, 2)
+        case0.cx(2, 0)
+        case1 = QuantumCircuit(qreg, creg[:])
+        case1.cx(1, 2)
+        case1.cx(2, 3)
+        case1.cx(3, 1)
+        case2 = QuantumCircuit(qreg, creg[:])
+        case2.cx(2, 3)
+        case2.cx(3, 4)
+        case2.cx(4, 2)
+        qc.switch(expr.bit_or(creg, 5), [(0, case0), ((1, 2), case1), (3, case2)], qreg, creg)
+
+        coupling = CouplingMap.from_line(len(qreg))
+        pass_ = SabreSwap(coupling, "lookahead", seed=82, trials=1)
+        test = pass_(qc)
+
+        check = CheckMap(coupling)
+        check(test)
+        self.assertTrue(check.property_set["is_swap_mapped"])
+
+        expected = QuantumCircuit(qreg, creg)
+        case0 = QuantumCircuit(qreg, creg[:])
+        case0.cx(0, 1)
+        case0.cx(1, 2)
+        case0.swap(0, 1)
+        case0.cx(2, 1)
+        case0.swap(0, 1)
+        case1 = QuantumCircuit(qreg, creg[:])
+        case1.cx(1, 2)
+        case1.cx(2, 3)
+        case1.swap(1, 2)
+        case1.cx(3, 2)
+        case1.swap(1, 2)
+        case2 = QuantumCircuit(qreg, creg[:])
+        case2.cx(2, 3)
+        case2.cx(3, 4)
+        case2.swap(2, 3)
+        case2.cx(4, 3)
+        case2.swap(2, 3)
+        expected.switch(expr.bit_or(creg, 5), [(0, case0), ((1, 2), case1), (3, case2)], qreg, creg)
+
+        self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected))
+
     def test_nested_inner_cnot(self):
         """test swap in nested if else controlflow construct; swap in inner"""
         num_qubits = 3