Repository: Qiskit/qiskit

Issue:
Qasm3 Exporter needs to export arrays of qubtis for concatenation in alias of OpenQASM3.
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?

Currently Qasm3 Exporter generates `qubit[2] _q; let q = _q[0] || _q[1];` to generate an array of `qubit`s in OpenQASM 3.
https://github.com/Qiskit/qiskit-terra/blob/main/test/python/circuit/test_circuit_qasm3.py#L34-L35

However, I think that the spec allows concatenating of only two arrays.
https://qiskit.github.io/openqasm/language/types.html#register-concatenation-and-slicing
 
In the above case, the type of ` _q[0]` and  `_q[1]` is `qubit` and not `qubit[1]`.

One of quick fixes may be generating `let q = _q[0:0] || _q[1:1];` instead of `let q = _q[0] || _q[1];`.


Discussion:
This isn't 100% decided one way or the other (see also Qiskit/openqasm#218) but right now I'm pretty sure that our internal compiler supports the form the exporter outputs.  I'm not sure if it actually supports concatenating arrays, but likely it does (@taalexander?).
The internal compiler does not like the current form of exporter arrays very much. Rather, we always use the transpiled circuit which is laid out onto physical qubits. At this point, the exporter contains no reference to qubit arrays.
I can make the exporter turn this circuit:
```python
qubits = [Qubit() for _ in [None] * 10]
first_four = QuantumRegister(name="first_four", bits=qubits[:4])
last_five = QuantumRegister(name="last_five", bits=qubits[5:])
alternate = QuantumRegister(name="alternate", bits=qubits[::2])
sporadic = QuantumRegister(name="sporadic", bits=[qubits[4], qubits[2], qubits[9]])

QuantumCircuit(qubits, first_four, last_five, alternate, sporadic)
```
into this OpenQASM 3:
```
OPENQASM 3;
include "stdgates.inc";
qubit[10] _q;
let first_four = _q[0:3];
let last_five = _q[5:9];
let alternate = _q[0:0] || _q[2:2] || _q[4:4] || _q[6:6] || _q[8:8];
let sporadic = _q[4:4] || _q[2:2] || _q[9:9];
```
Is that appropriate for everyone?  I haven't bothered to try and make the exporter cleverer about inserting steps (`alternate` in this example could be more efficiently written as `_q[0:2:8]`), but it's easy if people want it.

