Repository: Qiskit/qiskit

Issue:
QASM exporter producing invalid QASM
### Environment

- **Qiskit Terra version**: 0.23.1
- **Python version**: 3.10.9
- **Operating system**: macOS Ventura 13.1


### What is happening?

When a `ClassicalRegister` is given a `name` containing curly braces, QASM 3 exporter produces invalid QASM, leading to job failures in the runtime. Note that this behavior is not observed when a `QuantumRegister` is given a `name` containing curly braces.

The motivation for using the curly braces in `name` was to subscript the register name when calling `QuantumCircuit.draw(output="mpl")`, which does not work `ClassicalRegister` and does work for `QuantumRegister`, but only after transpiling (a separate but related issue, I think).

### How can we reproduce the issue?

```
from qiskit import transpile, QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_ibm_provider import IBMProvider

qreg = QuantumRegister(2, name="qreg")
creg = ClassicalRegister(2, name="c_{reg}")
qc = QuantumCircuit(qreg, creg)
qc.h(qreg[0])
qc.cx(qreg[0], qreg[1])
qc.measure(qreg, creg)

provider = IBMProvider()
backend = provider.get_backend("ibm_peekskill")

transpiled = transpile(qc, backend, optimization_level=0)
job = backend.run(transpiled, shots=1024, dynamic=True)
results = job.result()
```

### Actual output
```
---------------------------------------------------------------------------
IBMJobFailureError                        Traceback (most recent call last)
Cell In[97], line 2
      1 job = backend.run(transpiled, shots=4096, dynamic=True)
----> 2 results = job.result()

File ~/.venvs/py310-iqx-deprecated/lib/python3.10/site-packages/qiskit_ibm_provider/job/ibm_circuit_job.py:255, in IBMCircuitJob.result(self, timeout, refresh)
    253     if self._status == JobStatus.ERROR:
    254         error_message = self.error_message()
--> 255         raise IBMJobFailureError(f"Job failed: " f"{error_message}")
    256     self._retrieve_result(refresh=refresh)
    257 return self._result

IBMJobFailureError: 'Job failed: Unknown error; Traceback (most recent call last):
2023-02-24T21:56:18.704024473Z   File "/provider/server/main.py", line 75, in execute_program
2023-02-24T21:56:18.704024473Z     starter.execute()
2023-02-24T21:56:18.704024473Z   File "/provider/programruntime/program_starter_wrapper.py", line 99, in execute
2023-02-24T21:56:18.704024473Z     raise ex
2023-02-24T21:56:18.704024473Z   File "/provider/programruntime/program_starter_wrapper.py", line 91, in execute
2023-02-24T21:56:18.704024473Z     final_result = self.main(backend, self.messenger, **self.user_params)
2023-02-24T21:56:18.704024473Z   File "/code/./program.py", line 600, in main
2023-02-24T21:56:18.704024473Z     result = backend.run(payload, **filtered_run_config).result()
2023-02-24T21:56:18.704024473Z   File "/provider/programruntime/runtime_job.py", line 80, in result
2023-02-24T21:56:18.704024473Z     self.set_final_state(raw_result)
2023-02-24T21:56:18.704024473Z   File "/provider/programruntime/runtime_job.py", line 132, in set_final_state
2023-02-24T21:56:18.704024473Z     return final_results
2023-02-24T21:56:18.704024473Z UnboundLocalError: local variable \'final_results\' referenced before assignment
2023-02-24T21:56:18.707519839Z jaeger_tracing - INFO Span publisher exited
2023-02-24T21:56:18.707845266Z webserver-starter - DEBUG Writing job status to termination marker file: \'{"status": "Failed"}\'
2023-02-24T21:56:18.708131533Z /pod-data/ CLOSE_WRITE,CLOSE terminated
2023-02-24T21:56:18.708617041Z Termination marker file found. Kill process (7).
2023-02-24T21:56:18.731898748Z /bin/bash: line 3:     7 Killed                  python -m uvicorn server.main:app --port 8081
2023-02-24T21:56:18.732227067Z Termination signal received, exited.'
```

