Repository: Qiskit/qiskit

Issue:
`QuantumCircuit.qasm` can output invalid gate names
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**: 0.19dev @ 6b08154
- **Python version**: any
- **Operating system**: any

### What is the current behavior?

`Instruction` instances can be given any Python string as a name.  If the name is not of a known gate, the QASM 2 dumper will create a `gate <name> { ... }` block to describe it, and use `Instruction.name` verbatim.  If the name contains invalid characters for a QASM 2 identifier, they will be reproduced immediately.

### Steps to reproduce the problem

There's currently a bug in `QuantumCircuit.qasm` when working with `UnitaryGate` (#7051) which will throw a type error earlier, but in principle
```python
import qiskit
qv = qiskit.circuit.library.QuantumVolume(2)
print(qv.qasm())
```
_would_ produce something like
```text
OPENQASM 2.0;
include "qelib1.inc";
gate su4_755 q0,q1 { <truncated> }
gate su4_747 q0,q1 { <truncated> }
gate quantum_volume_[2,2,257] q0,q1 { su4_755 q0,q1; su4_747 q1,q0; }
//   ^^^^^^^^^^^^^^^^^^^^^^^^ error
qreg q[2];
quantum_volume_[2,2,257] q[0],q[1];
```

### What is the expected behavior?

Output identifiers should be valid.


### Suggested solutions

Either we only allow `Instruction.name` to be valid QASM identifiers, or we escape invalid characters when outputting the identifiers.  Using escapes (e.g. just replacing all invalid characters by an underscore) is probably the best, because it'll be the easiest to work with when we have QASM 2 and 3 outputters - QASM 3 is far less restrictive in what's an allowed identifier, and we're more likely then to be in a position when users are giving us their own names.


Discussion:
If possible, I'd like to work on this :). I wasn't able to find the format valid identifiers should follow in the documentation page for QASM, could you direct me to where this information is located?
Sure, I'll assign you.  OpenQASM 2 identifiers must follow the regex `[a-z][a-zA-Z_0-9]*` (i.e. a lowercase letter followed by zero or more letters, underscores or digits).  The identifier in the example doesn't match because it's got literal square brackets `[` and `]`, and commas `,` in it.  The paper describing OpenQASM 2 in full is here on arXiv: https://arxiv.org/abs/1707.03429.

Bear in mind that currently the QASM 2 exporter gets the name from fields in these instructions such as `Instruction.name`.  We can't really change those without affecting other parts of Qiskit, so the best bet is probably to make sure that the QASM exporter takes the name it's given, and turns it into a "safe" form by replacing invalid characters, or such like.
Hi @jakelishman, I came up with the following code to check the name of an instruction and correct it if it doesn't match what QASM requires. 

```python
# check instructions names are valid
if not qasm_pattern.match(instruction.name):
    name = instruction.name
    while name[0] not in string.ascii_lowercase:
        # if first letter is not lowercase, just lowercase it
        if name[0] in string.ascii_letters and name[0] not in string.ascii_lowercase:
            name[0] = name[0].lower()

        # if first character is not a letter and the name 
        # has more characters, delete the first character
        elif len(name) > 1:
            name = name[1:]

        # if there are no more characters, append an "i_"
        # for instruction
        else:
            name = "i_" + name

    # replace all remaining invalid characters with underscores
    for idx, char in enumerate(name):
        if char not in string.ascii_lowercase and char not in string.ascii_uppercase and char not in string.digits and char != "_":
            name[idx] = "_"
    
    # make instruction copy to not alter the original circuit 
    instruction = instruction.copy(name=name)
```
For this to work `re` needs to be imported. So, I was wondering if that's all right? Also, `qasm_pattern` is defined as `qasm_pattern = re.compile("[a-z][a-zA-Z_0-9]*")`.

Now, right now I'm placing this piece of code at line 1608, so it runs for every instruction that is not a measure instruction. 

https://github.com/Qiskit/qiskit-terra/blob/6af71f9d1e249a3747772449f299160df4047b83/qiskit/circuit/quantumcircuit.py#L1607-L1609

However, this gives some strange behavior. For example, 

```python
from qiskit import QuantumCircuit
qc = QuantumCircuit(2)

# just some random custom gate
custom = QuantumCircuit(1)
custom.h(0)
custom.y(0)
gate = custom.to_gate()
gate.name = '[custom'

# another random custom gate
custom2 = QuantumCircuit(2)
custom2.x(0)
custom2.append(gate, [1])
gate2 = custom2.to_gate()
gate2.name = '[custom'

qc.append(gate, [0])
qc.append(gate2, [1, 0])

print(qc.qasm())
```

Gives the output