(This shouldn't change anything about how `initial_layout` is handled.)

Resolving patch:
diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py
--- a/qiskit/qasm3/ast.py
+++ b/qiskit/qasm3/ast.py
@@ -15,7 +15,7 @@
 """QASM3 AST Nodes"""
 
 import enum
-from typing import Optional, List
+from typing import Optional, List, Union
 
 
 class ASTNode:
@@ -132,17 +132,6 @@ def __init__(self, identifier: Identifier):
         self.identifier = identifier
 
 
-class IndexIdentifier(Identifier):
-    """
-    indexIdentifier
-        : Identifier rangeDefinition
-        | Identifier ( LBRACKET expressionList RBRACKET )?
-        | indexIdentifier '||' indexIdentifier
-    """
-
-    pass
-
-
 class Expression(ASTNode):
     """
     expression
@@ -158,25 +147,40 @@ def __init__(self, something):
         self.something = something
 
 
-class Constant(Expression, enum.Enum):
-    """A constant value defined by the QASM 3 spec."""
+class Range(ASTNode):
+    """
+    A range expression::
 
-    pi = enum.auto()
-    euler = enum.auto()
-    tau = enum.auto()
+        <start>? (: <step>)? : <end>?
+    """
+
+    def __init__(
+        self,
+        start: Optional[Expression] = None,
+        end: Optional[Expression] = None,
+        step: Optional[Expression] = None,
+    ):
+        self.start = start
+        self.step = step
+        self.end = end
 
 
-class IndexIdentifier2(IndexIdentifier):
+class SubscriptedIdentifier(Identifier):
     """
-    indexIdentifier
-        : Identifier rangeDefinition
-        | Identifier ( LBRACKET expressionList RBRACKET )? <--
-        | indexIdentifier '||' indexIdentifier
+    An identifier with subscripted access.
     """
 
-    def __init__(self, identifier: Identifier, expressionList: Optional[List[Expression]] = None):
+    def __init__(self, identifier: Identifier, subscript: Union[Range, Expression]):
         self.identifier = identifier
-        self.expressionList = expressionList
+        self.subscript = subscript
+
+
+class Constant(Expression, enum.Enum):
+    """A constant value defined by the QASM 3 spec."""
+
+    pi = enum.auto()
+    euler = enum.auto()
+    tau = enum.auto()
 
 
 class QuantumMeasurement(ASTNode):
@@ -185,8 +189,8 @@ class QuantumMeasurement(ASTNode):
         : 'measure' indexIdentifierList
     """
 
-    def __init__(self, indexIdentifierList: List[Identifier]):
-        self.indexIdentifierList = indexIdentifierList
+    def __init__(self, identifierList: List[Identifier]):
+        self.identifierList = identifierList
 
 
 class QuantumMeasurementAssignment(Statement):
@@ -196,8 +200,8 @@ class QuantumMeasurementAssignment(Statement):
         | indexIdentifier EQUALS quantumMeasurement  # eg: bits = measure qubits;
     """
 
-    def __init__(self, indexIdentifier: IndexIdentifier2, quantumMeasurement: QuantumMeasurement):
-        self.indexIdentifier = indexIdentifier
+    def __init__(self, identifier: Identifier, quantumMeasurement: QuantumMeasurement):
+        self.identifier = identifier
         self.quantumMeasurement = quantumMeasurement
 
 
@@ -267,9 +271,9 @@ class AliasStatement(ASTNode):
         : 'let' Identifier EQUALS indexIdentifier SEMICOLON
     """
 
-    def __init__(self, identifier: Identifier, qubits: List[IndexIdentifier2]):
+    def __init__(self, identifier: Identifier, concatenation: List[Identifier]):
         self.identifier = identifier
-        self.qubits = qubits
+        self.concatenation = concatenation
 
 
 class QuantumGateModifierName(enum.Enum):
diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py
--- a/qiskit/qasm3/exporter.py
+++ b/qiskit/qasm3/exporter.py
@@ -63,7 +63,8 @@
     Include,
     Header,
     Identifier,
-    IndexIdentifier2,
+    SubscriptedIdentifier,
+    Range,
     QuantumBlock,
     QuantumBarrier,
     Designator,
@@ -448,15 +449,33 @@ def build_quantumdeclarations(self):
         )
         # aliases
         for qreg in self.circuit_ctx[-1].qregs:
-            qubits = []
+            elements = []
+            # Greedily consolidate runs of qubits into ranges.  We don't bother trying to handle
+            # steps; there's no need in generated code.  Even single qubits are referenced as ranges
+            # because the concatenation in an alias statement can only concatenate arraylike values.
+            start_index, prev_index = None, None
             for qubit in qreg:
-                qubits.append(
-                    IndexIdentifier2(
+                cur_index = self.circuit_ctx[-1].find_bit(qubit).index
+                if start_index is None:
+                    start_index = cur_index
+                elif cur_index != prev_index + 1:
+                    elements.append(
+                        SubscriptedIdentifier(
+                            Identifier(self.base_register_name),
+                            Range(start=Integer(start_index), end=Integer(prev_index)),
+                        )
+                    )
+                    start_index = prev_index = cur_index
+                prev_index = cur_index
+            # After the loop, if there were any bits at all, there's always one unemitted range.
+            if len(qreg) != 0:
+                elements.append(
+                    SubscriptedIdentifier(
                         Identifier(self.base_register_name),
-                        [Integer(self.circuit_ctx[-1].find_bit(qubit).index)],
+                        Range(start=Integer(start_index), end=Integer(prev_index)),
                     )
                 )
-            ret.append(AliasStatement(Identifier(qreg.name), qubits))
+            ret.append(AliasStatement(Identifier(qreg.name), elements))
         return ret
 
     def build_quantuminstructions(self, instructions):
@@ -475,17 +494,17 @@ def build_quantuminstructions(self, instructions):
                 else:
                     ret.append(self.build_quantumgatecall(instruction))
             elif isinstance(instruction[0], Barrier):
-                indexIdentifierList = self.build_indexIdentifierlist(instruction[1])
-                ret.append(QuantumBarrier(indexIdentifierList))
+                operands = [self.build_single_bit_reference(operand) for operand in instruction[1]]
+                ret.append(QuantumBarrier(operands))
             elif isinstance(instruction[0], Measure):
                 quantumMeasurement = QuantumMeasurement(
-                    self.build_indexIdentifierlist(instruction[1])
+                    [self.build_single_bit_reference(operand) for operand in instruction[1]]
                 )
-                indexIdentifierList = self.build_indexidentifier(instruction[2][0])
-                ret.append(QuantumMeasurementAssignment(indexIdentifierList, quantumMeasurement))
+                qubit = self.build_single_bit_reference(instruction[2][0])
+                ret.append(QuantumMeasurementAssignment(qubit, quantumMeasurement))
             elif isinstance(instruction[0], Reset):
-                for identifier in instruction[1]:
-                    ret.append(QuantumReset(self.build_indexidentifier(identifier)))
+                for operand in instruction[1]:
+                    ret.append(QuantumReset(self.build_single_bit_reference(operand)))
             else:
                 ret.append(self.build_subroutinecall(instruction))
         return ret
@@ -497,7 +516,7 @@ def build_programblock(self, instructions):
     def build_eqcondition(self, condition):
         """Classical Conditional condition from a instruction.condition"""
         if isinstance(condition[0], Clbit):
-            condition_on = self.build_indexidentifier(condition[0])
+            condition_on = self.build_single_bit_reference(condition[0])
         else:
             condition_on = Identifier(condition[0].name)
         return ComparisonExpression(condition_on, EqualsOperator(), Integer(int(condition[1])))
@@ -519,20 +538,13 @@ def build_quantumArgumentList(self, qregs: [QuantumRegister], circuit=None):
                 )
         return quantumArgumentList
 
-    def build_indexIdentifierlist(self, bitlist: [Bit]):
-        """Builds a indexIdentifierList"""
-        indexIdentifierList = []
-        for bit in bitlist:
-            indexIdentifierList.append(self.build_indexidentifier(bit))
-        return indexIdentifierList
-
     def build_quantumgatecall(self, instruction):
         """Builds a QuantumGateCall"""
         if isinstance(instruction[0], UGate):
             quantumGateName = Identifier("U")
         else:
             quantumGateName = Identifier(self.global_namespace[instruction[0]])
-        indexIdentifierList = self.build_indexIdentifierlist(instruction[1])
+        qubits = [self.build_single_bit_reference(qubit) for qubit in instruction[1]]
         if self.disable_constants:
             parameters = [Expression(param) for param in instruction[0].params]
         else:
@@ -540,28 +552,28 @@ def build_quantumgatecall(self, instruction):
                 Expression(pi_check(param, output="qasm")) for param in instruction[0].params
             ]
 
-        return QuantumGateCall(quantumGateName, indexIdentifierList, parameters=parameters)
+        return QuantumGateCall(quantumGateName, qubits, parameters=parameters)
 
     def build_subroutinecall(self, instruction):
         """Builds a SubroutineCall"""
         identifier = Identifier(self.global_namespace[instruction[0]])
         expressionList = [Expression(param) for param in instruction[0].params]
-        indexIdentifierList = self.build_indexIdentifierlist(instruction[1])
-
+        # TODO: qubits should go inside the brackets of subroutine calls, but neither Terra nor the
+        # AST here really support the calls, so there's no sensible way of writing it yet.
+        indexIdentifierList = [self.build_single_bit_reference(bit) for bit in instruction[1]]
         return SubroutineCall(identifier, indexIdentifierList, expressionList)
 
