Repository: Qiskit/qiskit

Issue:
OpenQASM3 exporter emits variables with reserved keywords
### Environment

- **Qiskit Terra version**: 0.19.2
- **Python version**: 3.8 
- **Operating system**:  OSX


### What is happening?

When dumping QASM3 from a Qiskit circuit the emitted QASM3 has variables with names that are reserved keywords.

### How can we reproduce the issue?

```python
from qiskit import IBMQ, transpile, qasm3
from qiskit_experiments.library import QuantumVolume

from qiskit.test.mock import FakeMontreal

qv_exp = QuantumVolume([0, 1], seed=42)
circuits = qv_exp.circuits()

backend = FakeMontreal()

def dump_qasm3(circuit, backend=backend):
    return qasm3.Exporter(includes=[], basis_gates=backend.configuration().basis_gates).dumps(circuit)

transpiled_circuits = transpile(circuits, backend, optimization_level=0)
print(dump_qasm3(transpiled_circuits[0]))
```
Emits:
```qasm
OPENQASM 3;
bit[2] measure;
rz(-0.527043787493904) $0;
sx $0;
rz(5.16854203223675) $0;
sx $0;
rz(7.52041849263069) $0;
rz(2.52883547684609) $1;
sx $1;
rz(4.18263287241389) $1;
sx $1;
rz(11.4206034322753) $1;
cx $0, $1;
rz(pi/2) $0;
sx $0;
rz(3.83403185518176) $0;
sx $0;
rz(5*pi/2) $0;
rz(-3.13722964498385) $1;
sx $1;
rz(3.43607384014569) $1;
sx $1;
rz(7.84942236436758) $1;
cx $0, $1;
rz(pi/2) $0;
sx $0;
rz(3.15129722109744) $0;
sx $0;
rz(2*pi) $0;
rz(-pi) $1;
sx $1;
rz(4.71106568778747) $1;
sx $1;
rz(10.9942509926498) $1;
cx $0, $1;
rz(0.0407864518524126) $0;
sx $0;
rz(3.68646663680408) $0;
sx $0;
rz(8.261303299549) $0;
rz(0.384886550311654) $0;
sx $0;
rz(4.72101136719892) $0;
sx $0;
rz(7.05118934560937) $0;
rz(0.360649613151623) $1;
sx $1;
rz(4.92286155451527) $1;
sx $1;
rz(11.7465986327618) $1;
rz(-0.879779034806911) $1;
sx $1;
rz(5.92954645622555) $1;
sx $1;
rz(7.17120533229796) $1;
cx $0, $1;
rz(pi/2) $0;
sx $0;
rz(4.06066473777189) $0;
sx $0;
rz(5*pi/2) $0;
rz(-3.1391797270208) $1;
sx $1;
rz(3.64322156481995) $1;
sx $1;
rz(7.85122966784221) $1;
cx $0, $1;
rz(-pi/2) $0;
sx $0;
rz(3.7166355179822) $0;
sx $0;
rz(3*pi) $0;
rz(-pi) $1;
sx $1;
rz(4.71106568778747) $1;
sx $1;
rz(10.9942509926498) $1;
cx $0, $1;
rz(-1.47560665166994) $0;
sx $0;
rz(4.18505473328482) $0;
sx $0;
rz(9.47043569736961) $0;
rz(0.398413507779781) $1;
sx $1;
rz(4.1804199287629) $1;
sx $1;
rz(12.3633552711276) $1;
barrier $0, $1;
measure[0] = measure $0;
measure[1] = measure $1;
```
Note the `bit[2] measure;` declaration which is invalid.

### What should happen?

The QASM3 exporter should mangle the names of reserved keywords for classical/qubit registers with names that collied.

### Any suggestions?

_No response_


Discussion:

Resolving patch:
diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py
--- a/qiskit/qasm3/exporter.py
+++ b/qiskit/qasm3/exporter.py
@@ -26,6 +26,7 @@
     Instruction,
     Measure,
     Parameter,
+    ParameterExpression,
     QuantumCircuit,
     QuantumRegister,
     Qubit,
@@ -49,6 +50,63 @@
 from .printer import BasicPrinter
 
 