### QASM output
`qreg` and `c_{reg}`
```
OPENQASM 3;
include "stdgates.inc";
bit[2] c_{reg};
qubit[2] _all_qubits;
let qreg__generated0 = _all_qubits[0:1];
h qreg__generated0[0];
cx qreg__generated0[0], qreg__generated0[1];
c_{reg}[0] = measure qreg__generated0[0];
c_{reg}[1] = measure qreg__generated0[1];
```

`q_{reg}` and `creg`
```
OPENQASM 3;
include "stdgates.inc";
bit[2] creg__generated0;
qubit[2] _all_qubits;
let q_{reg} = _all_qubits[0:1];
h q_{reg}[0];
cx q_{reg}[0], q_{reg}[1];
creg__generated0[0] = measure q_{reg}[0];
creg__generated0[1] = measure q_{reg}[1];
```

`q_{reg}` and `c_{reg}`
```
OPENQASM 3;
include "stdgates.inc";
bit[2] c_{reg};
qubit[2] _all_qubits;
let q_{reg} = _all_qubits[0:1];
h q_{reg}[0];
cx q_{reg}[0], q_{reg}[1];
c_{reg}[0] = measure q_{reg}[0];
c_{reg}[1] = measure q_{reg}[1];
```

`qreg` and `creg`
```
OPENQASM 3;
include "stdgates.inc";
bit[2] creg__generated0;
qubit[2] _all_qubits;
let qreg__generated1 = _all_qubits[0:1];
h qreg__generated1[0];
cx qreg__generated1[0], qreg__generated1[1];
creg__generated0[0] = measure qreg__generated1[0];
creg__generated0[1] = measure qreg__generated1[1];
```

### What should happen?

`results` should contain expected `Result` for the given circuit.

### Any suggestions?

@jakelishman mentioned, "[it] sounds like there’s some place where the exporter fails to normalise an identifier into something exportable."


Discussion:
Now I think about it, I know how this came about: originally, there was no need to test the register names for special symbols because they were constrained to only be valid OQ2 identifiers (i.e. `[a-z][a-zA-Z0-9_]*`) during the construction of the object, which is a strict subset of valid OQ3 identifiers once extra keywords are removed too.  When that restriction was relaxed in #9100 it became possible in the OQ3 exporter to have an invalid register name.

I'm vaguely surprised that it doesn't cause similar problems for quantum registers, but no big deal.

Resolving patch:
diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py
--- a/qiskit/qasm3/exporter.py
+++ b/qiskit/qasm3/exporter.py
@@ -13,6 +13,7 @@
 """QASM3 Exporter"""
 
 import collections
+import re
 import io
 import itertools
 import numbers
@@ -108,6 +109,17 @@
     }
 )
 
+# This probably isn't precisely the same as the OQ3 spec, but we'd need an extra dependency to fully
+# handle all Unicode character classes, and this should be close enough for users who aren't
+# actively _trying_ to break us (fingers crossed).
+_VALID_IDENTIFIER = re.compile(r"[\w][\w\d]*", flags=re.U)
+
+
+def _escape_invalid_identifier(name: str) -> str:
+    if name in _RESERVED_KEYWORDS or not _VALID_IDENTIFIER.fullmatch(name):
+        name = "_" + re.sub(r"[^\w\d]", "_", name)
+    return name
+
 
 class Exporter:
     """QASM3 exporter main class."""
@@ -359,9 +371,9 @@ def _register_variable(self, variable, name=None) -> ast.Identifier:
                     f"tried to reserve '{name}', but it is already used by '{table[name]}'"
                 )
         else:
-            name = variable.name
-            while name in table or name in _RESERVED_KEYWORDS:
-                name = f"{variable.name}__generated{next(self._counter)}"
+            name = basename = _escape_invalid_identifier(variable.name)
+            while name in table:
+                name = f"{basename}__generated{next(self._counter)}"
         identifier = ast.Identifier(name)
         table[identifier.string] = variable
         table[variable] = identifier


Test patch:
diff --git a/test/python/circuit/test_circuit_qasm3.py b/test/python/circuit/test_circuit_qasm3.py
--- a/test/python/circuit/test_circuit_qasm3.py
+++ b/test/python/circuit/test_circuit_qasm3.py
@@ -75,8 +75,13 @@ class TestCircuitQASM3(QiskitTestCase):
     @classmethod
     def setUpClass(cls):
         # These regexes are not perfect by any means, but sufficient for simple tests on controlled
