Repository: qBraid/pyqasm

Issue:
[BUG] qasm3_to_qir: "Variable c3 is not an array", despite c3 being an array.
### Environment

- **qBraid-QIR version**: 0.2.3
- **Python version**: 3.11.5
- **Operating system**: Linux c5ce507e9ef3 5.15.153.1-microsoft-standard-WSL2


### What happened?

The following Qiskit circuit is converted to qasm3 using `qiskit.qasm3`.

```python
qubits = QuantumRegister(1)
clbits = ClassicalRegister(1)
circuit = QuantumCircuit(qubits, clbits)
(q0,) = qubits
(c0,) = clbits
 
circuit.h(q0)
circuit.measure(q0, c0)
with circuit.switch(c0) as case:
    with case(0):
        circuit.x(q0)
    with case(1):
        circuit.z(q0)
circuit.measure(q0, c0)
```

This yields the following QASM3:
```qasm
OPENQASM 3.0;
include "stdgates.inc";
bit[1] c3;
int switch_dummy;
qubit[1] q3;
h q3[0];
c3[0] = measure q3[0];
switch_dummy = c3[0];
switch (switch_dummy) {
  case 0 {
    x q3[0];
  }
  case 1 {
    z q3[0];
  }
}
c3[0] = measure q3[0];
```

I get the following error message: `qbraid_qir.qasm3.exceptions.Qasm3ConversionError: Indexing error. Variable c3 is not an array` and `Error at line 8, column 18 in QASM file`.

I.e. the indexing `c3[0]` seems to fail.

### Suggestions (Optional)

_No response_

Hints from issue comments:
@TheGupta2012: Hi @Sola85 , thanks for raising an issue! 

Since, internally, we are handling `c3[0] = measure q3[0];` as classical assignment statements we see this kind of error message.`c3` is technically not an array (because it is a `bit` type scalar variable with 1 bit length), the classical assignment handler raises an error. Whenever we have indexing on the lhs, our semantic analyser expects that the variable is of type `array`.

Also, we are not correctly handling the syntax `c3[0] = measure q3[0];` in our `pyqasm.unroll` function. It makes sense to convert this statement to a `QuantumMeasurementStatement` instead of a `ClassicalAssignment` before appending to the unrolled AST. 