+# Reserved keywords that gates and variables cannot be named.  It is possible that some of these
+# _could_ be accepted as variable names by OpenQASM 3 parsers, but it's safer for us to just be very
+# conservative.
+_RESERVED_KEYWORDS = frozenset(
+    {
+        "OPENQASM",
+        "U",
+        "angle",
+        "array",
+        "barrier",
+        "bit",
+        "bool",
+        "box",
+        "break",
+        "cal",
+        "complex",
+        "const",
+        "continue",
+        "creg",
+        "ctrl",
+        "def",
+        "defcal",
+        "defcalgrammar",
+        "delay",
+        "duration",
+        "durationof",
+        "else",
+        "end",
+        "extern",
+        "float",
+        "for",
+        "gate",
+        "gphase",
+        "if",
+        "in",
+        "include",
+        "input",
+        "int",
+        "inv",
+        "let",
+        "measure",
+        "mutable",
+        "negctrl",
+        "output",
+        "pow",
+        "qreg",
+        "qubit",
+        "reset",
+        "return",
+        "sizeof",
+        "stretch",
+        "uint",
+        "while",
+    }
+)
+
+
 class Exporter:
     """QASM3 expoter main class."""
 
@@ -223,9 +281,10 @@ def register(self, instruction):
 # the top-level scope where the bits were actually defined.  In the class, 'circuit' is an instance
 # of QuantumCircuit that defines this level, and 'bit_map' is a mapping of 'Bit: Bit', where the
 # keys are bits in the circuit in this scope, and the values are the Bit in the top-level scope in
-# this context that this bit actually represents.  This is a cheap hack around actually implementing
-# a proper symbol table.
-_Scope = collections.namedtuple("_Scope", ("circuit", "bit_map"))
+# this context that this bit actually represents.  'symbol_map' is a bidirectional mapping of
+# '<Terra object>: Identifier' and 'str: <Terra object>', where the string in the second map is the
+# name of the identifier.  This is a cheap hack around actually implementing a proper symbol table.
+_Scope = collections.namedtuple("_Scope", ("circuit", "bit_map", "symbol_map"))
 
 
 class QASM3Builder:
@@ -256,6 +315,9 @@ def __init__(
         self._flat_reg = False
         self._physical_qubit = False
         self._loose_clbit_index_lookup = {}
+        # An arbitrary counter to help with generation of unique ids for symbol names when there are
+        # clashes (though we generally prefer to keep user names if possible).
+        self._counter = itertools.count()
         self.disable_constants = disable_constants
         self.alias_classical_registers = alias_classical_registers
         self.global_namespace = GlobalNamespace(includeslist, basis_gates)
@@ -273,6 +335,60 @@ def _register_opaque(self, instruction):
             self.global_namespace.register(instruction)
             self._opaque_to_declare[id(instruction)] = instruction
 
+    def _register_variable(self, variable, name=None) -> ast.Identifier:
+        """Register a variable in the symbol table for the current scope, returning the name that
+        should be used to refer to the variable.  The same name will be returned by subsequent calls
+        to :meth:`_lookup_variable` within the same scope.
+
+        If ``name`` is given explicitly, it must not already be defined in the scope.
+        """
+        # Note that the registration only checks for the existence of a variable that was declared
+        # in the current scope, not just one that's available.  This is a rough implementation of
+        # the shadowing proposal currently being drafted for OpenQASM 3, though we expect it to be
+        # expanded and modified in the future (2022-03-07).
+        table = self.current_scope().symbol_map
+        if name is not None:
+            if name in _RESERVED_KEYWORDS:
+                raise QASM3ExporterError(f"cannot reserve the keyword '{name}' as a variable name")
+            if name in table:
+                raise QASM3ExporterError(
+                    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)}"
+        identifier = ast.Identifier(name)
+        table[identifier.string] = variable
+        table[variable] = identifier
+        return identifier
+
+    def _reserve_variable_name(self, name: ast.Identifier) -> ast.Identifier:
+        """Reserve a variable name in the current scope, raising a :class:`.QASM3ExporterError` if
+        the name is already in use.
+
+        This is useful for autogenerated names that the exporter itself reserves when dealing with
+        objects that have no standard Terra object backing them, such as the declaration of all
+        circuit qubits, so cannot be placed into the symbol table by the normal means.
+
+        Returns the same identifier, for convenience in chaining."""
+        table = self.current_scope().symbol_map
+        if name.string in table:
+            variable = table[name.string]
+            raise QASM3ExporterError(
+                f"tried to reserve '{name.string}', but it is already used by '{variable}'"
+            )
+        table[name.string] = "<internal object>"
+        return name
+
+    def _lookup_variable(self, variable) -> ast.Identifier:
+        """Lookup a Terra object within the current context, and return the name that should be used
+        to represent it in OpenQASM 3 programmes."""
+        for scope in reversed(self.current_context()):
+            if variable in scope.symbol_map:
+                return scope.symbol_map[variable]
+        raise KeyError(f"'{variable}' is not defined in the current context")
+
     def build_header(self):
         """Builds a Header"""
         version = ast.Version("3")