-    def build_indexidentifier(self, bit: Bit):
-        """Build a IndexIdentifier2"""
-        reg, idx = self.find_bit(bit)
+    def build_single_bit_reference(self, bit: Bit) -> Identifier:
+        """Get an identifier node that refers to one particular bit."""
         if self._physical_qubit and isinstance(bit, Qubit):
             return PhysicalQubitIdentifier(
                 Identifier(str(self.circuit_ctx[-1].find_bit(bit).index))
             )
+        reg, idx = self.find_bit(bit)
         if self._flat_reg:
             bit_name = f"{reg.name}_{idx}"
-            return IndexIdentifier2(Identifier(bit_name))
-        else:
-            return IndexIdentifier2(Identifier(reg.name), [Integer(idx)])
+            return Identifier(bit_name)
+        return SubscriptedIdentifier(Identifier(reg.name), Integer(idx))
 
     def find_bit(self, bit):
         """Returns the register and the index in that register for a particular bit"""
diff --git a/qiskit/qasm3/printer.py b/qiskit/qasm3/printer.py
--- a/qiskit/qasm3/printer.py
+++ b/qiskit/qasm3/printer.py
@@ -33,7 +33,6 @@
     IOModifier,
     Identifier,
     Include,
-    IndexIdentifier2,
     Integer,
     LtOperator,
     PhysicalQubitIdentifier,
@@ -51,9 +50,11 @@
     QuantumMeasurement,
     QuantumMeasurementAssignment,
     QuantumReset,
+    Range,
     ReturnStatement,
     SubroutineCall,
     SubroutineDefinition,
+    SubscriptedIdentifier,
     Version,
 )
 
@@ -177,18 +178,29 @@ def _visit_Expression(self, node: Expression) -> None:
     def _visit_Constant(self, node: Constant) -> None:
         self.stream.write(self._CONSTANT_LOOKUP[node])
 
-    def _visit_IndexIdentifier2(self, node: IndexIdentifier2) -> None:
+    def _visit_SubscriptedIdentifier(self, node: SubscriptedIdentifier) -> None:
         self.visit(node.identifier)
-        if node.expressionList:
-            self._visit_sequence(node.expressionList, start="[", end="]", separator=", ")
+        self.stream.write("[")
+        self.visit(node.subscript)
+        self.stream.write("]")
+
+    def _visit_Range(self, node: Range) -> None:
+        if node.start is not None:
+            self.visit(node.start)
+        self.stream.write(":")
+        if node.step is not None:
+            self.visit(node.step)
+            self.stream.write(":")
+        if node.end is not None:
+            self.visit(node.end)
 
     def _visit_QuantumMeasurement(self, node: QuantumMeasurement) -> None:
         self.stream.write("measure ")
-        self._visit_sequence(node.indexIdentifierList, separator=", ")
+        self._visit_sequence(node.identifierList, separator=", ")
 
     def _visit_QuantumMeasurementAssignment(self, node: QuantumMeasurementAssignment) -> None:
         self._start_line()
-        self.visit(node.indexIdentifier)
+        self.visit(node.identifier)
         self.stream.write(" = ")
         self.visit(node.quantumMeasurement)
         self._end_statement()
@@ -231,7 +243,7 @@ def _visit_AliasStatement(self, node: AliasStatement) -> None:
         self.stream.write("let ")
         self.visit(node.identifier)
         self.stream.write(" = ")
-        self._visit_sequence(node.qubits, separator=" || ")
+        self._visit_sequence(node.concatenation, separator=" || ")
         self._end_statement()
 
     def _visit_QuantumGateModifier(self, node: QuantumGateModifier) -> None:


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
@@ -15,7 +15,7 @@
 from io import StringIO
 
 from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, transpile
