Repository: Qiskit/qiskit-terra (python)

Issue:
Transpiler pass for converting `c_if` to `if_test`
### What should we add?

As developers, we want to transition away from `Instruction.condition` (via the `Instruction{,Set}.c_if` methods) to remove unnecessary state from `Instruction` subclasses.  Part of the backwards compatibility for this support can be a transpiler pass that converts instructions with non-`None` conditions to instances `IfElseBlock`.

This has two main benefits as a temporary measure:
- we will have a path for converting old-style `Instruction` instances within the transpiler, without immediately needing to change the behaviour of `Instruction.c_if`, which is very hard to do within the deprecation policy.
- the dynamic-circuits paths in the IBM provider prefer to have conditionals in this form, in order to more easily apply dynamic decoupling against blocks of instructions.

The first point is the justification for why this is useful to be in Terra (albeit not yet in the preset pass managers - the rest of the support for `IfElseBlock` is too shallow right now), and the second point is why we want this sooner rather than later.

Potential further work: complex conditionals (e.g. `custom_gate.c_if()`) are likely to be expanded by the basis translator into many conditional gates all with the same condition.  Ideally these would be transformed by this pass _before_ basis translation so that they all appear in the same `IfElseBlock`, but right now the support for the control-flow operations in the rest of the transpiler (especially routing) is still being worked on.  One could have a separate classical optimisation pass that runs during optimisation that collects runs of conditionals on the same condition into a single `IfElseBlock`.  This might be moving Terra too far in the direction of classical optimisation, however, and may be better suited for other libraries.

cc: @taalexander.

Pull request description:
Add conversion transpiler pass from `c_if` to `IfElseOp`
### Summary

This is intended to be used to ease the transition from the old-style conditions to the new-style form.  Backends may use it in their custom pipelines, so they can guarantee they only need to deal with one form.

A further classical optimisation may be to group `IfElseOp` blocks that share the same condition into a single operation, but this would likely be a separate pass, if added, or potentially be the domain of a separate part of the circuit-description/compilation/running pipeline.

### Details

Fixes #8601.

Will need rebasing over #8627 and #8615 once they merge, as this depends on them, and currently includes their commits.  I just can't look at my screen any more tonight to wait til it merges.

*edit*: This is now rebased.

cc: @taalexander (this is the pass I wrote for you)

Patch:
diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py
index 8a08566c4..a85409366 100644
--- a/qiskit/transpiler/passes/__init__.py
+++ b/qiskit/transpiler/passes/__init__.py
@@ -170,6 +170,7 @@ Additional Passes
    FixedPoint
    ContainsInstruction
    GatesInBasis