```
OPENQASM 2.0;
include "qelib1.inc";
gate [custom q0 { h q0; y q0; }
gate custom q0 { h q0; y q0; }
gate custom_2371225638080 q0,q1 { x q0; [custom q1; }
qreg q[2];
custom q[0];
custom_2371225638080 q[1],q[0];
```

As you can see, the first `[custom` gets printed twice, once with a valid name and once with its invalid name. But then the secon `[custom` gets corrected and it even identifies that there is already one instruction with the same name. I guess this issue has to do with where I've placed my code, but I can't figure out exactly what's going on. Can I get a hand with this?
`re` is part of the standard library, so it's fine to import - it's not an external dependency - and we actually already use it in a few places.

For escaping strings, it's probably a bit easier to do something like:
```python
# Replace all non-ASCII-word characters with the underscore.
escaped_name = re.sub(r"\W", "_", name, flags=re.ASCII)
if not escaped_name or escaped_name[0] not in string.ascii_lowercase:
    # Add an arbitrary, guaranteed-to-be-valid prefix.
    escaped_name = "gate_" + escaped_name
```
Here simplicity is a virtue, because it's easier to see if something's wrong in the future, and it'll be easier to maintain for us.

Looking at the full output of your QASM there, you're probably missing another place where `instruction.name` is accessed, likely in the part of code that handles nested gate calls.  Everywhere that tries to get a QASM name from an instruction needs to know to attempt to escape the name before it's put down in the file.

Resolving patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -19,6 +19,7 @@
 import functools
 import multiprocessing as mp
 import string