@ryanhill1: @TheGupta2012 Based on your evaluation, would it make sense to transfer this issue to the [pyqasm](https://github.com/qBraid/pyqasm/issues) repo?@TheGupta2012: Yea that makes more sense @ryanhill1.
@Sola85 closing this as I've opened the [issue in pyqasm](https://github.com/qBraid/pyqasm/issues/42)@ryanhill1: @TheGupta2012 Transferred and re-opened original issue, closed your duplicate@Sola85: Awesome, thanks! Will try it out as soon as I find the time!

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f8b7e9..6c473d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,52 +15,29 @@ Types of changes:
 ## [Unreleased]
 
 ### Added
-- Sphinx docs and examples added for pyqasm ([#20](https://github.com/qBraid/pyqasm/pull/20))
-- qBraid header check enabled in format action([#29](https://github.com/qBraid/pyqasm/pull/29))
-- Integrated code coverage with codecov ([#30](https://github.com/qBraid/pyqasm/pull/30))
+
+- Dependabot configuration file [#37](https://github.com/qBraid/pyqasm/pull/37)
 
 ### Improved / Modified
-- Housekeeping updates involving codeowners, workflows, pyproject, and readme ([#16](https://github.com/qBraid/pyqasm/pull/16))
-- Fixed parsing of compile-time constants for register sizes. Statements like `const int[32] N = 3; qubit[N] q;` are now supported ([#21](https://github.com/qBraid/pyqasm/pull/21)).
-- Update project `README.md` ([#22](https://github.com/qBraid/pyqasm/pull/22))
-- Updated sphinx docs page ([#26](https://github.com/qBraid/pyqasm/pull/26))
-- **Major Change**: The default type for `pyqasm.unroller.unroll` has been changed from `pyqasm.elements.Qasm3Module` to `str`. This change is backward-incompatible and will require users to update their code. The following code snippet demonstrates the change - 
-
-```python
-from pyqasm.unroller import unroll
-qasm_str = """
-OPENQASM 3;
-qubit[3] q;
-h q;
-"""
-
-# Old way : The default type for unroll was pyqasm.elements.Qasm3Module
-program = unroll(qasm_str, as_module=True)
-unrolled_qasm_old = program.unrolled_qasm
-
-# New way : The default type for unroll is now str
-unrolled_qasm_new = unroll(qasm_str)
-```
-To force the return type to be `pyqasm.elements.Qasm3Module`, users can set the `as_module` parameter to `True` as shown above to update their code.
+
+- Improved qubit declaration semantics by adding check for quantum registers being declared as predefined constants [#44](https://github.com/qBraid/pyqasm/pull/44)
 
 ### Deprecated
 
 ### Removed
 
 ### Fixed
-- Issue with aliasing of qubits was fixed where aliased qubits were referenced with non-aliased qubits in a quantum gate ([#14](https://github.com/qBraid/pyqasm/pull/14)). The following program is now supported - 
 
-```python
-OPENQASM 3;
-include "stdgates.inc";
-qubit[4] q;
-let alias = q[0:2];
-cx alias[1], q[2];
-```
+- Bug in initial sizes of classical registers. `bit c;` was being initialized with size `32` instead of `1` [#43](https://github.com/qBraid/pyqasm/pull/43)
+- Fixed bug in the handling of classical register type. Whenever a `bit` was referenced in an expression, it was treated as a scalar when it should be treated as an element of a 1D array with type `bit` [#44](https://github.com/qBraid/pyqasm/pull/44)
 
-- Issue with subroutines, when `return` keyword was absent, was fixed in ([#21](https://github.com/qBraid/pyqasm/pull/21))
 
 ### Dependencies
 
+- Update sphinx-autodoc-typehints requirement from <2.5,>=1.24 to >=1.24,<2.6 [#38](https://github.com/qBraid/pyqasm/pull/38)
+- Update sphinx requirement from <8.1.0,>=7.3.7 to >=7.3.7,<8.2.0 [#39](https://github.com/qBraid/pyqasm/pull/39)
+- Update sphinx-rtd-theme requirement from <3.0.0,>=2.0.0 to >=2.0.0,<4.0.0 [#40](https://github.com/qBraid/pyqasm/pull/40)
+
+
 ## References
 [Changelog for release v0.0.1](https://github.com/qBraid/pyqasm/releases/tag/v0.0.1)
diff --git a/pyqasm/elements.py b/pyqasm/elements.py
index 4eafbe0..2fbd5d6 100644
--- a/pyqasm/elements.py
+++ b/pyqasm/elements.py
@@ -8,7 +8,7 @@
 #
 # THERE IS NO WARRANTY for pyqasm, as per Section 15 of the GPL v3.
 
-# pylint: disable=too-many-arguments
+# pylint: disable=too-many-arguments, too-many-instance-attributes
 
 """
 Module defining Qasm3 Converter elements.
@@ -53,6 +53,7 @@ class Variable:
         dims (list[int]): Dimensions of the variable.
         value (Optional[Union[int, float, list]]): Value of the variable.
         is_constant (bool): Flag indicating if the variable is constant.
+        is_register (bool): Flag indicating if the variable is a register.
         readonly(bool): Flag indicating if the variable is readonly.
 
     """
@@ -65,6 +66,7 @@ def __init__(
         dims: Optional[list[int]] = None,
         value: Optional[Union[int, float, np.ndarray]] = None,
         is_constant: bool = False,
+        is_register: bool = False,
         readonly: bool = False,
     ):
         self.name = name
@@ -73,8 +75,17 @@ def __init__(
         self.dims = dims
         self.value = value
         self.is_constant = is_constant
+        self.is_register = is_register
         self.readonly = readonly
 
+    def __repr__(self) -> str:
+        return (
+            f"Variable(name = {self.name}, base_type = {self.base_type}, "
+            f"base_size = {self.base_size}, dimensions = {self.dims}, "
+            f"value = {self.value}, is_constant = {self.is_constant}, "
+            f"readonly = {self.readonly}, is_register = {self.is_register})"
+        )
+
 
 class Qasm3Module:
     """
diff --git a/pyqasm/maps.py b/pyqasm/maps.py
index 0e744d0..4cf507c 100644
--- a/pyqasm/maps.py
+++ b/pyqasm/maps.py
@@ -721,7 +721,7 @@ def qasm_variable_type_cast(openqasm_type, var_name, base_size, rhs_value):
     # not sure if we wanna hande array bit assignments too.
     # For now, we only cater to single bit assignment.
     if openqasm_type == BitType:
-        return bool(rhs_value)
+        return rhs_value != 0
     if openqasm_type == AngleType:
         return rhs_value  # not sure
 
diff --git a/pyqasm/validator.py b/pyqasm/validator.py
index 47582d8..5d2faed 100644
--- a/pyqasm/validator.py
+++ b/pyqasm/validator.py
@@ -15,7 +15,7 @@
 from typing import Any, Optional, Union
 
 import numpy as np
-from openqasm3.ast import ArrayType, ClassicalDeclaration
+from openqasm3.ast import ArrayType, ClassicalDeclaration, FloatType
 from openqasm3.ast import IntType as Qasm3IntType
 from openqasm3.ast import QuantumGate, QuantumGateDefinition, ReturnStatement, SubroutineDefinition
 
@@ -158,6 +158,27 @@ def validate_variable_assignment_value(variable: Variable, value) -> Any:
 
         return type_casted_value
 
+    @staticmethod
+    def validate_classical_type(base_type, base_size, var_name, span) -> None:
+        """Validate the type and size of a classical variable.
+
+        Args:
+            base_type (Any): The base type of the variable.
+            base_size (int): The size of the variable.
+            var_name (str): The name of the variable.
+            span (Span): The span of the variable.
+
+        Raises:
+            ValidationError: If the type or size is invalid.
+        """
+        if not isinstance(base_size, int) or base_size <= 0:
+            raise_qasm3_error(f"Invalid base size {base_size} for variable {var_name}", span=span)
+
+        if isinstance(base_type, FloatType) and base_size not in [32, 64]:
+            raise_qasm3_error(
+                f"Invalid base size {base_size} for float variable {var_name}", span=span
+            )
+
     @staticmethod
     def validate_array_assignment_values(
         variable: Variable, dimensions: list[int], values: np.ndarray
diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py
index bfa420b..0cf0808 100644
--- a/pyqasm/visitor.py
+++ b/pyqasm/visitor.py
@@ -243,13 +243,13 @@ def _in_gate_scope(self) -> bool:
     def _in_block_scope(self) -> bool:  # block scope is for if/else/for/while constructs
         return len(self._scope) > 1 and self._get_curr_context() == Context.BLOCK
 
-    def _visit_qubit_register(
+    def _visit_quantum_register(
         self, register: qasm3_ast.QubitDeclaration
     ) -> list[qasm3_ast.QubitDeclaration]:
-        """Visit a Qubit / Classical declaration statement.
+        """Visit a Qubit declaration statement.
 
         Args:
-            register (QubitDeclaration|ClassicalDeclaration): The register name and size.
+            register (QubitDeclaration): The register name and size.
 
         Returns:
             None
@@ -272,17 +272,19 @@ def _visit_qubit_register(
 
         if self._check_in_scope(register_name):
             raise_qasm3_error(
-                f"Invalid declaration of register with name '{register_name}'", span=register.span
+                f"Re-declaration of quantum register with name '{register_name}'",
+                span=register.span,
+            )
+
+        if register_name in CONSTANTS_MAP:
+            raise_qasm3_error(
+                f"Can not declare quantum register with keyword name '{register_name}'",
+                span=register.span,
             )
 
         self._add_var_in_scope(
             Variable(
-                register_name,
-                qasm3_ast.QubitDeclaration,
-                register_size,
-                None,
-                None,
-                False,
+                register_name, qasm3_ast.QubitDeclaration, register_size, None, None, False, True
             )
         )
         size_map[f"{register_name}"] = register_size
@@ -857,11 +859,34 @@ def _visit_classical_declaration(
 
         init_value = None
         base_type = statement.type
+        dimensions = []
         final_dimensions = []
 
         if isinstance(base_type, qasm3_ast.ArrayType):
             dimensions = base_type.dimensions
+            base_type = base_type.base_type
 
+        base_size = 1
+        if not isinstance(base_type, qasm3_ast.BoolType):
+            initial_size = 1 if isinstance(base_type, qasm3_ast.BitType) else 32
+            base_size = (
+                initial_size
+                if not hasattr(base_type, "size") or base_type.size is None
+                else Qasm3ExprEvaluator.evaluate_expression(base_type.size, const_expr=True)[0]
+            )
+        Qasm3Validator.validate_classical_type(base_type, base_size, var_name, statement.span)
+
+        # initialize the bit register
+        if isinstance(base_type, qasm3_ast.BitType):
+            final_dimensions = [base_size]
+            init_value = np.full(final_dimensions, 0)
+
+        if len(dimensions) > 0:
+            # bit type arrays are not allowed
+            if isinstance(base_type, qasm3_ast.BitType):
+                raise_qasm3_error(
+                    f"Can not declare array {var_name} with type 'bit'", span=statement.span
+                )
             if len(dimensions) > MAX_ARRAY_DIMENSIONS:
                 raise_qasm3_error(
                     f"Invalid dimensions {len(dimensions)} for array declaration for {var_name}. "
@@ -869,8 +894,6 @@ def _visit_classical_declaration(
                     span=statement.span,
                 )
 
-            base_type = base_type.base_type
-            num_elements = 1
             for dim in dimensions:
                 dim_value = Qasm3ExprEvaluator.evaluate_expression(dim, const_expr=True)[0]
                 if not isinstance(dim_value, int) or dim_value <= 0:
@@ -879,10 +902,10 @@ def _visit_classical_declaration(
                         span=statement.span,
                     )
                 final_dimensions.append(dim_value)
-                num_elements *= dim_value
 
             init_value = np.full(final_dimensions, None)
 
+        # populate the variable
         if statement.init_expression:
             if isinstance(statement.init_expression, qasm3_ast.ArrayLiteral):
                 init_value = self._evaluate_array_initialization(
@@ -893,27 +916,17 @@ def _visit_classical_declaration(
                     statement.init_expression
                 )
                 statements.extend(stmts)
-        base_size = 1
-        if not isinstance(base_type, qasm3_ast.BoolType):
-            initial_size = 1 if isinstance(base_type, qasm3_ast.BitType) else 32
-            base_size = (
-                initial_size
-                if not hasattr(base_type, "size") or base_type.size is None
-                else Qasm3ExprEvaluator.evaluate_expression(base_type.size, const_expr=True)[0]
-            )
-
-        if not isinstance(base_size, int) or base_size <= 0:
-            raise_qasm3_error(
-                f"Invalid base size {base_size} for variable {var_name}", span=statement.span
-            )
 
-        if isinstance(base_type, qasm3_ast.FloatType) and base_size not in [32, 64]:
-            raise_qasm3_error(
-                f"Invalid base size {base_size} for float variable {var_name}", span=statement.span
-            )
-
-        variable = Variable(var_name, base_type, base_size, final_dimensions, init_value)
+        variable = Variable(
+            var_name,
+            base_type,
+            base_size,
+            final_dimensions,
+            init_value,
+            is_register=isinstance(base_type, qasm3_ast.BitType),
+        )
 
+        # validate the assignment
         if statement.init_expression:
             if isinstance(init_value, np.ndarray):
                 assert variable.dims is not None
@@ -925,7 +938,6 @@ def _visit_classical_declaration(
         self._add_var_in_scope(variable)
 
         if isinstance(base_type, qasm3_ast.BitType):
-            # handle classical register declaration
             self._global_creg_size_map[var_name] = base_size
             current_classical_size = len(self._clbit_labels)
             for i in range(base_size):
@@ -1513,7 +1525,7 @@ def visit_statement(self, statement: qasm3_ast.Statement) -> list[qasm3_ast.Stat
             qasm3_ast.QuantumMeasurementStatement: self._visit_measurement,
             qasm3_ast.QuantumReset: self._visit_reset,
             qasm3_ast.QuantumBarrier: self._visit_barrier,
-            qasm3_ast.QubitDeclaration: self._visit_qubit_register,
+            qasm3_ast.QubitDeclaration: self._visit_quantum_register,
             qasm3_ast.QuantumGateDefinition: self._visit_gate_definition,
             qasm3_ast.QuantumGate: self._visit_generic_gate_operation,
             qasm3_ast.ClassicalDeclaration: self._visit_classical_declaration,


Test patch:
diff --git a/tests/declarations/test_quantum.py b/tests/declarations/test_quantum.py
index 8e4b958..ef197f0 100644
--- a/tests/declarations/test_quantum.py
+++ b/tests/declarations/test_quantum.py
@@ -115,7 +115,7 @@ def test_qubit_clbit_declarations():
 
 def test_qubit_redeclaration_error():
     """Test redeclaration of qubit"""
-    with pytest.raises(ValidationError, match="Invalid declaration of register with name 'q1'"):
+    with pytest.raises(ValidationError, match="Re-declaration of quantum register with name 'q1'"):
         qasm3_string = """
         OPENQASM 3;
         include "stdgates.inc";
@@ -125,6 +125,19 @@ def test_qubit_redeclaration_error():
         validate(qasm3_string)
 
 
+def test_invalid_qubit_name():
+    """Test that qubit name can not be one of constants"""
+    with pytest.raises(
+        ValidationError, match="Can not declare quantum register with keyword name 'pi'"
+    ):
+        qasm3_string = """
+        OPENQASM 3;
+        include "stdgates.inc";
+        qubit pi;
+        """
+        validate(qasm3_string)
+
+
 def test_clbit_redeclaration_error():
     """Test redeclaration of clbit"""
     with pytest.raises(ValidationError, match=r"Re-declaration of variable c1"):
diff --git a/tests/resources/variables.py b/tests/resources/variables.py
index 972789b..a89edc6 100644
--- a/tests/resources/variables.py
+++ b/tests/resources/variables.py
@@ -47,7 +47,7 @@
         int x;
         qubit x;
         """,
-        "Invalid declaration of register with name 'x'",
+        "Re-declaration of quantum register with name 'x'",
     ),
     "variable_redeclaration_with_qubits_2": (
         """
@@ -164,6 +164,24 @@
         """,
         "Invalid dimensions for array assignment to variable x. Expected 3 but got 1",
     ),
+    "invalid_bit_type_array_1": (
+        """
+        OPENQASM 3.0;
+        include "stdgates.inc";
+
+        array[bit, 3] x;
+        """,
+        "Can not declare array x with type 'bit'",
+    ),
+    "invalid_bit_type_array_2": (
+        """
+        OPENQASM 3.0;
+        include "stdgates.inc";
+
+        array[bit[32], 3] x;
+        """,
+        "Can not declare array x with type 'bit'",
+    ),
 }
 
 ASSIGNMENT_TESTS = {
diff --git a/tests/test_expressions.py b/tests/test_expressions.py
index 916b062..9a5aa15 100644
--- a/tests/test_expressions.py
+++ b/tests/test_expressions.py
@@ -17,7 +17,7 @@
 from pyqasm.exceptions import ValidationError
 from pyqasm.unroller import unroll
 from pyqasm.validate import validate
-from tests.utils import check_single_qubit_rotation_op
+from tests.utils import check_measure_op, check_single_qubit_gate_op, check_single_qubit_rotation_op
 
 
 def test_correct_expressions():
@@ -47,6 +47,27 @@ def test_correct_expressions():
     check_single_qubit_rotation_op(result.unrolled_ast, 2, [0] * 2, rz_expression_values, "rz")
 
 
+def test_bit_in_expression():
+    qasm_str = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+
+    bit[1] c3;
+    qubit[1] q3;
+    int dummy_int;
+    h q3[0];
+    c3[0] = measure q3[0];
+    dummy_int = c3[0];
+    """
+
+    result = unroll(qasm_str, as_module=True)
+    assert result.num_qubits == 1
+    assert result.num_clbits == 1
+    check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "h")
+    meas_pairs = [(("q3", 0), ("c3", 0))]
+    check_measure_op(result.unrolled_ast, 1, meas_pairs)
+
+
 def test_incorrect_expressions():
     with pytest.raises(ValidationError, match=r"Unsupported expression type .*"):
         validate("OPENQASM 3; qubit q; rz(1 - 2 + 32im) q;")
diff --git a/tests/test_sizeof.py b/tests/test_sizeof.py
index 382a991..ba36d15 100644
--- a/tests/test_sizeof.py
+++ b/tests/test_sizeof.py
@@ -55,25 +55,21 @@ def test_sizeof_multiple_types():
     OPENQASM 3;
     include "stdgates.inc";
 
-    array[bit, 3, 2] my_bits;
     array[int[32], 3, 2] my_ints;
     array[float[64], 3, 2] my_floats;
 
-    int[32] size1 = sizeof(my_bits); // this is 3
+    int[32] size1 = sizeof(my_ints, 1); // this is 2
 
-    int[32] size2 = sizeof(my_ints, 1); // this is 2
-
-    int[32] size3 = sizeof(my_floats, 0); // this is 3 too
+    int[32] size2 = sizeof(my_floats, 0); // this is 3 
     qubit[2] q;
 
-    rx(size1) q[0];
+    rx(size1) q[1];
     rx(size2) q[1];
-    rx(size3) q[1];
     """
 
     result = unroll(qasm3_string, as_module=True)
     assert result.num_qubits == 2
-    check_single_qubit_rotation_op(result.unrolled_ast, 3, [0, 1, 1], [3, 2, 3], "rx")
+    check_single_qubit_rotation_op(result.unrolled_ast, 2, [1, 1], [2, 3], "rx")
 
 
 def test_unsupported_target():
diff --git a/tests/test_switch.py b/tests/test_switch.py
index d6949c7..7c85556 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -291,7 +291,7 @@ def test_invalid_scalar_switch_target(invalid_type):
         validate(qasm3_switch_program)
 
 
-@pytest.mark.parametrize("invalid_type", ["float", "bool", "bit"])
+@pytest.mark.parametrize("invalid_type", ["float", "bool"])
 def test_invalid_array_switch_target(invalid_type):
     """Test that switch raises error if target is array element and not an integer."""
 
diff --git a/tests/utils.py b/tests/utils.py
index f33663d..3b7216d 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -102,6 +102,32 @@ def check_u3_gate_op(unrolled_ast, num_gates, qubit_list, param_list):
     assert op_count == num_gates
 
 
+def check_measure_op(unrolled_ast, num_ops, meas_pairs):
+    """Check that the unrolled ast contains the correct number of measurements.
+
+    Args:
+        unrolled_ast (qasm3_ast.Program): The unrolled ast to check.
+        num_ops (int): The number of measurements to check for.
+        meas_pairs (list[tuple[(str, int), (str, int)]]): The list of measurement
+                                                          pairs to check for.
+    """
+
+    meas_count = 0
+    for stmt in unrolled_ast.statements:
+        if isinstance(stmt, qasm3_ast.QuantumMeasurementStatement):
+            source_name = stmt.measure.qubit.name.name
+            source_index = stmt.measure.qubit.indices[0][0].value
+            target_name = stmt.target.name.name
+            target_index = stmt.target.indices[0][0].value
+
+            # preserve order of measurement pairs
+            assert (source_name, source_index) == meas_pairs[meas_count][0]
+            assert (target_name, target_index) == meas_pairs[meas_count][1]
+            meas_count += 1
+
+    assert meas_count == num_ops
+
+
 def check_single_qubit_rotation_op(unrolled_ast, num_gates, qubit_list, param_list, gate_name):
     if gate_name == "u3":
         check_u3_gate_op(unrolled_ast, num_gates, qubit_list, param_list)