Repository: Qiskit/qiskit

Issue:
ControlFlow context manager places "composed" elements outside scope
### Environment

- **Qiskit Terra version**: 0.21.0rc1-45
- **Python version**: 3.10
- **Operating system**: Linux


### What is happening?

When using the `compose` method within a ControlFlow context builder the composed element is inserted at the top level scope.



### How can we reproduce the issue?

```python
from qiskit.circuit.library.standard_gates.x import CCXGate
qreg = QuantumRegister(3)
creg = ClassicalRegister(3)
circ = QuantumCircuit(qreg, creg)
with circ.for_loop(range(3)):
    circ.x(qreg)
    circ.compose(CCXGate().definition, inplace=True)
print(circ)
```

yields,

```
                                                        ┌───┐      ┌───────────┐
q0_0: ───────────────────■─────────────────────■────■───┤ T ├───■──┤0          ├
                         │             ┌───┐   │  ┌─┴─┐┌┴───┴┐┌─┴─┐│           │
q0_1: ───────■───────────┼─────────■───┤ T ├───┼──┤ X ├┤ Tdg ├┤ X ├┤1 For_loop ├
      ┌───┐┌─┴─┐┌─────┐┌─┴─┐┌───┐┌─┴─┐┌┴───┴┐┌─┴─┐├───┤└┬───┬┘└───┘│           │
q0_2: ┤ H ├┤ X ├┤ Tdg ├┤ X ├┤ T ├┤ X ├┤ Tdg ├┤ X ├┤ T ├─┤ H ├──────┤2          ├
      └───┘└───┘└─────┘└───┘└───┘└───┘└─────┘└───┘└───┘ └───┘      └───────────┘
c0: 3/══════════════════════════════════════════════════════════════════════════
```

### What should happen?

The composed circuit should appear inside the `for-loop` as an appended instruction currently does.

### Any suggestions?

_No response_


Discussion:
If you just want the decomposed CCX gate 3 times, you can can use a for loop like so:

from qiskit.circuit.library.standard_gates.x import CCXGate
from qiskit import *
qreg = QuantumRegister(3)
creg = ClassicalRegister(3)
circ = QuantumCircuit(qreg, creg)
for i in (range(3)):
        circ.x(qreg)
        circ.compose(CCXGate().definition, inplace=True)
print(circ)
Thanks for the report, Erick.  The issue is that `QuantumCircuit.compose` manipulates `.data` directly, or calls `QuantumCircuit._append` without doing the requisite checks on the control-flow scoping (noted in its docstring).  It certainly needs a fix - I'll have a look when I've finished combing through my backlog.