+import re
 from collections import OrderedDict, defaultdict, namedtuple
 from typing import (
     Union,
@@ -102,6 +103,9 @@
 # which operate on either type of bit, but not both at the same time.
 BitType = TypeVar("BitType", Qubit, Clbit)
 
+# Regex pattern to match valid OpenQASM identifiers
+VALID_QASM2_IDENTIFIER = re.compile("[a-z][a-zA-Z_0-9]*")
+
 
 class QuantumCircuit:
     """Create a new circuit.
@@ -1605,6 +1609,10 @@ def qasm(
                     bit_labels[clbit],
                 )
             else:
+                # Check instructions names or label are valid
+                if not VALID_QASM2_IDENTIFIER.fullmatch(instruction.name):
+                    instruction = instruction.copy(name=_qasm_escape_gate_name(instruction.name))
+
                 # decompose gate using definitions if they are not defined in OpenQASM2
                 if (
                     instruction.name not in existing_gate_names
@@ -4081,6 +4089,11 @@ def _add_sub_instruction_to_existing_composite_circuits(
     instruction to existing_composite_circuit list.
     """
     for sub_instruction, _, _ in instruction.definition:
+        # Check instructions names are valid
+        if not VALID_QASM2_IDENTIFIER.fullmatch(sub_instruction.name):
+            sub_instruction = sub_instruction.copy(
+                name=_qasm_escape_gate_name(sub_instruction.name)
+            )
         if (
             sub_instruction.name not in existing_gate_names
             and sub_instruction not in existing_composite_circuits
@@ -4091,6 +4104,17 @@ def _add_sub_instruction_to_existing_composite_circuits(
             )
 
 
+def _qasm_escape_gate_name(name: str) -> str:
+    """Returns a valid OpenQASM gate identifier"""
+    # Replace all non-ASCII-word characters with the underscore.
+    escaped_name = re.sub(r"\W", "_", name, flags=re.ASCII)
+    if not escaped_name or escaped_name[0] not in string.ascii_lowercase:
+        # Add an arbitrary, guaranteed-to-be-valid prefix.
+        escaped_name = "gate_" + escaped_name
+
+    return escaped_name
+
+
 def _get_composite_circuit_qasm_from_instruction(instruction: Instruction) -> str:
     """Returns OpenQASM string composite circuit given an instruction.
     The given instruction should be the result of composite_circuit.to_instruction()."""
@@ -4107,6 +4131,11 @@ def _get_composite_circuit_qasm_from_instruction(instruction: Instruction) -> st
         bit: idx for bits in (definition.qubits, definition.clbits) for idx, bit in enumerate(bits)
     }
     for sub_instruction, qargs, _ in definition:
+        if not VALID_QASM2_IDENTIFIER.fullmatch(sub_instruction.name):
+            sub_instruction = sub_instruction.copy(
+                name=_qasm_escape_gate_name(sub_instruction.name)
+            )
+
         gate_qargs = ",".join(
             ["q%i" % index for index in [definition_bit_labels[qubit] for qubit in qargs]]
         )
diff --git a/qiskit/extensions/unitary.py b/qiskit/extensions/unitary.py
--- a/qiskit/extensions/unitary.py
+++ b/qiskit/extensions/unitary.py
@@ -22,6 +22,7 @@
 from qiskit.circuit import QuantumRegister, Qubit
 from qiskit.circuit.exceptions import CircuitError
 from qiskit.circuit._utils import _compute_control_matrix
+from qiskit.circuit.quantumcircuit import _qasm_escape_gate_name
 from qiskit.circuit.library.standard_gates import U3Gate
 from qiskit.extensions.quantum_initializer import isometry
 from qiskit.quantum_info.operators.predicates import matrix_equal
@@ -167,7 +168,9 @@ def qasm(self):
         """
 
         # give this unitary a name
-        self._qasm_name = self.label if self.label else "unitary" + str(id(self))
+        self._qasm_name = (
+            _qasm_escape_gate_name(self.label) if self.label else "unitary" + str(id(self))
+        )
 
         # map from gates in the definition to params in the method
         reg_to_qasm = OrderedDict()


Test patch:
diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py
--- a/test/python/circuit/test_circuit_qasm.py
+++ b/test/python/circuit/test_circuit_qasm.py
@@ -20,6 +20,9 @@
 from qiskit.circuit import Parameter, Qubit, Clbit
 from qiskit.qasm.exceptions import QasmError
 
+# Regex pattern to match valid OpenQASM identifiers
+VALID_QASM2_IDENTIFIER = re.compile("[a-z][a-zA-Z_0-9]*")
+
 
 class TestCircuitQasm(QiskitTestCase):
     """QuantumCircuit Qasm tests."""
@@ -368,3 +371,82 @@ def test_circuit_qasm_with_repeated_instruction_names(self):
         names = ["h", "x", "custom", "custom"]
         for idx, (instruction, _, _) in enumerate(qc._data):
             self.assertEqual(instruction.name, names[idx])
+
+    def test_circuit_qasm_with_invalid_identifiers(self):
+        """Test that qasm() detects and corrects invalid OpenQASM gate identifiers,
+        while not changing the instructions on the original circuit"""
+        qc = QuantumCircuit(2)
+
+        # Create some gate and give it an invalid name
+        custom = QuantumCircuit(1)
+        custom.x(0)
+        custom.u(0, 0, pi, 0)
+        gate = custom.to_gate()
+        gate.name = "A[$]"
+
+        # Another gate also with invalid name
+        custom2 = QuantumCircuit(2)
+        custom2.x(0)
+        custom2.append(gate, [1])
+        gate2 = custom2.to_gate()
+        gate2.name = "invalid[name]"
+
+        # Unitary gate, for which qasm string is produced by internal method
+        qc.unitary([[0, 1], [1, 0]], 0, label="[valid?]")
+
+        # Append gates
+        qc.append(gate, [0])
+        qc.append(gate2, [1, 0])
+
+        # Expected qasm with valid identifiers
+        expected_qasm = "\n".join(
+            [
+                "OPENQASM 2.0;",
+                'include "qelib1.inc";',
+                "gate gate__valid__ p0 {",
+                "	u3(pi,pi/2,-pi/2) p0;",
+                "}",
+                "gate gate_A___ q0 { x q0; u(0,0,pi) q0; }",
+                "gate invalid_name_ q0,q1 { x q0; gate_A___ q1; }",
+                "qreg q[2];",
+                "gate__valid__ q[0];",
+                "gate_A___ q[0];",
+                "invalid_name_ q[1],q[0];",
+                "",
+            ]
+        )
+
+        # Check qasm() produces the correct string
+        self.assertEqual(expected_qasm, qc.qasm())
+
+        # Check instruction names were not changed by qasm()
+        names = ["unitary", "A[$]", "invalid[name]"]
+        for idx, (instruction, _, _) in enumerate(qc._data):
+            self.assertEqual(instruction.name, names[idx])
+
+    def test_circuit_qasm_with_duplicate_invalid_identifiers(self):
+        """Test that qasm() corrects invalid identifiers and the de-duplication
+        code runs correctly, without altering original instructions"""
+        base = QuantumCircuit(1)
+
+        # First gate with invalid name, escapes to "invalid__"
+        clash1 = QuantumCircuit(1, name="invalid??")
+        clash1.x(0)
+        base.append(clash1, [0])
+
+        # Second gate with invalid name that also escapes to "invalid__"
+        clash2 = QuantumCircuit(1, name="invalid[]")
+        clash2.z(0)
+        base.append(clash2, [0])
+
+        # Check qasm is correctly produced
+        names = set()
+        for match in re.findall(r"gate (\S+)", base.qasm()):
+            self.assertTrue(VALID_QASM2_IDENTIFIER.fullmatch(match))
+            names.add(match)
+        self.assertEqual(len(names), 2)
+
+        # Check instruction names were not changed by qasm()
+        names = ["invalid??", "invalid[]"]
+        for idx, (instruction, _, _) in enumerate(base._data):
+            self.assertEqual(instruction.name, names[idx])