+   ConvertConditionsToIfOps
 """
 
 # layout selection (placement)
@@ -278,3 +279,4 @@ from .utils import Error
 from .utils import RemoveBarriers
 from .utils import ContainsInstruction
 from .utils import GatesInBasis
+from .utils import ConvertConditionsToIfOps
diff --git a/qiskit/transpiler/passes/utils/__init__.py b/qiskit/transpiler/passes/utils/__init__.py
index 65209c35b..311bae726 100644
--- a/qiskit/transpiler/passes/utils/__init__.py
+++ b/qiskit/transpiler/passes/utils/__init__.py
@@ -26,6 +26,7 @@ from .error import Error
 from .remove_barriers import RemoveBarriers
 from .contains_instruction import ContainsInstruction
 from .gates_basis import GatesInBasis
+from .convert_conditions_to_if_ops import ConvertConditionsToIfOps
 
 # Utility functions
 from . import control_flow
diff --git a/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py b/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py
new file mode 100644
index 000000000..c5ff85926
--- /dev/null
+++ b/qiskit/transpiler/passes/utils/convert_conditions_to_if_ops.py
@@ -0,0 +1,86 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2022.
+#
+# This code is licensed under the Apache License, Version 2.0. You may
+# obtain a copy of this license in the LICENSE.txt file in the root directory
+# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# Any modifications or derivative works of this code must retain this
+# copyright notice, and modified files need to carry a notice indicating
+# that they have been altered from the originals.
+
+"""Replace conditional instructions with equivalent :class:`.IfElseOp` objects."""
+
+from qiskit.converters import dag_to_circuit, circuit_to_dag
+from qiskit.circuit import (
+    CircuitInstruction,
+    ClassicalRegister,
+    Clbit,
+    ControlFlowOp,
+    IfElseOp,
+    QuantumCircuit,
+)
+from qiskit.dagcircuit import DAGCircuit
+from qiskit.transpiler import TransformationPass
+
+
+class ConvertConditionsToIfOps(TransformationPass):
+    """Convert instructions whose ``condition`` attribute is set to a non-``None`` value into the
+    equivalent single-statement :class:`.IfElseBlock`.
+
+    This is a simple pass aimed at easing the conversion from the old style of using
+    :meth:`.InstructionSet.c_if` into the new style of using more complex conditional logic."""
+
+    def _run_inner(self, dag):
+        """Run the pass on one :class:`.DAGCircuit`, mutating it.  Returns ``True`` if the circuit
+        was modified and ``False`` if not."""
+        modified = False
+        for node in dag.op_nodes():
+            if isinstance(node.op, ControlFlowOp):
+                modified_blocks = False
+                new_dags = []
+                for block in node.op.blocks:
+                    new_dag = circuit_to_dag(block)
+                    modified_blocks |= self._run_inner(new_dag)
+                    new_dags.append(new_dag)
+                if not modified_blocks:
+                    continue
+                dag.substitute_node(
+                    node,
+                    node.op.replace_blocks(dag_to_circuit(block) for block in new_dags),
+                    inplace=True,
+                )
+            elif getattr(node.op, "condition", None) is None:
+                continue
+            else:
+                target, value = node.op.condition
+                clbits = list(node.cargs)
+                condition_clbits = [target] if isinstance(target, Clbit) else list(target)
+                clbits_set = set(clbits)
+                clbits += [bit for bit in condition_clbits if bit not in clbits_set]
+                block_body = QuantumCircuit(list(node.qargs) + clbits)
+                if isinstance(target, ClassicalRegister):
+                    block_body.add_register(target)
+                new_op = node.op.copy()
+                new_op.condition = None
+                block_body._append(CircuitInstruction(new_op, node.qargs, node.cargs))
+                # Despite only being a node-for-node replacement, control-flow ops contain the
+                # condition bits in their cargs, which requires slightly different handling in the
+                # DAGCircuit methods right now.
+                replacement = DAGCircuit()
+                replacement.add_qubits(block_body.qubits)
+                replacement.add_clbits(block_body.clbits)
+                if isinstance(target, ClassicalRegister):
+                    replacement.add_creg(target)
+                replacement.apply_operation_back(
+                    IfElseOp((target, value), block_body), block_body.qubits, block_body.clbits
+                )
+                wire_map = {bit: bit for bit in block_body.qubits + block_body.clbits}
+                dag.substitute_node_with_dag(node, replacement, wire_map, propagate_condition=False)
+            modified = True
+        return modified
+
+    def run(self, dag):
+        self._run_inner(dag)
+        return dag
diff --git a/releasenotes/notes/c_if-to-if_else-converter-2d48046de31814a8.yaml b/releasenotes/notes/c_if-to-if_else-converter-2d48046de31814a8.yaml
new file mode 100644
index 000000000..65c93ef53
--- /dev/null
+++ b/releasenotes/notes/c_if-to-if_else-converter-2d48046de31814a8.yaml
@@ -0,0 +1,9 @@
+---
+features:
+  - |
+    A new transpiler pass, :class:`.ConvertConditionsToIfOps` was added, which
+    can be manually run to convert old-style :meth:`.Instruction.c_if`-conditioned
+    instructions into :class:`.IfElseOp` objects.  This is to help ease the transition
+    from the old type to the new type for backends.  For most users, there is no
+    need to add this to your pass managers, and it is not included in any preset
+    pass managers.


Test patch:
diff --git a/test/python/transpiler/test_convert_conditions_to_if_ops.py b/test/python/transpiler/test_convert_conditions_to_if_ops.py
new file mode 100644
index 000000000..8ddf6f2d1
--- /dev/null
+++ b/test/python/transpiler/test_convert_conditions_to_if_ops.py
@@ -0,0 +1,139 @@
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2022.
+#
+# This code is licensed under the Apache License, Version 2.0. You may
+# obtain a copy of this license in the LICENSE.txt file in the root directory
+# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# Any modifications or derivative works of this code must retain this
+# copyright notice, and modified files need to carry a notice indicating
+# that they have been altered from the originals.
+
+# pylint: disable=missing-class-docstring,missing-module-docstring
+
+from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister, Qubit, Clbit
+from qiskit.test import QiskitTestCase
+from qiskit.test._canonical import canonicalize_control_flow
+from qiskit.transpiler import PassManager
+from qiskit.transpiler.passes import ConvertConditionsToIfOps
+
+
+class TestConvertConditionsToIfOps(QiskitTestCase):
+    def test_simple_loose_bits(self):
+        """Test that basic conversions work when operating on loose classical bits."""
+        bits = [Qubit(), Qubit(), Clbit(), Clbit()]
+
+        base = QuantumCircuit(bits)
+        base.h(0)
+        base.x(0).c_if(0, 1)
+        base.z(1).c_if(1, 0)
+        base.measure(0, 0)
+        base.measure(1, 1)
+        base.h(0)
+        base.x(0).c_if(0, 1)
+        base.cx(0, 1).c_if(1, 0)
+
+        expected = QuantumCircuit(bits)
+        expected.h(0)
+        with expected.if_test((expected.clbits[0], True)):
+            expected.x(0)
+        with expected.if_test((expected.clbits[1], False)):
+            expected.z(1)
+        expected.measure(0, 0)
+        expected.measure(1, 1)
+        expected.h(0)
+        with expected.if_test((expected.clbits[0], True)):
+            expected.x(0)
+        with expected.if_test((expected.clbits[1], False)):
+            expected.cx(0, 1)
+        expected = canonicalize_control_flow(expected)
+
+        output = PassManager([ConvertConditionsToIfOps()]).run(base)
+        self.assertEqual(output, expected)
+
+    def test_simple_registers(self):
+        """Test that basic conversions work when operating on conditions over registers."""
+        registers = [QuantumRegister(2), ClassicalRegister(2), ClassicalRegister(1)]
+
+        base = QuantumCircuit(*registers)
+        base.h(0)
+        base.x(0).c_if(base.cregs[0], 1)
+        base.z(1).c_if(base.cregs[1], 0)
+        base.measure(0, 0)
+        base.measure(1, 2)
+        base.h(0)
+        base.x(0).c_if(base.cregs[0], 1)
+        base.cx(0, 1).c_if(base.cregs[1], 0)
+
+        expected = QuantumCircuit(*registers)
+        expected.h(0)
+        with expected.if_test((expected.cregs[0], 1)):
+            expected.x(0)
+        with expected.if_test((expected.cregs[1], 0)):
+            expected.z(1)
+        expected.measure(0, 0)
+        expected.measure(1, 2)
+        expected.h(0)
+        with expected.if_test((expected.cregs[0], 1)):
+            expected.x(0)
+        with expected.if_test((expected.cregs[1], 0)):
+            expected.cx(0, 1)
+        expected = canonicalize_control_flow(expected)
+
+        output = PassManager([ConvertConditionsToIfOps()]).run(base)
+        self.assertEqual(output, expected)
+
+    def test_nested_control_flow(self):
+        """Test that the pass successfully converts instructions nested within control-flow
+        blocks."""
+        bits = [Clbit()]
+        registers = [QuantumRegister(3), ClassicalRegister(2)]
+
+        base = QuantumCircuit(*registers, bits)
+        base.x(0).c_if(bits[0], False)
+        with base.if_test((base.cregs[0], 0)) as else_:
+            base.z(1).c_if(bits[0], False)
+        with else_:
+            base.z(1).c_if(base.cregs[0], 1)
+        with base.for_loop(range(2)):
+            with base.while_loop((base.cregs[0], 1)):
+                base.cx(1, 2).c_if(base.cregs[0], 1)
+        base = canonicalize_control_flow(base)
+
+        expected = QuantumCircuit(*registers, bits)
+        with expected.if_test((bits[0], False)):
+            expected.x(0)
+        with expected.if_test((expected.cregs[0], 0)) as else_:
+            with expected.if_test((bits[0], False)):
+                expected.z(1)
+        with else_:
+            with expected.if_test((expected.cregs[0], 1)):
+                expected.z(1)
+        with expected.for_loop(range(2)):
+            with expected.while_loop((expected.cregs[0], 1)):
+                with expected.if_test((expected.cregs[0], 1)):
+                    expected.cx(1, 2)
+        expected = canonicalize_control_flow(expected)
+
+        output = PassManager([ConvertConditionsToIfOps()]).run(base)
+        self.assertEqual(output, expected)
+
+    def test_no_op(self):
+        """Test that the pass works when recursing into control-flow structures, but there's nothing
+        that actually needs replacing."""
+        bits = [Clbit()]
+        registers = [QuantumRegister(3), ClassicalRegister(2)]
+
+        base = QuantumCircuit(*registers, bits)
+        base.x(0)
+        with base.if_test((base.cregs[0], 0)) as else_:
+            base.z(1)
+        with else_:
+            base.z(2)
+        with base.for_loop(range(2)):
+            with base.while_loop((base.cregs[0], 1)):
+                base.cx(1, 2)
+        base = canonicalize_control_flow(base)
+        output = PassManager([ConvertConditionsToIfOps()]).run(base)
+        self.assertEqual(output, base)