@@ -345,7 +461,7 @@ def push_scope(self, circuit: QuantumCircuit, qubits: Iterable[Qubit], clbits: I
                 f" provided {len(clbits)} clbits to create the mapping."
             )
         mapping = dict(itertools.chain(zip(circuit.qubits, qubits), zip(circuit.clbits, clbits)))
-        self._circuit_ctx[-1].append(_Scope(circuit, mapping))
+        self._circuit_ctx[-1].append(_Scope(circuit, mapping, {}))
 
     def pop_scope(self) -> _Scope:
         """Pop the current scope (like a ``for`` or ``while`` loop body) off the current context
@@ -359,7 +475,7 @@ def pop_scope(self) -> _Scope:
     def push_context(self, outer_context: QuantumCircuit):
         """Push a new context (like for a ``gate`` or ``def`` body) onto the stack."""
         mapping = {bit: bit for bit in itertools.chain(outer_context.qubits, outer_context.clbits)}
-        self._circuit_ctx.append([_Scope(outer_context, mapping)])
+        self._circuit_ctx.append([_Scope(outer_context, mapping, {})])
 
     def pop_context(self):
         """Pop the current context (like for a ``gate`` or ``def`` body) onto the stack."""
@@ -471,7 +587,9 @@ def build_subroutine_definition(self, instruction):
         name = self.global_namespace[instruction]
         self.push_context(instruction.definition)
         quantum_arguments = [
-            ast.QuantumArgument(ast.Identifier(f"{self.gate_qubit_prefix}_{n_qubit}"))
+            ast.QuantumArgument(
+                self._reserve_variable_name(ast.Identifier(f"{self.gate_qubit_prefix}_{n_qubit}"))
+            )
             for n_qubit in range(len(instruction.definition.qubits))
         ]
         subroutine_body = ast.SubroutineBlock(
@@ -482,8 +600,8 @@ def build_subroutine_definition(self, instruction):
 
     def build_gate_definition(self, gate):
         """Builds a QuantumGateDefinition"""
-        signature = self.build_gate_signature(gate)
         self.push_context(gate.definition)
+        signature = self.build_gate_signature(gate)
         body = ast.QuantumBlock(self.build_quantum_instructions(gate.definition.data))
         self.pop_context()
         return ast.QuantumGateDefinition(signature, body)
@@ -496,10 +614,10 @@ def build_gate_signature(self, gate):
         # Dummy parameters
         for num in range(len(gate.params) - len(definition.parameters)):
             param_name = f"{self.gate_parameter_prefix}_{num}"
-            params.append(ast.Identifier(param_name))
-        params += [ast.Identifier(param.name) for param in definition.parameters]
+            params.append(self._reserve_variable_name(ast.Identifier(param_name)))
+        params += [self._register_variable(param) for param in definition.parameters]
         quantum_arguments = [
-            ast.Identifier(f"{self.gate_qubit_prefix}_{n_qubit}")
+            self._reserve_variable_name(ast.Identifier(f"{self.gate_qubit_prefix}_{n_qubit}"))
             for n_qubit in range(len(definition.qubits))
         ]
         return ast.QuantumGateSignature(ast.Identifier(name), quantum_arguments, params or None)
@@ -509,7 +627,8 @@ def build_variable_declarations(self):
         inputs, outputs, variables = [], [], []
         global_scope = self.global_scope(assert_=True).circuit
         for parameter in global_scope.parameters:
-            declaration = _infer_variable_declaration(global_scope, parameter)
+            parameter_name = self._register_variable(parameter)
+            declaration = _infer_variable_declaration(global_scope, parameter, parameter_name)
             if declaration is None:
                 continue
             if isinstance(declaration, ast.IODeclaration):
@@ -555,7 +674,8 @@ def build_classical_declarations(self):
                 bit: index for index, bit in enumerate(circuit.clbits)
             }
             flat_declaration = self.build_clbit_declaration(
-                len(circuit.clbits), self.base_classical_register_name
+                len(circuit.clbits),
+                self._reserve_variable_name(ast.Identifier(self.base_classical_register_name)),
             )
             return [flat_declaration] + self.build_aliases(circuit.cregs)
         loose_register_size = 0
@@ -571,17 +691,23 @@ def build_classical_declarations(self):
                 loose_register_size += 1
         if loose_register_size > 0:
             loose = [
-                self.build_clbit_declaration(loose_register_size, self.base_classical_register_name)
+                self.build_clbit_declaration(
+                    loose_register_size,
+                    self._reserve_variable_name(ast.Identifier(self.base_classical_register_name)),
+                )
             ]
         else:
             loose = []
         return loose + [
-            self.build_clbit_declaration(len(register), register.name) for register in circuit.cregs
+            self.build_clbit_declaration(len(register), self._register_variable(register))
+            for register in circuit.cregs
         ]
 
-    def build_clbit_declaration(self, n_clbits: int, name: str) -> ast.ClassicalDeclaration:
+    def build_clbit_declaration(
+        self, n_clbits: int, name: ast.Identifier
+    ) -> ast.ClassicalDeclaration:
         """Return a declaration of the :obj:`.Clbit`\\ s as a ``bit[n]``."""
-        return ast.ClassicalDeclaration(ast.BitArrayType(n_clbits), ast.Identifier(name))
+        return ast.ClassicalDeclaration(ast.BitArrayType(n_clbits), name)
 
     def build_quantum_declarations(self):
         """Return a list of AST nodes declaring all the qubits in the current scope, and all the