-        # input circuits.
-        cls.register_regex = re.compile(r"^\s*let\s+(?P<name>\w+\b)", re.U | re.M)
+        # input circuits.  They can allow false negatives (in which case, update the regex), but to
+        # be useful for the tests must _never_ have false positive matches.  We use an explicit
+        # space (`\s`) or semicolon rather than the end-of-word `\b` because we want to ensure that
+        # the exporter isn't putting out invalid characters as part of the identifiers.
+        cls.register_regex = re.compile(
+            r"^\s*(let|bit(\[\d+\])?)\s+(?P<name>\w+)[\s;]", re.U | re.M
+        )
         scalar_type_names = {
             "angle",
             "duration",
@@ -88,7 +93,7 @@ def setUpClass(cls):
         cls.scalar_parameter_regex = re.compile(
             r"^\s*((input|output|const)\s+)?"  # Modifier
             rf"({'|'.join(scalar_type_names)})\s*(\[[^\]]+\])?\s+"  # Type name and designator
-            r"(?P<name>\w+\b)",  # Parameter name
+            r"(?P<name>\w+)[\s;]",  # Parameter name
             re.U | re.M,
         )
         super().setUpClass()
@@ -1390,6 +1395,26 @@ def test_custom_gate_used_in_loop_scope(self):
         )
         self.assertEqual(dumps(qc), expected_qasm)
 
+    def test_registers_have_escaped_names(self):
+        """Test that both types of register are emitted with safely escaped names if they begin with
+        invalid names. Regression test of gh-9658."""
+        qc = QuantumCircuit(
+            QuantumRegister(2, name="q_{reg}"), ClassicalRegister(2, name="c_{reg}")
+        )
+        qc.measure([0, 1], [0, 1])
+        out_qasm = dumps(qc)
+        matches = {match_["name"] for match_ in self.register_regex.finditer(out_qasm)}
+        self.assertEqual(len(matches), 2, msg=f"Observed OQ3 output:\n{out_qasm}")
+
+    def test_parameters_have_escaped_names(self):
+        """Test that parameters are emitted with safely escaped names if they begin with invalid
+        names. Regression test of gh-9658."""
+        qc = QuantumCircuit(1)
+        qc.u(Parameter("p_{0}"), 2 * Parameter("p_?0!"), 0, 0)
+        out_qasm = dumps(qc)
+        matches = {match_["name"] for match_ in self.scalar_parameter_regex.finditer(out_qasm)}
+        self.assertEqual(len(matches), 2, msg=f"Observed OQ3 output:\n{out_qasm}")
+
     def test_parameter_expression_after_naming_escape(self):
         """Test that :class:`.Parameter` instances are correctly renamed when they are used with
         :class:`.ParameterExpression` blocks, even if they have names that needed to be escaped."""
@@ -1401,10 +1426,10 @@ def test_parameter_expression_after_naming_escape(self):
             [
                 "OPENQASM 3;",
                 'include "stdgates.inc";',
-                "input float[64] measure__generated0;",
+                "input float[64] _measure;",
                 "qubit[1] _all_qubits;",
                 "let q = _all_qubits[0:0];",
-                "U(2*measure__generated0, 0, 0) q[0];",
+                "U(2*_measure, 0, 0) q[0];",
                 "",
             ]
         )
@@ -1437,7 +1462,7 @@ def test_reserved_keywords_as_names_are_escaped(self, keyword):
             qc = QuantumCircuit(qreg)
             out_qasm = dumps(qc)
             register_name = self.register_regex.search(out_qasm)
-            self.assertTrue(register_name)
+            self.assertTrue(register_name, msg=f"Observed OQ3:\n{out_qasm}")
             self.assertNotEqual(keyword, register_name["name"])
         with self.subTest("parameter"):
             qc = QuantumCircuit(1)
@@ -1445,7 +1470,7 @@ def test_reserved_keywords_as_names_are_escaped(self, keyword):
             qc.u(param, 0, 0, 0)
             out_qasm = dumps(qc)
             parameter_name = self.scalar_parameter_regex.search(out_qasm)
-            self.assertTrue(parameter_name)
+            self.assertTrue(parameter_name, msg=f"Observed OQ3:\n{out_qasm}")
             self.assertNotEqual(keyword, parameter_name["name"])