Repository: Qiskit/qiskit

Issue:
Cyclic-definition checking in `Clifford` segfaults Python on Windows
### Environment

- **Qiskit Terra version**: 0.24.1
- **Python version**: 3.8
- **Operating system**: Windows


### What is happening?

In the file `test/python/quantum_info/operators/symplectic/test_clifford.py`, the test `TestCliffordGates.test_from_gate_with_cyclic_definition` segfaults Python on Windows if the system recursion depth is greater than about 2700.  Importing `jedi` (which might happen via the Terra optional dependencies `ipython`, `seaborn` or `ipywidgets`) always sets the recursion limit to 3000.

### How can we reproduce the issue?

The root cause is any too-deep recursion.  If you need to test easily, you can put a call to `sys.setrecursionlimit(3000)` at the top of the test I mentioned above (or something like `import IPython` will do it for you).

### What should happen?

No segfault.

### Any suggestions?

I don't understand why `Clifford` is attempting to handle cyclic definitions - perhaps @itoko knows since the code was added in #9475.  Such gates are broken in the Terra data model, and would cause an infinite loop in many places in Qiskit - `definition` is (from its historical roots) meant to move towards the `["u", "cx"]` basis and should never cycle.

If the cyclic-definition handling wasn't added for some specific use, I'd strongly suggest that it and its test are removed.  If it was added with some use, then I'd first see if the use can be changed (because it's pretty broken), and if it _absolutely_ cannot be, then the cyclic handling needs fixing to not rely on `RecursionError` being raised.  For one that's very slow, and for two we can't rely on the user having changed the limit; `RecursionError` is meant to be a catch of last resort like `MemoryError`, not something to be relied on.

See davidhalter/jedi#1902 and davidhalter/jedi#1925 for more detail from the Jedi side - it's arguable that Jedi shouldn't be messing with the limit in a global manner, but either way, we shouldn't be relying on `RecursionError` being raised.


Discussion:

Resolving patch:
diff --git a/qiskit/quantum_info/operators/symplectic/clifford_circuits.py b/qiskit/quantum_info/operators/symplectic/clifford_circuits.py
--- a/qiskit/quantum_info/operators/symplectic/clifford_circuits.py
+++ b/qiskit/quantum_info/operators/symplectic/clifford_circuits.py
@@ -12,8 +12,9 @@
 """
 Circuit simulation for the Clifford class.
 """
+
 from __future__ import annotations
-import copy
+
 import numpy as np
 
 from qiskit.circuit import Barrier, Delay, Gate
@@ -21,14 +22,13 @@
 from qiskit.exceptions import QiskitError
 
 
-def _append_circuit(clifford, circuit, qargs=None, recursion_depth=0):
+def _append_circuit(clifford, circuit, qargs=None):
     """Update Clifford inplace by applying a Clifford circuit.
 
     Args:
         clifford (Clifford): The Clifford to update.
         circuit (QuantumCircuit): The circuit to apply.
         qargs (list or None): The qubits to apply circuit to.
-        recursion_depth (int): The depth of mutual recursion with _append_operation
 
     Returns:
         Clifford: the updated Clifford.
@@ -46,18 +46,17 @@ def _append_circuit(clifford, circuit, qargs=None, recursion_depth=0):
             )
         # Get the integer position of the flat register
         new_qubits = [qargs[circuit.find_bit(bit).index] for bit in instruction.qubits]
-        clifford = _append_operation(clifford, instruction.operation, new_qubits, recursion_depth)
+        clifford = _append_operation(clifford, instruction.operation, new_qubits)
     return clifford
 
 
-def _append_operation(clifford, operation, qargs=None, recursion_depth=0):
+def _append_operation(clifford, operation, qargs=None):
     """Update Clifford inplace by applying a Clifford operation.
 
     Args:
         clifford (Clifford): The Clifford to update.
         operation (Instruction or Clifford or str): The operation or composite operation to apply.
         qargs (list or None): The qubits to apply operation to.
-        recursion_depth (int): The depth of mutual recursion with _append_circuit
 
     Returns:
         Clifford: the updated Clifford.
@@ -135,16 +134,10 @@ def _append_operation(clifford, operation, qargs=None, recursion_depth=0):
     # This succeeds only if the gate has all-Clifford definition (decomposition).
     # If fails, we need to restore the clifford that was before attempting to unroll and append.
     if gate.definition is not None:
-        if recursion_depth > 0:
-            return _append_circuit(clifford, gate.definition, qargs, recursion_depth + 1)
-        else:  # recursion_depth == 0
-            # clifford may be updated in _append_circuit
-            org_clifford = copy.deepcopy(clifford)
-            try:
-                return _append_circuit(clifford, gate.definition, qargs, 1)
-            except (QiskitError, RecursionError):
-                # discard incompletely updated clifford and continue
-                clifford = org_clifford
+        try:
+            return _append_circuit(clifford.copy(), gate.definition, qargs)
+        except QiskitError:
+            pass
 
     # As a final attempt, if the gate is up to 3 qubits,
     # we try to construct a Clifford to be appended from its matrix representation.


Test patch:
diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py
--- a/test/python/quantum_info/operators/symplectic/test_clifford.py
+++ b/test/python/quantum_info/operators/symplectic/test_clifford.py
@@ -555,39 +555,6 @@ def test_from_circuit_with_all_types(self):
         expected_clifford = Clifford.from_dict(expected_clifford_dict)
         self.assertEqual(combined_clifford, expected_clifford)
 
-    def test_from_gate_with_cyclic_definition(self):
-        """Test if a Clifford can be created from gate with cyclic definition"""
-
-        class MyHGate(HGate):
-            """Custom HGate class for test"""
-
-            def __init__(self):
-                super().__init__()
-                self.name = "my_h"
-
-            def _define(self):
-                qc = QuantumCircuit(1, name=self.name)
-                qc.s(0)
-                qc.append(MySXGate(), [0])
-                qc.s(0)
-                self.definition = qc
-
-        class MySXGate(SXGate):
-            """Custom SXGate class for test"""
-
-            def __init__(self):
-                super().__init__()
-                self.name = "my_sx"
-
-            def _define(self):
-                qc = QuantumCircuit(1, name=self.name)
-                qc.sdg(0)
-                qc.append(MyHGate(), [0])
-                qc.sdg(0)
-                self.definition = qc
-
-        Clifford(MyHGate())
-
 
 @ddt
 class TestCliffordSynthesis(QiskitTestCase):