-from qiskit.circuit import Parameter
+from qiskit.circuit import Parameter, Qubit
 from qiskit.test import QiskitTestCase
 from qiskit.qasm3 import Exporter, dumps, dump
 from qiskit.qasm import pi
@@ -32,7 +32,7 @@ def setUp(self):
                 "OPENQASM 3;",
                 'include "stdgates.inc";',
                 "qubit[2] _q;",
-                "let q = _q[0] || _q[1];",
+                "let q = _q[0:1];",
                 "U(2*pi, 3*pi, -5*pi) q[0];",
                 "",
             ]
@@ -73,8 +73,8 @@ def test_regs_conds_qasm(self):
                 'include "stdgates.inc";',
                 "bit[3] cr;",
                 "qubit[3] _q;",
-                "let qr1 = _q[0];",
-                "let qr2 = _q[1] || _q[2];",
+                "let qr1 = _q[0:0];",
+                "let qr2 = _q[1:2];",
                 "cr[0] = measure qr1[0];",
                 "cr[1] = measure qr2[0];",
                 "cr[2] = measure qr2[1];",
@@ -92,6 +92,29 @@ def test_regs_conds_qasm(self):
         )
         self.assertEqual(Exporter().dumps(qc), expected_qasm)
 
+    def test_registers_as_aliases(self):
+        """Test that different types of alias creation and concatenation work."""
+        qubits = [Qubit() for _ in [None] * 10]
+        first_four = QuantumRegister(name="first_four", bits=qubits[:4])
+        last_five = QuantumRegister(name="last_five", bits=qubits[5:])
+        alternate = QuantumRegister(name="alternate", bits=qubits[::2])
+        sporadic = QuantumRegister(name="sporadic", bits=[qubits[4], qubits[2], qubits[9]])
+        qc = QuantumCircuit(qubits, first_four, last_five, alternate, sporadic)
+        expected_qasm = "\n".join(
+            [
+                "OPENQASM 3;",
+                'include "stdgates.inc";',
+                "qubit[10] _q;",
+                "let first_four = _q[0:3];",
+                "let last_five = _q[5:9];",
+                # The exporter does not attempt to output steps.
+                "let alternate = _q[0:0] || _q[2:2] || _q[4:4] || _q[6:6] || _q[8:8];",
+                "let sporadic = _q[4:4] || _q[2:2] || _q[9:9];",
+                "",
+            ]
+        )
+        self.assertEqual(Exporter().dumps(qc), expected_qasm)
+
     def test_composite_circuit(self):
         """Test with a composite circuit instruction and barriers"""
         composite_circ_qreg = QuantumRegister(2)
@@ -121,7 +144,7 @@ def test_composite_circuit(self):
                 "}",
                 "bit[2] cr;",
                 "qubit[2] _q;",
-                "let qr = _q[0] || _q[1];",
+                "let qr = _q[0:1];",
                 "h qr[0];",
                 "cx qr[0], qr[1];",
                 "barrier qr[0], qr[1];",
@@ -162,7 +185,7 @@ def test_custom_gate(self):
                 "}",
                 "bit[2] cr;",
                 "qubit[2] _q;",
-                "let qr = _q[0] || _q[1];",
+                "let qr = _q[0:1];",
                 "h qr[0];",
                 "cx qr[0], qr[1];",
                 "barrier qr[0], qr[1];",
@@ -204,7 +227,7 @@ def test_same_composite_circuits(self):
                 "}",
                 "bit[2] cr;",
                 "qubit[2] _q;",
-                "let qr = _q[0] || _q[1];",
+                "let qr = _q[0:1];",
                 "h qr[0];",
                 "cx qr[0], qr[1];",
                 "barrier qr[0], qr[1];",
@@ -252,7 +275,7 @@ def test_composite_circuits_with_same_name(self):
                 "  x q_0;",
                 "}",
                 "qubit[1] _q;",
-                "let qr = _q[0];",
+                "let qr = _q[0:0];",
                 "my_gate qr[0];",
                 f"my_gate_{my_gate_inst2_id} qr[0];",
                 f"my_gate_{my_gate_inst3_id} qr[0];",
