Repository: Qiskit/qiskit-terra (python)

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_

Pull request description:
Fix QuantumCircuit.compose within control-flow scopes
### Summary

Previously, `QuantumCircuit.compose` with `inplace=True` would insert
the appended instructions outside the scope due to unchecked use of
`QuantumCircuit._append` (and direct modification of the `data`
attribute).  This now respects any active control-flow scopes.

Using `front=True` in conjunction with `inplace=True` within a
control-flow scope has no clear meaning, and so the behaviour is
forbidden.  Emitting a new circuit (`inplace=False`) while in an active
scope would produce a broken circuit - the active scope would need to be
cloned to ensure equivalence, but it wouldn't be part of any
Python-managed context, so the scope could never be closed.

<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->



### Details and comments

Fix #8433.

Patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
index 381d9cba6..9d6cf84c8 100644
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -835,7 +835,8 @@ class QuantumCircuit:
                 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 @@ class QuantumCircuit:
             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 @@ class QuantumCircuit:
                 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 @@ class QuantumCircuit:
             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)
diff --git a/releasenotes/notes/fix-QuantumCircuit.compose-in-control-flow-scopes-a8aad3b87efbe77c.yaml b/releasenotes/notes/fix-QuantumCircuit.compose-in-control-flow-scopes-a8aad3b87efbe77c.yaml
new file mode 100644
index 000000000..c6674cf88
--- /dev/null
+++ b/releasenotes/notes/fix-QuantumCircuit.compose-in-control-flow-scopes-a8aad3b87efbe77c.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+  - |
+    :meth:`.QuantumCircuit.compose` will now function correctly when used with
+    the ``inplace=True`` argument within control-flow builder contexts.
+    Previously the instructions would be added outside the control-flow scope.
+    Fixed `#8433 <https://github.com/Qiskit/qiskit-terra/issues/8433>`__.


Test patch:
diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py
index 369237684..0452ef8a7 100644
--- a/test/python/circuit/test_control_flow_builders.py
+++ b/test/python/circuit/test_control_flow_builders.py
@@ -2062,6 +2062,64 @@ class TestControlFlowBuilders(QiskitTestCase):
             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 @@ class TestControlFlowBuildersFailurePaths(QiskitTestCase):
         )
         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)