Resolving patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -835,7 +835,8 @@ def compose(
                 this can be anything that :obj:`.append` will accept.
             qubits (list[Qubit|int]): qubits of self to compose onto.
             clbits (list[Clbit|int]): clbits of self to compose onto.
-            front (bool): If True, front composition will be performed (not implemented yet).
+            front (bool): If True, front composition will be performed.  This is not possible within
+                control-flow builder context managers.
             inplace (bool): If True, modify the object. Otherwise return composed circuit.
             wrap (bool): If True, wraps the other circuit into a gate (or instruction, depending on
                 whether it contains only unitary instructions) before composing it onto self.
@@ -844,12 +845,18 @@ def compose(
             QuantumCircuit: the composed circuit (returns None if inplace==True).
 
         Raises:
-            CircuitError: if composing on the front.
-            QiskitError: if ``other`` is wider or there are duplicate edge mappings.
+            CircuitError: if no correct wire mapping can be made between the two circuits, such as
+                if ``other`` is wider than ``self``.
+            CircuitError: if trying to emit a new circuit while ``self`` has a partially built
+                control-flow context active, such as the context-manager forms of :meth:`if_test`,
+                :meth:`for_loop` and :meth:`while_loop`.
+            CircuitError: if trying to compose to the front of a circuit when a control-flow builder
+                block is active; there is no clear meaning to this action.
 
-        Examples::
+        Examples:
+            .. code-block:: python
 
-            lhs.compose(rhs, qubits=[3, 2], inplace=True)
+                >>> lhs.compose(rhs, qubits=[3, 2], inplace=True)
 
             .. parsed-literal::
 
@@ -869,6 +876,19 @@ def compose(
                 lcr_1: 0 ═══════════                           lcr_1: 0 ═══════════════════════
 
         """
+        if inplace and front and self._control_flow_scopes:
+            # If we're composing onto ourselves while in a stateful control-flow builder context,
+            # there's no clear meaning to composition to the "front" of the circuit.
+            raise CircuitError(
+                "Cannot compose to the front of a circuit while a control-flow context is active."
+            )
+        if not inplace and self._control_flow_scopes:
+            # If we're inside a stateful control-flow builder scope, even if we successfully cloned
+            # the partial builder scope (not simple), the scope wouldn't be controlled by an active
+            # `with` statement, so the output circuit would be permanently broken.
+            raise CircuitError(
+                "Cannot emit a new composed circuit while a control-flow context is active."
+            )
 
         if inplace:
             dest = self
@@ -962,8 +982,9 @@ def compose(
             mapped_instrs += dest.data
             dest.data.clear()
             dest._parameter_table.clear()
+        append = dest._control_flow_scopes[-1].append if dest._control_flow_scopes else dest._append
         for instr in mapped_instrs:
-            dest._append(instr)
+            append(instr)
 
         for gate, cals in other.calibrations.items():
             dest._calibrations[gate].update(cals)


Test patch:
diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py
--- a/test/python/circuit/test_control_flow_builders.py
+++ b/test/python/circuit/test_control_flow_builders.py
@@ -2062,6 +2062,64 @@ def test_copy_of_instruction_parameters(self):
             self.assertEqual(while_body, copy.copy(while_body))
             self.assertEqual(while_body, copy.deepcopy(while_body))
 
+    def test_inplace_compose_within_builder(self):
+        """Test that QuantumCircuit.compose used in-place works as expected within control-flow
+        scopes."""
+        inner = QuantumCircuit(1)
+        inner.x(0)
+
+        base = QuantumCircuit(1, 1)
+        base.h(0)
+        base.measure(0, 0)
+
+        with self.subTest("if"):
+            outer = base.copy()
+            with outer.if_test((outer.clbits[0], 1)):
+                outer.compose(inner, inplace=True)
+
+            expected = base.copy()
+            with expected.if_test((expected.clbits[0], 1)):
+                expected.x(0)
+
+            self.assertCircuitsEquivalent(outer, expected)
+
+        with self.subTest("else"):
+            outer = base.copy()
+            with outer.if_test((outer.clbits[0], 1)) as else_:
+                outer.compose(inner, inplace=True)
+            with else_:
+                outer.compose(inner, inplace=True)
+
+            expected = base.copy()
+            with expected.if_test((expected.clbits[0], 1)) as else_:
+                expected.x(0)
+            with else_:
+                expected.x(0)
+
+            self.assertCircuitsEquivalent(outer, expected)
+
+        with self.subTest("for"):
+            outer = base.copy()
+            with outer.for_loop(range(3)):
+                outer.compose(inner, inplace=True)
+
+            expected = base.copy()
+            with expected.for_loop(range(3)):
+                expected.x(0)
+
+            self.assertCircuitsEquivalent(outer, expected)
+
+        with self.subTest("while"):
+            outer = base.copy()
+            with outer.while_loop((outer.clbits[0], 0)):
+                outer.compose(inner, inplace=True)
+
+            expected = base.copy()
+            with expected.while_loop((outer.clbits[0], 0)):
+                expected.x(0)
+
+            self.assertCircuitsEquivalent(outer, expected)
+
 
 @ddt.ddt
 class TestControlFlowBuildersFailurePaths(QiskitTestCase):
@@ -2458,3 +2516,28 @@ def dummy_requester(resource):
         )
         with self.assertRaisesRegex(TypeError, r"Can only add qubits or classical bits.*"):
             builder_block.add_bits([bit])
+
+    def test_compose_front_inplace_invalid_within_builder(self):
+        """Test that `QuantumCircuit.compose` raises a sensible error when called within a
+        control-flow builder block."""
+        inner = QuantumCircuit(1)
+        inner.x(0)
+
+        outer = QuantumCircuit(1, 1)
+        outer.measure(0, 0)
+        outer.compose(inner, front=True, inplace=True)
+        with outer.if_test((outer.clbits[0], 1)):
+            with self.assertRaisesRegex(CircuitError, r"Cannot compose to the front.*"):
+                outer.compose(inner, front=True, inplace=True)
+
+    def test_compose_new_invalid_within_builder(self):
+        """Test that `QuantumCircuit.compose` raises a sensible error when called within a
+        control-flow builder block if trying to emit a new circuit."""
+        inner = QuantumCircuit(1)
+        inner.x(0)
+
+        outer = QuantumCircuit(1, 1)
+        outer.measure(0, 0)
+        with outer.if_test((outer.clbits[0], 1)):
+            with self.assertRaisesRegex(CircuitError, r"Cannot emit a new composed circuit.*"):
+                outer.compose(inner, inplace=False)