@@ -270,7 +293,7 @@ def test_pi_disable_constants_false(self):
                 "OPENQASM 3;",
                 'include "stdgates.inc";',
                 "qubit[2] _q;",
-                "let q = _q[0] || _q[1];",
+                "let q = _q[0:1];",
                 "U(2*pi, 3*pi, -5*pi) q[0];",
                 "",
             ]
@@ -286,7 +309,7 @@ def test_pi_disable_constants_true(self):
                 "OPENQASM 3;",
                 'include "stdgates.inc";',
                 "qubit[2] _q;",
-                "let q = _q[0] || _q[1];",
+                "let q = _q[0:1];",
                 "U(6.283185307179586, 9.42477796076938, -15.707963267948966) q[0];",
                 "",
             ]
@@ -311,7 +334,7 @@ def test_custom_gate_with_unbound_parameter(self):
                 "}",
                 "input float[32] a;",
                 "qubit[1] _q;",
-                "let q = _q[0];",
+                "let q = _q[0:0];",
                 "custom(a) q[0];",
                 "",
             ]
@@ -338,7 +361,7 @@ def test_custom_gate_with_bound_parameter(self):
                 "  rx(0.5) q_0;",
                 "}",
                 "qubit[1] _q;",
-                "let q = _q[0];",
+                "let q = _q[0:0];",
                 "custom q[0];",
                 "",
             ]
@@ -371,8 +394,8 @@ def test_custom_gate_with_params_bound_main_call(self):
                 f"  rz(pi/4) {qubit_name}_1;",
                 "}",
                 "qubit[6] _q;",
-                "let q = _q[0] || _q[1] || _q[2];",
-                "let r = _q[3] || _q[4] || _q[5];",
+                "let q = _q[0:2];",
+                "let r = _q[3:5];",
                 "custom(pi, pi/2) q[0], r[0];",
                 "",
             ]
@@ -404,7 +427,7 @@ def test_reused_custom_gate_parameter(self):
                 "  rx(1) q_0;",
                 "}",
                 "qubit[1] _q;",
-                "let q = _q[0];",
+                "let q = _q[0:0];",
                 f"{circuit_name_0} q[0];",
                 f"{circuit_name_1} q[0];",
                 "",
@@ -423,7 +446,7 @@ def test_unbound_circuit(self):
                 'include "stdgates.inc";',
                 "input float[32] θ;",
                 "qubit[1] _q;",
-                "let q = _q[0];",
+                "let q = _q[0:0];",
                 "rz(θ) q[0];",
                 "",
             ]
@@ -445,7 +468,7 @@ def test_gate_qasm_with_ctrl_state(self):
                 "  x q_0;",
                 "}",
                 "qubit[2] _q;",
-                "let q = _q[0] || _q[1];",
+                "let q = _q[0:1];",
                 "ch_o0 q[0], q[1];",
                 "",
             ]
@@ -469,7 +492,7 @@ def test_custom_gate_collision_with_stdlib(self):
                 "  cx q_0, q_1;",
                 "}",
                 "qubit[2] _q;",
-                "let q = _q[0] || _q[1];",
+                "let q = _q[0:1];",
                 f"cx_{custom_gate_id} q[0], q[1];",
                 "",
             ]
@@ -513,7 +536,7 @@ def test_no_include(self):
                 "  sdg q_0;",
                 "}",
                 "qubit[2] _q;",
-                "let q = _q[0] || _q[1];",
+                "let q = _q[0:1];",
                 "rz(pi/2) q[0];",
                 "sx q[0];",
                 "cx q[0], q[1];",
@@ -653,7 +676,7 @@ def test_reset_statement(self):
                 "  reset q_0;",
                 "}",
                 "qubit[2] _q;",
-                "let qr = _q[0] || _q[1];",
+                "let qr = _q[0:1];",
                 "reset qr[0];",
                 "inner_gate qr[1];",
                 "",