@@ -594,7 +720,7 @@ def build_qubit_declarations(self):
         """Return a declaration of all the :obj:`.Qubit`\\ s in the current scope."""
         # Base register
         return ast.QuantumDeclaration(
-            ast.Identifier(self.base_quantum_register_name),
+            self._reserve_variable_name(ast.Identifier(self.base_quantum_register_name)),
             ast.Designator(self.build_integer(self.current_scope().circuit.num_qubits)),
         )
 
@@ -640,7 +766,7 @@ def build_aliases(self, registers: Iterable[Register]) -> List[ast.AliasStatemen
                         ),
                     )
                 )
-            out.append(ast.AliasStatement(ast.Identifier(register.name), elements))
+            out.append(ast.AliasStatement(self._register_variable(register), elements))
         return out
 
     def build_quantum_instructions(self, instructions):
@@ -719,14 +845,13 @@ def build_for_loop(
     ) -> ast.ForLoopStatement:
         """Build a :obj:`.ForLoopOp` into a :obj:`.ast.ForLoopStatement`."""
         indexset, loop_parameter, loop_circuit = instruction.params
+        self.push_scope(loop_circuit, qubits, clbits)
         if loop_parameter is None:
             # The loop parameter is implicitly declared by the ``for`` loop (see also
-            # _infer_parameter_declaration), so it doesn't matter that we haven't declared this,
-            # except for the concerns about symbol collision which can only be alleviated by
-            # introducing a proper symbol table to this exporter.
-            loop_parameter_ast = ast.Identifier("_")
+            # _infer_parameter_declaration), so it doesn't matter that we haven't declared this.
+            loop_parameter_ast = self._reserve_variable_name(ast.Identifier("_"))
         else:
-            loop_parameter_ast = ast.Identifier(loop_parameter.name)
+            loop_parameter_ast = self._register_variable(loop_parameter)
         if isinstance(indexset, range):
             # QASM 3 uses inclusive ranges on both ends, unlike Python.
             indexset_ast = ast.Range(
@@ -742,7 +867,6 @@ def build_for_loop(
                     "The values in QASM 3 'for' loops must all be integers, but received"
                     f" '{indexset}'."
                 ) from None
-        self.push_scope(loop_circuit, qubits, clbits)
         body_ast = self.build_program_block(loop_circuit)
         self.pop_scope()
         return ast.ForLoopStatement(indexset_ast, loop_parameter_ast, body_ast)
@@ -766,11 +890,26 @@ def build_eqcondition(self, condition):
         if isinstance(condition[0], Clbit):
             condition_on = self.build_single_bit_reference(condition[0])
         else:
-            condition_on = ast.Identifier(condition[0].name)
+            condition_on = self._lookup_variable(condition[0])
         return ast.ComparisonExpression(
             condition_on, ast.EqualsOperator(), self.build_integer(condition[1])
         )
 
+    def _rebind_scoped_parameters(self, expression):
+        """If the input is a :class:`.ParameterExpression`, rebind any internal
+        :class:`.Parameter`\\ s so that their names match their names in the scope.  Other inputs
+        are returned unchanged."""
+        # This is a little hacky, but the entirety of the Expression handling is essentially
+        # missing, pending a new system in Terra to replace it (2022-03-07).
+        if not isinstance(expression, ParameterExpression):
+            return expression
+        return expression.subs(
+            {
+                param: Parameter(self._lookup_variable(param).string)
+                for param in expression.parameters
+            }
+        )
+
     def build_gate_call(self, instruction):
         """Builds a QuantumGateCall"""
         if isinstance(instruction[0], standard_gates.UGate):
@@ -779,10 +918,14 @@ def build_gate_call(self, instruction):
             gate_name = ast.Identifier(self.global_namespace[instruction[0]])
         qubits = [self.build_single_bit_reference(qubit) for qubit in instruction[1]]
         if self.disable_constants:
-            parameters = [ast.Expression(param) for param in instruction[0].params]
+            parameters = [
+                ast.Expression(self._rebind_scoped_parameters(param))
+                for param in instruction[0].params
+            ]
         else:
             parameters = [
-                ast.Expression(pi_check(param, output="qasm")) for param in instruction[0].params
+                ast.Expression(pi_check(self._rebind_scoped_parameters(param), output="qasm"))
+                for param in instruction[0].params
             ]
 
         return ast.QuantumGateCall(gate_name, qubits, parameters=parameters)
@@ -808,7 +951,7 @@ def build_single_bit_reference(self, bit: Bit) -> ast.Identifier:
             # user is used to seeing as well.
             register, index = found_bit.registers[0]
             return ast.SubscriptedIdentifier(
-                ast.Identifier(register.name), self.build_integer(index)
+                self._lookup_variable(register), self.build_integer(index)
             )
         # Otherwise reference via the list of all qubits, or the list of loose clbits.
         if isinstance(bit, Qubit):
@@ -832,7 +975,7 @@ def find_bit(self, bit: Bit):
 
 
 def _infer_variable_declaration(
-    circuit: QuantumCircuit, parameter: Parameter
+    circuit: QuantumCircuit, parameter: Parameter, parameter_name: ast.Identifier
 ) -> Union[ast.ClassicalDeclaration, None]:
     """Attempt to infer what type a parameter should be declared as to work with a circuit.
 
@@ -851,6 +994,7 @@ def _infer_variable_declaration(
     Args:
         circuit: The global-scope circuit, which is the base of the exported program.
         parameter: The parameter to infer the type of.
+        parameter_name: The name of the parameter to use in the declaration.
 
     Returns:
         A suitable :obj:`.ast.ClassicalDeclaration` node, or, if the parameter should *not* be
@@ -878,6 +1022,4 @@ def is_loop_variable(circuit, parameter):
         return None
     # Arbitrary choice of double-precision float for all other parameters, but it's what we actually
     # expect people to be binding to their Parameters right now.
-    return ast.IODeclaration(
-        ast.IOModifier.INPUT, ast.FloatType.DOUBLE, ast.Identifier(parameter.name)
-    )
+    return ast.IODeclaration(ast.IOModifier.INPUT, ast.FloatType.DOUBLE, parameter_name)


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
@@ -16,9 +16,10 @@
 # pylint: disable=line-too-long
 
 from io import StringIO
+import re
 import unittest
 
-import ddt
+from ddt import ddt, data
 
 from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, transpile
 from qiskit.circuit import Parameter, Qubit, Clbit, Instruction
@@ -65,11 +66,33 @@ def test_dump(self):
         self.assertEqual(result, self.expected_qasm)
 
 
+@ddt
 class TestCircuitQASM3(QiskitTestCase):
     """QASM3 exporter."""
 
     maxDiff = 1_000_000
 
+    @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)
+        scalar_type_names = {
+            "angle",
+            "duration",
+            "float",
+            "int",
+            "stretch",
+            "uint",
+        }
+        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
+            re.U | re.M,
+        )
+        super().setUpClass()
+
     def test_regs_conds_qasm(self):
         """Test with registers and conditionals."""
         qr1 = QuantumRegister(1, "qr1")
@@ -1320,6 +1343,64 @@ def test_custom_gate_used_in_loop_scope(self):
         )
         self.assertEqual(dumps(qc), expected_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."""
+        param = Parameter("measure")  # an invalid name
+        qc = QuantumCircuit(1)
+        qc.u(2 * param, 0, 0, 0)
+
+        expected_qasm = "\n".join(
+            [
+                "OPENQASM 3;",
+                'include "stdgates.inc";',
+                "input float[64] measure__generated0;",
+                "qubit[1] _all_qubits;",
+                "let q = _all_qubits[0:0];",
+                "U(2*measure__generated0, 0, 0) q[0];",
+                "",
+            ]
+        )
+        self.assertEqual(dumps(qc), expected_qasm)
+
+    def test_parameters_and_registers_cannot_have_naming_clashes(self):
+        """Test that parameters and registers are considered part of the same symbol table for the
+        purposes of avoiding clashes."""
+        qreg = QuantumRegister(1, "clash")
+        param = Parameter("clash")
+        qc = QuantumCircuit(qreg)
+        qc.u(param, 0, 0, 0)
+
+        out_qasm = dumps(qc)
+        register_name = self.register_regex.search(out_qasm)
+        parameter_name = self.scalar_parameter_regex.search(out_qasm)
+        self.assertTrue(register_name)
+        self.assertTrue(parameter_name)
+        self.assertIn("clash", register_name["name"])
+        self.assertIn("clash", parameter_name["name"])
+        self.assertNotEqual(register_name["name"], parameter_name["name"])
+
+    # Not necessarily all the reserved keywords, just a sensibly-sized subset.
+    @data("bit", "const", "def", "defcal", "float", "gate", "include", "int", "let", "measure")
+    def test_reserved_keywords_as_names_are_escaped(self, keyword):
+        """Test that reserved keywords used to name registers and parameters are escaped into
+        another form when output, and the escaping cannot introduce new conflicts."""
+        with self.subTest("register"):
+            qreg = QuantumRegister(1, keyword)
+            qc = QuantumCircuit(qreg)
+            out_qasm = dumps(qc)
+            register_name = self.register_regex.search(out_qasm)
+            self.assertTrue(register_name)
+            self.assertNotEqual(keyword, register_name["name"])
+        with self.subTest("parameter"):
+            qc = QuantumCircuit(1)
+            param = Parameter(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.assertNotEqual(keyword, parameter_name["name"])
+
 
 class TestCircuitQASM3ExporterTemporaryCasesWithBadParameterisation(QiskitTestCase):
     """Test functionality that is not what we _want_, but is what we need to do while the definition
@@ -1569,7 +1650,7 @@ def test_no_include(self):
         self.assertEqual(Exporter(includes=[]).dumps(circuit), expected_qasm)
 
 
-@ddt.ddt
+@ddt
 class TestQASM3ExporterFailurePaths(QiskitTestCase):
     """Tests of the failure paths for the exporter."""
 
@@ -1584,7 +1665,7 @@ def test_disallow_overlapping_classical_registers_if_no_aliasing(self):
         with self.assertRaisesRegex(QASM3ExporterError, r"Clbit .* is in multiple registers.*"):
             exporter.dumps(qc)
 
-    @ddt.data([1, 2, 1.1], [1j, 2])
+    @data([1, 2, 1.1], [1j, 2])
     def test_disallow_for_loops_with_non_integers(self, indices):
         """Test that the exporter rejects ``for`` loops that include non-integer values in their
         index sets."""