Repository: qBraid/qbraid-qir (language: python)

Issue:
Refactor the `BasicQasmVisitor`
### Feature Description

The `qbraid_qir/qasm3/visitor.py` is growing rapidly with ~2200 lines at present. The `BasicQasmVisitor` class should be split into different handlers according to qasm's language structure. 

This is required to improve the readability and modularity of code.


### Implementation (Optional)

- The first level would be to identify and extract the methods which perform common functions like extracting qubits of an operation, unrolling a range, validating unique qubits etc. These methods can be easily decoupled from the `self` object and its attributes 
- Next, we want to have handlers for the more complex qasm3 constructs viz. `SubRoutines`. Since they require a lot of peripheral methods, it is best that they are kept in separate files for clarity and modularity. `tests/qasm3_qir/converter` is a good example of the type of segregation we want.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3396a4f..ee573b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ Types of changes:
 * Add support for pauli measurement operators in `cirq` converter ( [#144](https://github.com/qBraid/qbraid-qir/pull/144) )
 
 ### 🌟  Improvements 
+* Re-factor the `BasicQasmVisitor` and improve modularity ( [#142](https://github.com/qBraid/qbraid-qir/pull/142) )
 
 ### 📜  Documentation 
 * Housekeeping updates for release ( [#135](https://github.com/qBraid/qbraid-qir/pull/135) )
diff --git a/qbraid_qir/cirq/visitor.py b/qbraid_qir/cirq/visitor.py
index df38603..20408da 100644
--- a/qbraid_qir/cirq/visitor.py
+++ b/qbraid_qir/cirq/visitor.py
@@ -25,7 +25,7 @@
 from .elements import CirqModule
 from .opsets import map_cirq_op_to_pyqir_callable
 
-_log = logging.getLogger(name=__name__)
+logger = logging.getLogger(__name__)
 
 
 class CircuitElementVisitor(metaclass=ABCMeta):
@@ -58,7 +58,7 @@ def __init__(self, initialize_runtime: bool = True, record_output: bool = True):
         self._record_output = record_output
 
     def visit_cirq_module(self, module: CirqModule) -> None:
-        _log.debug("Visiting Cirq module '%s' (%d)", module.name, module.num_qubits)
+        logger.debug("Visiting Cirq module '%s' (%d)", module.name, module.num_qubits)
         self._module = module.module
         context = self._module.context
         entry = pyqir.entry_point(self._module, module.name, module.num_qubits, module.num_clbits)
@@ -90,7 +90,7 @@ def record_output(self, module: CirqModule) -> None:
             pyqir.rt.result_record_output(self._builder, result_ref, Constant.null(i8p))
 
     def visit_register(self, qids: List[cirq.Qid]) -> None:
-        _log.debug("Visiting qids '%s'", str(qids))
+        logger.debug("Visiting qids '%s'", str(qids))
 
         if not isinstance(qids, list):
             raise TypeError("Parameter is not a list.")
@@ -99,7 +99,7 @@ def visit_register(self, qids: List[cirq.Qid]) -> None:
             raise TypeError("All elements in the list must be of type cirq.Qid.")
 
         self._qubit_labels.update({bit: n + len(self._qubit_labels) for n, bit in enumerate(qids)})
-        _log.debug("Added labels for qubits %s", str(qids))
+        logger.debug("Added labels for qubits %s", str(qids))
 
     def visit_operation(self, operation: cirq.Operation) -> None:
         qlabels = [self._qubit_labels.get(bit) for bit in operation.qubits]
@@ -107,7 +107,7 @@ def visit_operation(self, operation: cirq.Operation) -> None:
         results = [pyqir.result(self._module.context, n) for n in qlabels]
 
         def handle_measurement(pyqir_func):
-            _log.debug("Visiting measurement operation '%s'", str(operation))
+            logger.debug("Visiting measurement operation '%s'", str(operation))
             for qubit, result in zip(qubits, results):
                 self._measured_qubits[pyqir.qubit_id(qubit)] = True
                 pyqir_func(self._builder, qubit, result)
diff --git a/qbraid_qir/qasm3/README.md b/qbraid_qir/qasm3/README.md
index 9c58c83..18f9bcc 100644
--- a/qbraid_qir/qasm3/README.md
+++ b/qbraid_qir/qasm3/README.md
@@ -22,8 +22,8 @@
 | BranchingStatement             | 🔜          | In progress            |
 | SubroutineDefinition           | 🔜          | In progress            |
 | Looping statements(eg. for)    | 🔜          | In progress            |
+| RangeDefinition                | 🔜          | In progress            |
 | IODeclaration                  | 📋          | Planned                |
-| RangeDefinition                | 📋          | Planned                |
 | Pragma                         | ❓          | Unsure                 |
 | Annotations                    | ❓          | Unsure                 |
 | Pulse-level ops (e.g. delay)   | ❌          | Not supported by QIR   |
diff --git a/qbraid_qir/qasm3/analyzer.py b/qbraid_qir/qasm3/analyzer.py
new file mode 100644
index 0000000..7077c00
--- /dev/null
+++ b/qbraid_qir/qasm3/analyzer.py
@@ -0,0 +1,182 @@
+# Copyright (C) 2024 qBraid
+#
+# This file is part of the qBraid-SDK
+#
+# The qBraid-SDK is free software released under the GNU General Public License v3
+# or later. You can redistribute and/or modify it under the terms of the GPL v3.
+# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
+#
+# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.
+
+"""
+Module with analysis functions for QASM3 visitor
+
+"""
+from typing import Any
+
+from openqasm3.ast import (
+    BinaryExpression,
+    IndexExpression,
+    IntegerLiteral,
+    RangeDefinition,
+    UnaryExpression,
+)
+
+from .elements import Variable
+from .exceptions import Qasm3ConversionError, raise_qasm3_error
+
+
+class Qasm3Analyzer:
+    """Class with utility functions for analyzing QASM3 elements"""
+
+    @staticmethod
+    def analyze_classical_indices(indices: list[IntegerLiteral], var: Variable) -> None:
+        """Validate the indices for a classical variable.
+
+        Args:
+            indices (list[list[Any]]): The indices to validate.
+            var_name (Variable): The variable to verify
+
+        Raises:
+            Qasm3ConversionError: If the indices are invalid.
+
+        Returns:
+            list: The list of indices.
+        """
+        indices_list = []
+        var_name = var.name
+        var_dimensions = var.dims
+
+        if not var_dimensions:
+            raise_qasm3_error(
+                message=f"Indexing error. Variable {var_name} is not an array",
+                err_type=Qasm3ConversionError,
+                span=indices[0].span,
+            )
+        if len(indices) != len(var_dimensions):
+            raise_qasm3_error(
+                message=f"Invalid number of indices for variable {var_name}. "
+                f"Expected {len(var_dimensions)} but got {len(indices)}",
+                err_type=Qasm3ConversionError,
+                span=indices[0].span,
+            )
+
+        for i, index in enumerate(indices):
+            if isinstance(index, RangeDefinition):
+                raise_qasm3_error(
+                    message=f"Range based indexing {index} not supported for "
+                    f"classical variable {var_name}",
+                    err_type=Qasm3ConversionError,
+                    span=index.span,
+                )
+
+            if not isinstance(index, IntegerLiteral):
+                raise_qasm3_error(
+                    message=f"Unsupported index type {type(index)} for "
+                    f"classical variable {var_name}",
+                    err_type=Qasm3ConversionError,
+                    span=index.span,
+                )
+            index_value = index.value
+            curr_dimension = var_dimensions[i]
+
+            if index_value < 0 or index_value >= curr_dimension:
+                raise_qasm3_error(
+                    message=f"Index {index_value} out of bounds for dimension {i+1} "
+                    f"of variable {var_name}",
+                    err_type=Qasm3ConversionError,
+                    span=index.span,
+                )
+            indices_list.append(index_value)
+
+        return indices_list
+
+    @staticmethod
+    def analyze_index_expression(index_expr: IndexExpression) -> tuple[str, list[list]]:
+        """analyze an index expression to get the variable name and indices.
+
+        Args:
+            index_expr (IndexExpression): The index expression to analyze.
+
+        Returns:
+            tuple[str, list[list]]: The variable name and indices.
+
+        """
+        indices = []
+        var_name = None
+        comma_separated = False
+
+        if isinstance(index_expr.collection, IndexExpression):
+            while isinstance(index_expr, IndexExpression):
+                indices.append(index_expr.index[0])
+                index_expr = index_expr.collection
+        else:
+            comma_separated = True
+            indices = index_expr.index
+
+        var_name = index_expr.collection.name if comma_separated else index_expr.name
+        if not comma_separated:
+            indices = indices[::-1]
+
+        return var_name, indices
+
+    @staticmethod
+    def find_array_element(multi_dim_arr: list[Any], indices: list[int]) -> Any:
+        """Find the value of an array at the specified indices.
+
+        Args:
+            multi_dim_arr (list): The multi-dimensional list to search.
+            indices (list[int]): The indices to search.
+
+        Returns:
+            Any: The value at the specified indices.
+        """
+        temp = multi_dim_arr
+        for index in indices:
+            temp = temp[index]
+        return temp
+
+    @staticmethod
+    def analyse_branch_condition(condition: Any) -> bool:
+        """
+        analyze the branching condition to determine the branch to take
+
+        Args:
+            condition (Any): The condition to analyze
+
+        Returns:
+            bool: The branch to take
+        """
+
+        if isinstance(condition, UnaryExpression):
+            if condition.op.name != "!":
+                raise_qasm3_error(
+                    message=f"Unsupported unary expression '{condition.op.name}' in if condition",
+                    err_type=Qasm3ConversionError,
+                    span=condition.span,
+                )
+            return False
+        if isinstance(condition, BinaryExpression):
+            if condition.op.name != "==":
+                raise_qasm3_error(
+                    message=f"Unsupported binary expression '{condition.op.name}' in if condition",
+                    err_type=Qasm3ConversionError,
+                    span=condition.span,
+                )
+            if not isinstance(condition.lhs, IndexExpression):
+                raise_qasm3_error(
+                    message=f"Unsupported expression type '{type(condition.rhs)}' in if condition",
+                    err_type=Qasm3ConversionError,
+                    span=condition.span,
+                )
+            return condition.rhs.value != 0
+        if not isinstance(condition, IndexExpression):
+            raise_qasm3_error(
+                message=(
+                    f"Unsupported expression type '{type(condition)}' in if condition. "
+                    "Can only be a simple comparison"
+                ),
+                err_type=Qasm3ConversionError,
+                span=condition.span,
+            )
+        return True
diff --git a/qbraid_qir/qasm3/exceptions.py b/qbraid_qir/qasm3/exceptions.py
index c44ed14..2bfe106 100644
--- a/qbraid_qir/qasm3/exceptions.py
+++ b/qbraid_qir/qasm3/exceptions.py
@@ -12,8 +12,40 @@
 Module defining exceptions for errors raised during QASM3 conversions.
 
 """
+import logging
+from typing import Optional, Type
+
+from openqasm3.ast import Span
+
 from qbraid_qir.exceptions import QirConversionError
 
 
 class Qasm3ConversionError(QirConversionError):
     """Class for errors raised when converting an OpenQASM 3 program to QIR."""
+
+
+def raise_qasm3_error(
+    message: Optional[str] = None,
+    err_type: Type[Exception] = Qasm3ConversionError,
+    span: Optional[Span] = None,
+    raised_from: Optional[Exception] = None,
+) -> None:
+    """Raises a QASM3 conversion error with optional chaining from another exception.
+
+    Args:
+        message: The error message. If not provided, a default message will be used.
+        err_type: The type of error to raise.
+        span: The span (location) in the QASM file where the error occurred.
+        raised_from: Optional exception from which this error was raised (chaining).
+
+    Raises:
+        err_type: The error type initialized with the specified message and chained exception.
+    """
+    if span:
+        logging.error(
+            "Error at line %s, column %s in QASM file", span.start_line, span.start_column
+        )
+
+    if raised_from:
+        raise err_type(message) from raised_from
+    raise err_type(message)
diff --git a/qbraid_qir/qasm3/expressions.py b/qbraid_qir/qasm3/expressions.py
new file mode 100644
index 0000000..2f5973e
--- /dev/null
+++ b/qbraid_qir/qasm3/expressions.py
@@ -0,0 +1,237 @@
+# Copyright (C) 2024 qBraid
+#
+# This file is part of the qBraid-SDK
+#
+# The qBraid-SDK is free software released under the GNU General Public License v3
+# or later. You can redistribute and/or modify it under the terms of the GPL v3.
+# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
+#
+# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.
+
+"""
+Module containing the class for evaluating QASM3 expressions.
+
+"""
+
+from openqasm3.ast import BinaryExpression, BooleanLiteral, BoolType, DurationLiteral, FloatLiteral
+from openqasm3.ast import FloatType as Qasm3FloatType
+from openqasm3.ast import (
+    FunctionCall,
+    Identifier,
+    ImaginaryLiteral,
+    IndexExpression,
+    IntegerLiteral,
+)
+from openqasm3.ast import IntType as Qasm3IntType
+from openqasm3.ast import UnaryExpression
+
+from .analyzer import Qasm3Analyzer
+from .exceptions import Qasm3ConversionError, raise_qasm3_error
+from .maps import CONSTANTS_MAP, qasm3_expression_op_map
+from .validator import Qasm3Validator
+
+
+class Qasm3ExprEvaluator:
+    """Class for evaluating QASM3 expressions."""
+
+    visitor_obj = None
+
+    @classmethod
+    def set_visitor_obj(cls, visitor_obj):
+        cls.visitor_obj = visitor_obj
+
+    @classmethod
+    def _check_var_in_scope(cls, var_name, expression):
+        """Checks if a variable is in scope.
+
+        Args:
+            var_name: The name of the variable to check.
+            expression: The expression containing the variable.
+        Raises:
+            Qasm3ConversionError: If the variable is undefined in the current scope.
+        """
+
+        if not cls.visitor_obj._check_in_scope(var_name):
+            raise_qasm3_error(
+                f"Undefined identifier {var_name} in expression",
+                Qasm3ConversionError,
+                expression.span,
+            )
+
+    @classmethod
+    def _check_var_constant(cls, var_name, const_expr, expression):
+        """Checks if a variable is constant.
+
+        Args:
+            var_name: The name of the variable to check.
+            const_expr: Whether the expression is a constant.
+            expression: The expression containing the variable.
+
+        Raises:
+            Qasm3ConversionError: If the variable is not a constant in the given
+                                expression.
+        """
+        const_var = cls.visitor_obj._get_from_visible_scope(var_name).is_constant
+        if const_expr and not const_var:
+            raise_qasm3_error(
+                f"Variable '{var_name}' is not a constant in given expression",
+                Qasm3ConversionError,
+                expression.span,
+            )
+
+    @classmethod
+    def _check_var_type(cls, var_name, reqd_type, expression):
+        """Check the type of a variable and raise an error if it does not match the
+        required type.
+
+        Args:
+            var_name: The name of the variable to check.
+            reqd_type: The required type of the variable.
+            expression: The expression where the variable is used.
+
+        Raises:
+            Qasm3ConversionError: If the variable has an invalid type for the required type.
+        """
+
+        if not Qasm3Validator.validate_variable_type(
+            cls.visitor_obj._get_from_visible_scope(var_name), reqd_type
+        ):
+            raise_qasm3_error(
+                f"Invalid type of variable {var_name} for required type {reqd_type}",
+                Qasm3ConversionError,
+                expression.span,
+            )
+
+    @staticmethod
+    def _check_var_initialized(var_name, var_value, expression):
+        """Checks if a variable is initialized and raises an error if it is not.
+
+        Args:
+            var_name (str): The name of the variable.
+            var_value: The value of the variable.
+            expression: The expression where the variable is used.
+        Raises:
+            Qasm3ConversionError: If the variable is uninitialized.
+        """
+
+        if var_value is None:
+            raise_qasm3_error(
+                f"Uninitialized variable {var_name} in expression",
+                Qasm3ConversionError,
+                expression.span,
+            )
+
+    @classmethod
+    def _get_var_value(cls, var_name, indices, expression):
+        """Retrieves the value of a variable.
+
+        Args:
+            var_name (str): The name of the variable.
+            indices (list): The indices of the variable (if it is an array).
+            expression (Identifier or Expression): The expression representing the variable.
+        Returns:
+            var_value: The value of the variable.
+        """
+
+        var_value = None
+        if isinstance(expression, Identifier):
+            var_value = cls.visitor_obj._get_from_visible_scope(var_name).value
+        else:
+            validated_indices = Qasm3Analyzer.analyze_classical_indices(
+                indices, cls.visitor_obj._get_from_visible_scope(var_name)
+            )
+            var_value = Qasm3Analyzer.find_array_element(
+                cls.visitor_obj._get_from_visible_scope(var_name).value, validated_indices
+            )
+        return var_value
+
+    @classmethod
+    # pylint: disable-next=too-many-return-statements, too-many-branches
+    def evaluate_expression(cls, expression, const_expr: bool = False, reqd_type=None):
+        """Evaluate an expression. Scalar types are assigned by value.
+
+        Args:
+            expression (Any): The expression to evaluate.
+            const_expr (bool): Whether the expression is a constant. Defaults to False.
+            reqd_type (Any): The required type of the expression. Defaults to None.
+
+        Returns:
+            Any : The result of the evaluation.
+
+        Raises:
+            Qasm3ConversionError: If the expression is not supported.
+        """
+        if expression is None:
+            return None
+
+        if isinstance(expression, (ImaginaryLiteral, DurationLiteral)):
+            raise_qasm3_error(
+                f"Unsupported expression type {type(expression)}",
+                Qasm3ConversionError,
+                expression.span,
+            )
+
+        def _process_variable(var_name, indices=None):
+            cls._check_var_in_scope(var_name, expression)
+            cls._check_var_constant(var_name, const_expr, expression)
+            cls._check_var_type(var_name, reqd_type, expression)
+            var_value = cls._get_var_value(var_name, indices, expression)
+            Qasm3ExprEvaluator._check_var_initialized(var_name, var_value, expression)
+            return var_value
+
+        if isinstance(expression, Identifier):
+            var_name = expression.name
+            if var_name in CONSTANTS_MAP:
+                if not reqd_type or reqd_type == Qasm3FloatType:
+                    return CONSTANTS_MAP[var_name]
+                raise_qasm3_error(
+                    f"Constant {var_name} not allowed in non-float expression",
+                    Qasm3ConversionError,
+                    expression.span,
+                )
+            return _process_variable(var_name)
+
+        if isinstance(expression, IndexExpression):
+            var_name, indices = Qasm3Analyzer.analyze_index_expression(expression)
+            return _process_variable(var_name, indices)
+
+        if isinstance(expression, (BooleanLiteral, IntegerLiteral, FloatLiteral)):
+            if reqd_type:
+                if reqd_type == BoolType and isinstance(expression, BooleanLiteral):
+                    return expression.value
+                if reqd_type == Qasm3IntType and isinstance(expression, IntegerLiteral):
+                    return expression.value
+                if reqd_type == Qasm3FloatType and isinstance(expression, FloatLiteral):
+                    return expression.value
+                raise_qasm3_error(
+                    f"Invalid type {type(expression)} for required type {reqd_type}",
+                    Qasm3ConversionError,
+                    expression.span,
+                )
+            return expression.value
+
+        if isinstance(expression, UnaryExpression):
+            operand = cls.evaluate_expression(expression.expression, const_expr, reqd_type)
+            if expression.op.name == "~" and not isinstance(operand, int):
+                raise_qasm3_error(
+                    f"Unsupported expression type {type(operand)} in ~ operation",
+                    Qasm3ConversionError,
+                    expression.span,
+                )
+            return qasm3_expression_op_map(
+                "UMINUS" if expression.op.name == "-" else expression.op.name, operand
+            )
+        if isinstance(expression, BinaryExpression):
+            lhs = cls.evaluate_expression(expression.lhs, const_expr, reqd_type)
+            rhs = cls.evaluate_expression(expression.rhs, const_expr, reqd_type)
+            return qasm3_expression_op_map(expression.op.name, lhs, rhs)
+
+        if isinstance(expression, FunctionCall):
+            # function will not return a reqd / const type
+            # Reference : https://openqasm.com/language/types.html#compile-time-constants
+            # para      : 5
+            return cls.visitor_obj._visit_function_call(expression)
+
+        raise_qasm3_error(
+            f"Unsupported expression type {type(expression)}", Qasm3ConversionError, expression.span
+        )
diff --git a/qbraid_qir/qasm3/oq3_maps.py b/qbraid_qir/qasm3/maps.py
similarity index 100%
rename from qbraid_qir/qasm3/oq3_maps.py
rename to qbraid_qir/qasm3/maps.py
diff --git a/qbraid_qir/qasm3/transformer.py b/qbraid_qir/qasm3/transformer.py
new file mode 100644
index 0000000..e5ad65b
--- /dev/null
+++ b/qbraid_qir/qasm3/transformer.py
@@ -0,0 +1,210 @@
+# Copyright (C) 2024 qBraid
+#
+# This file is part of the qBraid-SDK
+#
+# The qBraid-SDK is free software released under the GNU General Public License v3
+# or later. You can redistribute and/or modify it under the terms of the GPL v3.
+# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
+#
+# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.
+
+"""
+Module with transformation functions for QASM3 visitor
+
+"""
+from typing import Any, Optional, Union
+
+from openqasm3.ast import (
+    BinaryExpression,
+    DiscreteSet,
+    FloatLiteral,
+    Identifier,
+    IndexedIdentifier,
+    IndexExpression,
+    IntegerLiteral,
+    QuantumBarrier,
+    QuantumGate,
+    QuantumReset,
+    RangeDefinition,
+    UnaryExpression,
+)
+
+from .exceptions import raise_qasm3_error
+from .expressions import Qasm3ExprEvaluator
+from .validator import Qasm3Validator
+
+
+class Qasm3Transformer:
+    """Class with utility functions for transforming QASM3 elements"""
+
+    visitor_obj = None
+
+    @classmethod
+    def set_visitor_obj(cls, visitor_obj):
+        cls.visitor_obj = visitor_obj
+
+    @staticmethod
+    def update_array_element(multi_dim_arr: list[Any], indices: list[int], value: Any) -> None:
+        """Update the value of an array at the specified indices.
+
+        Args:
+            multi_dim_arr (list): The multi-dimensional array to update.
+            indices (list[int]): The indices to update.
+            value (Any): The value to update.
+
+        Returns:
+            None
+        """
+        temp = multi_dim_arr
+        for index in indices[:-1]:
+            temp = temp[index]
+        temp[indices[-1]] = value
+
+    @staticmethod
+    def extract_values_from_discrete_set(discrete_set: DiscreteSet) -> list[int]:
+        """Extract the values from a discrete set.
+
+        Args:
+            discrete_set (DiscreteSet): The discrete set to extract values from.
+
+        Returns:
+            list[int]: The extracted values.
+        """
+        values = []
+        for value in discrete_set.values:
+            if not isinstance(value, IntegerLiteral):
+                raise_qasm3_error(
+                    f"Unsupported discrete set value {value} in discrete set",
+                    span=discrete_set.span,
+                )
+            values.append(value.value)
+        return values
+
+    @staticmethod
+    def get_qubits_from_range_definition(
+        range_def: RangeDefinition, qreg_size: int, is_qubit_reg: bool
+    ) -> list[int]:
+        """Get the qubits from a range definition.
+        Args:
+            range_def (RangeDefinition): The range definition to get qubits from.
+            qreg_size (int): The size of the register.
+            is_qubit_reg (bool): Whether the register is a qubit register.
+        Returns:
+            list[int]: The list of qubit identifiers.
+        """
+        start_qid = (
+            0
+            if range_def.start is None
+            else Qasm3ExprEvaluator.evaluate_expression(range_def.start)
+        )
+        end_qid = (
+            qreg_size
+            if range_def.end is None
+            else Qasm3ExprEvaluator.evaluate_expression(range_def.end)
+        )
+        step = (
+            1 if range_def.step is None else Qasm3ExprEvaluator.evaluate_expression(range_def.step)
+        )
+        Qasm3Validator.validate_register_index(start_qid, qreg_size, qubit=is_qubit_reg)
+        Qasm3Validator.validate_register_index(end_qid - 1, qreg_size, qubit=is_qubit_reg)
+        return list(range(start_qid, end_qid, step))
+
+    @staticmethod
+    def transform_gate_qubits(
+        gate_op: QuantumGate, qubit_map: dict[str, IndexedIdentifier]
+    ) -> None:
+        """Transform the qubits of a gate operation with a qubit map.
+
+        Args:
+            gate_op (QuantumGate): The gate operation to transform.
+            qubit_map (dict[str, IndexedIdentifier]): The qubit map to use for transformation.
+
+        Returns:
+            None
+        """
+        for i, qubit in enumerate(gate_op.qubits):
+            if isinstance(qubit, IndexedIdentifier):
+                raise_qasm3_error(
+                    f"Indexing '{qubit.name.name}' not supported in gate definition",
+                    span=qubit.span,
+                )
+
+            gate_op.qubits[i] = qubit_map[qubit.name]
+
+    @staticmethod
+    def transform_gate_params(
+        gate_op: QuantumGate, param_map: dict[str, Union[FloatLiteral, IntegerLiteral]]
+    ) -> None:
+        """Transform the parameters of a gate operation with a parameter map.
+
+        Args:
+            gate_op (QuantumGate): The gate operation to transform.
+            param_map (Dict[str, Union[FloatLiteral, IntegerLiteral]]): The parameter map to use
+                                                                        for transformation.
+
+        Returns:
+            None
+        """
+        for i, param in enumerate(gate_op.arguments):
+            if isinstance(param, Identifier):
+                gate_op.arguments[i] = param_map[param.name]
+            # TODO : update the arg value in expressions not just SINGLE identifiers
+
+    @staticmethod
+    def get_branch_params(condition: Any) -> tuple[Optional[int], Optional[str]]:
+        """
+        Get the branch parameters from the branching condition
+
+        Args:
+            condition (Union[UnaryExpression, BinaryExpression, IndexExpression]): The condition
+                                                                                   to analyze
+
+        Returns:
+            tuple[int, str]: The branch parameters
+        """
+        if isinstance(condition, UnaryExpression):
+            return (
+                condition.expression.index[0].value,
+                condition.expression.collection.name,
+            )
+        if isinstance(condition, BinaryExpression):
+            return condition.lhs.index[0].value, condition.lhs.collection.name
+        if isinstance(condition, IndexExpression):
+            return condition.index[0].value, condition.collection.name
+        return None, None
+
+    @classmethod
+    def transform_function_qubits(
+        cls,
+        q_op: Union[QuantumGate, QuantumBarrier, QuantumReset],
+        formal_qreg_sizes: dict[str:int],
+        qubit_map: dict[tuple:tuple],
+    ) -> list:
+        """Transform the qubits of a function call to the actual qubits.
+
+        Args:
+            visitor_obj: The visitor object.
+            gate_op: The quantum operation to transform.
+            formal_qreg_sizes (dict[str: int]): The formal qubit register sizes.
+            qubit_map (dict[tuple: tuple]): The mapping of formal qubits to actual qubits.
+
+        Returns:
+            None
+        """
+        expanded_op_qubits = cls.visitor_obj._get_op_qubits(q_op, formal_qreg_sizes, qir_form=False)
+
+        transformed_qubits = []
+        for qubit in expanded_op_qubits:
+            formal_qreg_name = qubit.name.name
+            formal_qreg_idx = qubit.indices[0][0].value
+
+            # replace the formal qubit with the actual qubit
+            actual_qreg_name, actual_qreg_idx = qubit_map[(formal_qreg_name, formal_qreg_idx)]
+            transformed_qubits.append(
+                IndexedIdentifier(
+                    Identifier(actual_qreg_name),
+                    [[IntegerLiteral(actual_qreg_idx)]],
+                )
+            )
+
+        return transformed_qubits
diff --git a/qbraid_qir/qasm3/validator.py b/qbraid_qir/qasm3/validator.py
new file mode 100644
index 0000000..b76d60b
--- /dev/null
+++ b/qbraid_qir/qasm3/validator.py
@@ -0,0 +1,304 @@
+# Copyright (C) 2024 qBraid
+#
+# This file is part of the qBraid-SDK
+#
+# The qBraid-SDK is free software released under the GNU General Public License v3
+# or later. You can redistribute and/or modify it under the terms of the GPL v3.
+# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
+#
+# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.
+
+"""
+Module with utility functions for QASM3 visitor
+
+"""
+from typing import Any, Optional
+
+from openqasm3.ast import ArrayType, ClassicalDeclaration
+from openqasm3.ast import IntType as Qasm3IntType
+from openqasm3.ast import (
+    QuantumGate,
+    QuantumGateDefinition,
+    ReturnStatement,
+    Statement,
+    SubroutineDefinition,
+)
+
+from .elements import Variable
+from .exceptions import Qasm3ConversionError, raise_qasm3_error
+from .maps import LIMITS_MAP, VARIABLE_TYPE_MAP, qasm_variable_type_cast
+
+
+class Qasm3Validator:
+    """Class with validation functions for QASM3 visitor"""
+
+    @staticmethod
+    def validate_register_index(index: Optional[int], size: int, qubit: bool = False) -> None:
+        """Validate the index for a register.
+
+        Args:
+            index (optional, int): The index to validate.
+            size (int): The size of the register.
+            qubit (bool): Whether the register is a qubit register.
+
+        Raises:
+            Qasm3ConversionError: If the index is out of range.
+        """
+        if index is None or 0 <= index < size:
+            return None
+
+        raise Qasm3ConversionError(
+            f"Index {index} out of range for register of size {size} in "
+            f"{'qubit' if qubit else 'clbit'}"
+        )
+
+    @staticmethod
+    def validate_statement_type(
+        blacklisted_stmts: set, statement: Statement, construct: str
+    ) -> None:
+        """Validate the type of a statement.
+
+        Args:
+            blacklisted_stmts (set): The set of blacklisted statements.
+            statement (Statement): The statement to validate.
+            construct (str): The construct the statement is in.
+
+        Raises:
+            Qasm3ConversionError: If the statement is not supported.
+        """
+        stmt_type = statement.__class__
+        if stmt_type in blacklisted_stmts:
+            if stmt_type != ClassicalDeclaration:
+                raise_qasm3_error(
+                    f"Unsupported statement {stmt_type} in {construct} block",
+                    span=statement.span,
+                )
+
+            if statement.type.__class__ == ArrayType:
+                raise_qasm3_error(
+                    f"Unsupported statement {stmt_type} with {statement.type.__class__}"
+                    f" in {construct} block",
+                    span=statement.span,
+                )
+
+    @staticmethod
+    def validate_variable_type(variable: Optional[Variable], reqd_type: Any) -> bool:
+        """Validate the type of a variable.
+
+        Args:
+            variable (Variable): The variable to validate.
+            reqd_type (Any): The required Qasm3 type of the variable.
+
+        Returns:
+            bool: True if the variable is of the required type, False otherwise.
+        """
+        if not reqd_type:
+            return True
+        if variable is None:
+            return False
+        return isinstance(variable.base_type, reqd_type)
+
+    @staticmethod
+    def validate_variable_assignment_value(variable: Variable, value) -> Any:
+        """Validate the assignment of a value to a variable.
+
+        Args:
+            variable (Variable): The variable to assign to.
+            value (Any): The value to assign.
+
+        Raises:
+            Qasm3ConversionError: If the value is not of the correct type.
+
+        Returns:
+            Any: The value casted to the correct type.
+        """
+        # check 1 - type match
+        qasm_type = variable.base_type.__class__
+        base_size = variable.base_size
+
+        try:
+            type_to_match = VARIABLE_TYPE_MAP[qasm_type]
+        except KeyError as err:
+            raise Qasm3ConversionError(
+                f"Invalid type {qasm_type} for variable {variable.name}"
+            ) from err
+
+        # For each type we will have a "castable" type set and its corresponding cast operation
+        type_casted_value = qasm_variable_type_cast(qasm_type, variable.name, base_size, value)
+
+        # check 2 - range match , if bits mentioned in base size
+        if type_to_match == int:
+            base_size = variable.base_size
+            left, right = 0, 0
+            if qasm_type == Qasm3IntType:
+                left, right = (
+                    -1 * (2 ** (base_size - 1)),
+                    2 ** (base_size - 1) - 1,
+                )
+            else:
+                # would be uint only so we correctly get this
+                left, right = 0, 2**base_size - 1
+            if type_casted_value < left or type_casted_value > right:
+                raise_qasm3_error(
+                    f"Value {value} out of limits for variable {variable.name} with "
+                    f"base size {base_size}",
+                )
+
+        elif type_to_match == float:
+            base_size = variable.base_size
+            left, right = 0, 0
+
+            if base_size == 32:
+                left, right = -(LIMITS_MAP["float_32"]), (LIMITS_MAP["float_32"])
+            else:
+                left, right = -(LIMITS_MAP["float_64"]), (LIMITS_MAP["float_64"])
+
+            if type_casted_value < left or type_casted_value > right:
+                raise_qasm3_error(
+                    f"Value {value} out of limits for variable {variable.name} with "
+                    f"base size {base_size}",
+                )
+        elif type_to_match == bool:
+            pass
+        else:
+            raise_qasm3_error(
+                f"Invalid type {type_to_match} for variable {variable.name}", TypeError
+            )
+
+        return type_casted_value
+
+    @staticmethod
+    def validate_array_assignment_values(
+        variable: Variable, dimensions: list[int], values: list
+    ) -> None:
+        """Validate the assignment of values to an array variable.
+
+        Args:
+            variable (Variable): The variable to assign to.
+            dimensions (list[int]): The dimensions of the array.
+            values (list[Any]): The values to assign.
+
+        Raises:
+            Qasm3ConversionError: If the values are not of the correct type.
+        """
+        # recursively check the array
+        if len(values) != dimensions[0]:
+            raise_qasm3_error(
+                f"Invalid dimensions for array assignment to variable {variable.name}. "
+                f"Expected {dimensions[0]} but got {len(values)}",
+            )
+        for i, value in enumerate(values):
+            if isinstance(value, list):
+                Qasm3Validator.validate_array_assignment_values(variable, dimensions[1:], value)
+            else:
+                if len(dimensions) != 1:
+                    raise_qasm3_error(
+                        f"Invalid dimensions for array assignment to variable {variable.name}. "
+                        f"Expected {len(dimensions)} but got 1",
+                    )
+                values[i] = Qasm3Validator.validate_variable_assignment_value(variable, value)
+
+    @staticmethod
+    def validate_gate_call(
+        operation: QuantumGate,
+        gate_definition: QuantumGateDefinition,
+        qubits_in_op,
+    ) -> None:
+        """Validate the call of a gate operation.
+
+        Args:
+            operation (QuantumGate): The gate operation to validate.
+            gate_definition (QuantumGateDefinition): The gate definition to validate against.
+            qubits_in_op (int): The number of qubits in the operation.
+
+        Raises:
+            Qasm3ConversionError: If the number of parameters or qubits is invalid.
+        """
+        op_num_args = len(operation.arguments)
+        gate_def_num_args = len(gate_definition.arguments)
+        if op_num_args != gate_def_num_args:
+            s = "" if gate_def_num_args == 1 else "s"
+            raise_qasm3_error(
+                f"Parameter count mismatch for gate {operation.name.name}: "
+                f"expected {gate_def_num_args} argument{s}, but got {op_num_args} instead.",
+                span=operation.span,
+            )
+
+        gate_def_num_qubits = len(gate_definition.qubits)
+        if qubits_in_op != gate_def_num_qubits:
+            s = "" if gate_def_num_qubits == 1 else "s"
+            raise_qasm3_error(
+                f"Qubit count mismatch for gate {operation.name.name}: "
+                f"expected {gate_def_num_qubits} qubit{s}, but got {qubits_in_op} instead.",
+                span=operation.span,
+            )
+
+    @staticmethod
+    def validate_return_statement(  # pylint: disable=inconsistent-return-statements
+        subroutine_def: SubroutineDefinition,
+        return_statement: ReturnStatement,
+        return_value: Any,
+    ):
+        """Validate the return type of a function.
+
+        Args:
+            subroutine_def (SubroutineDefinition): The subroutine definition.
+            return_statement (ReturnStatement): The return statement.
+            return_value (Any): The return value.
+
+        Raises:
+            Qasm3ConversionError: If the return type is invalid.
+
+        Returns:
+            Any: The return value casted to the correct type
+        """
+
+        if subroutine_def.return_type is None:
+            if return_value is not None:
+                raise_qasm3_error(
+                    f"Return type mismatch for subroutine '{subroutine_def.name.name}'."
+                    f" Expected void but got {type(return_value)}",
+                    span=return_statement.span,
+                )
+        else:
+            if return_value is None:
+                raise_qasm3_error(
+                    f"Return type mismatch for subroutine '{subroutine_def.name.name}'."
+                    f" Expected {subroutine_def.return_type} but got void",
+                    span=return_statement.span,
+                )
+            base_size = 1
+            if hasattr(subroutine_def.return_type, "size"):
+                base_size = subroutine_def.return_type.size.value
+
+            return Qasm3Validator.validate_variable_assignment_value(
+                Variable(
+                    subroutine_def.name.name + "_return",
+                    subroutine_def.return_type,
+                    base_size,
+                    None,
+                    None,
+                ),
+                return_value,
+            )
+
+    @staticmethod
+    def validate_unique_qubits(qubit_map: dict, reg_name: str, indices: list) -> bool:
+        """
+        Validates that the qubits in the given register are unique.
+
+        Args:
+            qubit_map (dict): Dictionary of qubits.
+            reg_name (str): The name of the register.
+            indices (list): A list of indices representing the qubits.
+
+        Returns:
+            bool: True if the qubits are unique, False otherwise.
+        """
+        if reg_name not in qubit_map:
+            qubit_map[reg_name] = set(indices)
+        else:
+            for idx in indices:
+                if idx in qubit_map[reg_name]:
+                    return False
+        return True
diff --git a/qbraid_qir/qasm3/visitor.py b/qbraid_qir/qasm3/visitor.py
index 5c6abac..03e94ae 100644
--- a/qbraid_qir/qasm3/visitor.py
+++ b/qbraid_qir/qasm3/visitor.py
@@ -14,83 +14,32 @@
 """
 import copy
 import logging
-import sys
 from abc import ABCMeta, abstractmethod
 
 # pylint: disable=too-many-instance-attributes,too-many-lines
 from collections import deque
-from typing import Optional, Union
+from typing import Union
 
+import openqasm3.ast as qasm3_ast
 import pyqir
 import pyqir._native
 import pyqir.rt
-from openqasm3.ast import (
-    AliasStatement,
-    ArrayLiteral,
-    ArrayType,
-    BinaryExpression,
-    BooleanLiteral,
-    BoolType,
-    BranchingStatement,
-    ClassicalArgument,
-    ClassicalAssignment,
-    ClassicalDeclaration,
-    ConstantDeclaration,
-    DiscreteSet,
-    DurationLiteral,
-    ExpressionStatement,
-    FloatLiteral,
-)
-from openqasm3.ast import FloatType as Qasm3FloatType
-from openqasm3.ast import (
-    ForInLoop,
-    FunctionCall,
-    GateModifierName,
-    Identifier,
-    ImaginaryLiteral,
-    Include,
-    IndexedIdentifier,
-    IndexExpression,
-    IntegerLiteral,
-)
-from openqasm3.ast import IntType as Qasm3IntType
-from openqasm3.ast import (
-    IODeclaration,
-    QuantumBarrier,
-    QuantumGate,
-    QuantumGateDefinition,
-    QuantumGateModifier,
-    QuantumMeasurementStatement,
-    QuantumReset,
-    QubitDeclaration,
-    RangeDefinition,
-    ReturnStatement,
-    Span,
-    Statement,
-    SubroutineDefinition,
-    SwitchStatement,
-    UnaryExpression,
-    WhileLoop,
-)
-from pyqir import BasicBlock, Builder, Constant
-from pyqir import IntType as qirIntType
-from pyqir import PointerType
 
+from .analyzer import Qasm3Analyzer
 from .elements import Context, InversionOp, Qasm3Module, Variable
-from .exceptions import Qasm3ConversionError
-from .oq3_maps import (
+from .exceptions import Qasm3ConversionError, raise_qasm3_error
+from .expressions import Qasm3ExprEvaluator
+from .maps import (
     CONSTANTS_MAP,
-    LIMITS_MAP,
     MAX_ARRAY_DIMENSIONS,
     SWITCH_BLACKLIST_STMTS,
-    VARIABLE_TYPE_MAP,
     map_qasm_inv_op_to_pyqir_callable,
     map_qasm_op_to_pyqir_callable,
-    qasm3_expression_op_map,
-    qasm_variable_type_cast,
 )
+from .transformer import Qasm3Transformer
+from .validator import Qasm3Validator
 
-_log = logging.getLogger(name=__name__)
+logger = logging.getLogger(__name__)
 
 
 class ProgramElementVisitor(metaclass=ABCMeta):
@@ -133,6 +82,8 @@ def __init__(self, initialize_runtime: bool = True, record_output: bool = True):
         self._curr_scope = 0
         self._label_scope_level = {self._curr_scope: set()}
 
+        self._init_utilities()
+
     def visit_qasm3_module(self, module: Qasm3Module) -> None:
         """
         Visit a Qasm3 module.
@@ -143,20 +94,25 @@ def visit_qasm3_module(self, module: Qasm3Module) -> None:
         Returns:
             None
         """
-        _log.debug("Visiting Qasm3 module '%s' (%d)", module.name, module.num_qubits)
+        logger.debug("Visiting Qasm3 module '%s' (%d)", module.name, module.num_qubits)
         self._module = module.module
         context = self._module.context
         entry = pyqir.entry_point(self._module, module.name, module.num_qubits, module.num_clbits)
 
         self._entry_point = entry.name
-        self._builder = Builder(context)
-        self._builder.insert_at_end(BasicBlock(context, "entry", entry))
+        self._builder = pyqir.Builder(context)
+        self._builder.insert_at_end(pyqir.BasicBlock(context, "entry", entry))
 
         if self._initialize_runtime is True:
-            i8p = PointerType(qirIntType(context, 8))
-            nullptr = Constant.null(i8p)
+            i8p = pyqir.PointerType(pyqir.IntType(context, 8))
+            nullptr = pyqir.Constant.null(i8p)
             pyqir.rt.initialize(self._builder, nullptr)
 
+    def _init_utilities(self):
+        """Initialize the utilities for the visitor."""
+        for class_obj in [Qasm3Transformer, Qasm3ExprEvaluator]:
+            class_obj.set_visitor_obj(self)
+
     @property
     def entry_point(self) -> str:
         return self._entry_point
@@ -204,7 +160,7 @@ def _get_global_scope(self) -> dict:
             raise IndexError("No scopes available to get")
         return self._scope[0]
 
-    def _check_in_scope(self, var_name: str, curr_scope) -> bool:
+    def _check_in_scope(self, var_name: str) -> bool:
         """
         Checks if a variable is in scope.
 
@@ -225,12 +181,12 @@ def _check_in_scope(self, var_name: str, curr_scope) -> bool:
           OR the GLOBAL context
         - Why then do we need a new scope for a block?
         - Well, if the block redeclares a variable in its scope, then the
-          variable in the parent scope is shadowed
-        - We need to remember the original value of the shadowed variable when we exit
-          the block scope
+          variable in the parent scope is shadowed. We need to remember the
+          original value of the shadowed variable when we exit the block scope
 
         """
         global_scope = self._get_global_scope()
+        curr_scope = self._get_curr_scope()
         if self._in_global_scope():
             return var_name in global_scope
         if self._in_function_scope() or self._in_gate_scope():
@@ -289,24 +245,6 @@ def _add_var_in_scope(self, variable: Variable) -> None:
             raise ValueError(f"Variable '{variable.name}' already exists in current scope")
         curr_scope[variable.name] = variable
 
-    def _delete_var_from_scope(self, var_name: str) -> None:
-        """
-        Deletes a variable from the current scope.
-
-        Args:
-            var_name (str): The name of the variable to be deleted.
-
-        Raises:
-            ValueError: If the variable is not found in the current scope.
-
-        Returns:
-            None
-        """
-        curr_scope = self._get_curr_scope()
-        if var_name not in curr_scope:
-            raise ValueError(f"Variable '{var_name}' not found in current scope")
-        del curr_scope[var_name]
-
     def _update_var_in_scope(self, variable: Variable) -> None:
         """
         Updates the variable in the current scope.
@@ -353,23 +291,25 @@ def record_output(self, module: Qasm3Module) -> None:
         if self._record_output is False:
             return
 
-        i8p = PointerType(qirIntType(self._module.context, 8))
+        i8p = pyqir.PointerType(pyqir.IntType(self._module.context, 8))
 
         for i in range(module.num_qubits):
             result_ref = pyqir.result(self._module.context, i)
-            pyqir.rt.result_record_output(self._builder, result_ref, Constant.null(i8p))
+            pyqir.rt.result_record_output(self._builder, result_ref, pyqir.Constant.null(i8p))
 
-    def visit_register(self, register: Union[QubitDeclaration, ClassicalDeclaration]) -> None:
+    def visit_register(
+        self, register: Union[qasm3_ast.QubitDeclaration, qasm3_ast.ClassicalDeclaration]
+    ) -> None:
         """Visit a register element.
 
         Args:
-            register (QubitDeclaration | ClassicalDeclaration): The register name and size.
+            register (QubitDeclaration|ClassicalDeclaration): The register name and size.
 
         Returns:
             None
         """
-        _log.debug("Visiting register '%s'", str(register))
-        is_qubit = isinstance(register, QubitDeclaration)
+        logger.debug("Visiting register '%s'", str(register))
+        is_qubit = isinstance(register, qasm3_ast.QubitDeclaration)
 
         current_size = len(self._qubit_labels) if is_qubit else len(self._clbit_labels)
         if is_qubit:
@@ -381,17 +321,16 @@ def visit_register(self, register: Union[QubitDeclaration, ClassicalDeclaration]
         size_map = self._global_qreg_size_map if is_qubit else self._global_creg_size_map
         label_map = self._qubit_labels if is_qubit else self._clbit_labels
 
-        if self._check_in_scope(register_name, self._get_curr_scope()):
-            self._print_err_location(register.span)
-            raise Qasm3ConversionError(
-                f"Invalid declaration of register with name '{register_name}'"
+        if self._check_in_scope(register_name):
+            raise_qasm3_error(
+                f"Invalid declaration of register with name '{register_name}'", span=register.span
             )
 
         if is_qubit:  # as bit type vars are added in classical decl handler
             self._add_var_in_scope(
                 Variable(
                     register_name,
-                    QubitDeclaration,
+                    qasm3_ast.QubitDeclaration,
                     register_size,
                     None,
                     None,
@@ -406,118 +345,36 @@ def visit_register(self, register: Union[QubitDeclaration, ClassicalDeclaration]
 
         self._label_scope_level[self._curr_scope].add(register_name)
 
-        _log.debug("Added labels for register '%s'", str(register))
-
-    def _print_err_location(self, element: Span) -> str:
-        print(
-            f"Error at line {element.start_line}, column {element.start_column} in QASM file",
-            file=sys.stderr,
-        )
-
-    def _validate_register_index(
-        self, index: Optional[int], size: int, qubit: bool = False
-    ) -> None:
-        """Validate the index for a register.
-
-        Args:
-            index (optional, int): The index to validate.
-            size (int): The size of the register.
-            qubit (bool): Whether the register is a qubit register.
-
-        Raises:
-            Qasm3ConversionError: If the index is out of range.
-        """
-        # nothing to validate if index is None
-        if index is None:
-            return
-
-        if not 0 <= index < size:
-            raise Qasm3ConversionError(
-                f"Index {index} out of range for register of size {size} in "
-                f"{'qubit' if qubit else 'clbit'}"
-            )
-
-    def _validate_variable_type(self, var_name: str, reqd_type):
-        """Validate the type of a variable.
-
-        Args:
-            variable (Variable): The variable to validate.
-            reqd_type (any): The required Qasm3 type of the variable.
-        """
-        if not reqd_type:
-            return True
-        variable = self._get_from_visible_scope(var_name)
-        if not variable:
-            return False
-        return isinstance(variable.base_type, reqd_type)
-
-    def _validate_statement_type(
-        self, blacklisted_stmts: set, statement: Statement, construct: str
-    ):
-        """Validate the type of a statement.
-
-        Args:
-            blacklisted_stmts (set): The set of blacklisted statements.
-            statement (Statement): The statement to validate.
-            construct (str): The construct the statement is in.
+        logger.debug("Added labels for register '%s'", str(register))
 
-        Raises:
-            Qasm3ConversionError: If the statement is not supported.
-        """
-        stmt_type = statement.__class__
-        if stmt_type in blacklisted_stmts:
-            if stmt_type == ClassicalDeclaration:
-                if statement.type.__class__ == ArrayType:
-                    self._print_err_location(statement.span)
-                    raise Qasm3ConversionError(
-                        f"Unsupported statement {stmt_type} with {statement.type.__class__}"
-                        " in {construct} block"
-                    )
-            else:
-                self._print_err_location(statement.span)
-                raise Qasm3ConversionError(
-                    f"Unsupported statement {stmt_type} in {construct} block"
-                )
-
-    def _get_qubits_from_range_definition(
-        self, range_def: RangeDefinition, qreg_size: int, is_qubit_reg: bool
-    ) -> list[int]:
-        """Get the qubits from a range definition.
-        Args:
-            range_def (RangeDefinition): The range definition to get qubits from.
-            qreg_size (int): The size of the register.
-            is_qubit_reg (bool): Whether the register is a qubit register.
-        Returns:
-            list[int]: The list of qubit identifiers.
-        """
-        start_qid = 0 if range_def.start is None else self._evaluate_expression(range_def.start)
-        end_qid = qreg_size if range_def.end is None else self._evaluate_expression(range_def.end)
-        step = 1 if range_def.step is None else self._evaluate_expression(range_def.step)
-        self._validate_register_index(start_qid, qreg_size, qubit=is_qubit_reg)
-        self._validate_register_index(end_qid - 1, qreg_size, qubit=is_qubit_reg)
-        return list(range(start_qid, end_qid, step))
-
-    def _check_if_name_in_scope(self, name: str, operation) -> None:
+    def _check_if_name_in_scope(self, name: str, operation: any) -> None:
         """Check if a name is in scope to avoid duplicate declarations.
         Args:
             name (str): The name to check.
+            operation (Any): The operation to check the name in scope for.
+
         Returns:
             bool: Whether the name is in scope.
         """
         for scope_level in range(0, self._curr_scope + 1):
             if name in self._label_scope_level[scope_level]:
-                return None
-        self._print_err_location(operation.span)
-        raise Qasm3ConversionError(f"Variable {name} not in scope for operation {operation}")
+                return
+        raise_qasm3_error(
+            f"Variable {name} not in scope for operation {operation}", span=operation.span
+        )
 
-    def _get_op_qubits(self, operation, qreg_size_map, qir_form: bool = True) -> list[pyqir.qubit]:
+    def _get_op_qubits(
+        self, operation: any, qreg_size_map: dict, qir_form: bool = True
+    ) -> list[Union[pyqir.qubit, qasm3_ast.IndexedIdentifier]]:
         """Get the qubits for the operation.
 
         Args:
             operation (Any): The operation to get qubits for.
+            qreg_size_map (dict): The size map of the registers in scope.
+            qir_form (bool): Whether to return qubits in QIR form or not. Defaults to True.
 
         Returns:
-            list[pyqir.qubit]: The qubits for the operation.
+            list[Union[pyqir.qubit, qasm3_ast.IndexedIdentifier]]: The qubits for the operation.
         """
         qir_qubits = []
         openqasm_qubits = []
@@ -525,93 +382,104 @@ def _get_op_qubits(self, operation, qreg_size_map, qir_form: bool = True) -> lis
         qubit_list = operation.qubits if isinstance(operation.qubits, list) else [operation.qubits]
 
         for qubit in qubit_list:
-            if isinstance(qubit, IndexedIdentifier):
+            if isinstance(qubit, qasm3_ast.IndexedIdentifier):
                 qreg_name = qubit.name.name
             else:
                 qreg_name = qubit.name
 
             if qreg_name not in qreg_size_map:
-                self._print_err_location(operation.span)
-                raise Qasm3ConversionError(
-                    f"Missing register declaration for {qreg_name} in operation {operation}"
+                raise_qasm3_error(
+                    f"Missing register declaration for {qreg_name} in operation {operation}",
+                    span=operation.span,
                 )
             self._check_if_name_in_scope(qreg_name, operation)
             qreg_size = qreg_size_map[qreg_name]
 
-            if isinstance(qubit, IndexedIdentifier):
-                if isinstance(qubit.indices[0][0], RangeDefinition):
-                    qids = self._get_qubits_from_range_definition(
+            if isinstance(qubit, qasm3_ast.IndexedIdentifier):
+                if isinstance(qubit.indices[0][0], qasm3_ast.RangeDefinition):
+                    qids = Qasm3Transformer.get_qubits_from_range_definition(
                         qubit.indices[0][0], qreg_size, is_qubit_reg=True
                     )
                 else:
-                    qid = self._evaluate_expression(qubit.indices[0][0])
-                    self._validate_register_index(qid, qreg_size, qubit=True)
+                    qid = Qasm3ExprEvaluator.evaluate_expression(qubit.indices[0][0])
+                    Qasm3Validator.validate_register_index(qid, qreg_size, qubit=True)
                     qids = [qid]
                 openqasm_qubits.extend(
-                    [IndexedIdentifier(Identifier(qreg_name), [[IntegerLiteral(i)]]) for i in qids]
+                    [
+                        qasm3_ast.IndexedIdentifier(
+                            qasm3_ast.Identifier(qreg_name), [[qasm3_ast.IntegerLiteral(i)]]
+                        )
+                        for i in qids
+                    ]
                 )
             else:
                 qids = list(range(qreg_size))
                 openqasm_qubits.extend(
-                    [IndexedIdentifier(Identifier(qreg_name), [[IntegerLiteral(i)]]) for i in qids]
+                    [
+                        qasm3_ast.IndexedIdentifier(
+                            qasm3_ast.Identifier(qreg_name), [[qasm3_ast.IntegerLiteral(i)]]
+                        )
+                        for i in qids
+                    ]
                 )
 
             if qir_form:
                 qreg_qids = [self._qubit_labels[f"{qreg_name}_{i}"] for i in qids]
                 for qid in qreg_qids:
                     if qid in visited_qubits:
-                        self._print_err_location(operation.span)
-                        raise Qasm3ConversionError(f"Duplicate qubit {qreg_name}[{qid}] argument")
+                        raise_qasm3_error(
+                            f"Duplicate qubit {qreg_name}[{qid}] argument", span=operation.span
+                        )
                     visited_qubits.add(qid)
                 qir_qubits.extend([pyqir.qubit(self._module.context, n) for n in qreg_qids])
 
         return qir_qubits if qir_form else openqasm_qubits
 
-    def _visit_measurement(self, statement: QuantumMeasurementStatement) -> None:
+    def _visit_measurement(self, statement: qasm3_ast.QuantumMeasurementStatement) -> None:
         """Visit a measurement statement element.
 
         Args:
-            statement (QuantumMeasurementStatement): The measurement statement to visit.
+            statement (qasm3_ast.QuantumMeasurementStatement): The measurement statement to visit.
 
         Returns:
             None
         """
-        _log.debug("Visiting measurement statement '%s'", str(statement))
+        logger.debug("Visiting measurement statement '%s'", str(statement))
         source = statement.measure.qubit
         target = statement.target
         source_id, target_id = None, None
         # TODO: handle in-function measurements
         source_name = source.name
-        if isinstance(source, IndexedIdentifier):
+        if isinstance(source, qasm3_ast.IndexedIdentifier):
             source_name = source.name.name
-            if isinstance(source.indices[0][0], RangeDefinition):
-                self._print_err_location(statement.span)
-                raise Qasm3ConversionError(
-                    f"Range based measurement {statement} not supported at the moment"
+            if isinstance(source.indices[0][0], qasm3_ast.RangeDefinition):
+                raise_qasm3_error(
+                    f"Range based measurement {statement} not supported at the moment",
+                    span=statement.span,
                 )
             source_id = source.indices[0][0].value
 
         target_name = target.name
-        if isinstance(target, IndexedIdentifier):
+        if isinstance(target, qasm3_ast.IndexedIdentifier):
             target_name = target.name.name
-            if isinstance(target.indices[0][0], RangeDefinition):
-                self._print_err_location(statement.span)
-                raise Qasm3ConversionError(
-                    f"Range based measurement {statement} not supported at the moment"
+            if isinstance(target.indices[0][0], qasm3_ast.RangeDefinition):
+                raise_qasm3_error(
+                    f"Range based measurement {statement} not supported at the moment",
+                    span=statement.span,
                 )
             target_id = target.indices[0][0].value
 
         if source_name not in self._global_qreg_size_map:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(
+            raise_qasm3_error(
                 f"Missing register declaration for {source_name} in measurement "
-                f"operation {statement}"
+                f"operation {statement}",
+                span=statement.span,
             )
         if target_name not in self._global_creg_size_map:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(
+            raise_qasm3_error(
                 f"Missing register declaration for {target_name} in measurement "
-                f"operation {statement}"
+                f"operation {statement}",
+                span=statement.span,
             )
 
         def _build_qir_measurement(
@@ -634,47 +502,49 @@ def _build_qir_measurement(
 
         if source_id is None and target_id is None:
             if self._global_qreg_size_map[source_name] != self._global_creg_size_map[target_name]:
-                self._print_err_location(statement.span)
-                raise Qasm3ConversionError(
+                raise_qasm3_error(
                     f"Register sizes of {source_name} and {target_name} do not match "
-                    "for measurement operation"
+                    "for measurement operation",
+                    span=statement.span,
                 )
             for i in range(self._global_qreg_size_map[source_name]):
                 _build_qir_measurement(source_name, i, target_name, i)
         else:
-            self._validate_register_index(
+            Qasm3Validator.validate_register_index(
                 source_id, self._global_qreg_size_map[source_name], qubit=True
             )
-            self._validate_register_index(
+            Qasm3Validator.validate_register_index(
                 target_id, self._global_creg_size_map[target_name], qubit=False
             )
             _build_qir_measurement(source_name, source_id, target_name, target_id)
 
-    def _visit_reset(self, statement: QuantumReset) -> None:
+    def _visit_reset(self, statement: qasm3_ast.QuantumReset) -> None:
         """Visit a reset statement element.
 
         Args:
-            statement (QuantumReset): The reset statement to visit.
+            statement (qasm3_ast.QuantumReset): The reset statement to visit.
 
         Returns:
             None
         """
-        _log.debug("Visiting reset statement '%s'", str(statement))
+        logger.debug("Visiting reset statement '%s'", str(statement))
         if len(self._function_qreg_size_map) > 0:  # atleast in SOME function scope
             # transform qubits to use the global qreg identifiers
-            statement.qubits = self._transform_function_qubits(
-                statement, self._function_qreg_size_map[-1], self._function_qreg_transform_map[-1]
+            statement.qubits = Qasm3Transformer.transform_function_qubits(
+                statement,
+                self._function_qreg_size_map[-1],
+                self._function_qreg_transform_map[-1],
             )
         qubit_ids = self._get_op_qubits(statement, self._global_qreg_size_map, True)
 
         for qid in qubit_ids:
             pyqir._native.reset(self._builder, qid)
 
-    def _visit_barrier(self, barrier: QuantumBarrier) -> None:
+    def _visit_barrier(self, barrier: qasm3_ast.QuantumBarrier) -> None:
         """Visit a barrier statement element.
 
         Args:
-            statement (QuantumBarrier): The barrier statement to visit.
+            statement (qasm3_ast.QuantumBarrier): The barrier statement to visit.
 
         Returns:
             None
@@ -682,8 +552,10 @@ def _visit_barrier(self, barrier: QuantumBarrier) -> None:
         # if barrier is applied to ALL qubits at once, we are fine
         if len(self._function_qreg_size_map) > 0:  # atleast in SOME function scope
             # transform qubits to use the global qreg identifiers
-            barrier.qubits = self._transform_function_qubits(
-                barrier, self._function_qreg_size_map[-1], self._function_qreg_transform_map[-1]
+            barrier.qubits = Qasm3Transformer.transform_function_qubits(
+                barrier,
+                self._function_qreg_size_map[-1],
+                self._function_qreg_transform_map[-1],
             )
 
         barrier_qubits = self._get_op_qubits(barrier, self._global_qreg_size_map)
@@ -691,50 +563,49 @@ def _visit_barrier(self, barrier: QuantumBarrier) -> None:
         if len(barrier_qubits) == total_qubit_count:
             pyqir._native.barrier(self._builder)
         else:
-            self._print_err_location(barrier.span)
-            raise NotImplementedError(
-                "Barrier operation on a qubit subset is not supported in pyqir"
+            raise_qasm3_error(
+                "Barrier operation on a qubit subset is not supported in pyqir",
+                err_type=NotImplementedError,
+                span=barrier.span,
             )
 
-    def _is_parametric_gate(self, operation: QuantumGate) -> bool:
-        return len(operation.arguments) > 0
-
-    def _get_op_parameters(self, operation: QuantumGate) -> list[float]:
+    def _get_op_parameters(self, operation: qasm3_ast.QuantumGate) -> list[float]:
         """Get the parameters for the operation.
 
         Args:
-            operation (QuantumGate): The operation to get parameters for.
+            operation (qasm3_ast.QuantumGate): The operation to get parameters for.
 
         Returns:
             list[float]: The parameters for the operation.
         """
         param_list = []
         for param in operation.arguments:
-            param_value = self._evaluate_expression(param)
+            param_value = Qasm3ExprEvaluator.evaluate_expression(param)
             param_list.append(param_value)
 
         return param_list
 
-    def _visit_gate_definition(self, definition: QuantumGateDefinition) -> None:
+    def _visit_gate_definition(self, definition: qasm3_ast.QuantumGateDefinition) -> None:
         """Visit a gate definition element.
 
         Args:
-            definition (QuantumGateDefinition): The gate definition to visit.
+            definition (qasm3_ast.QuantumGateDefinition): The gate definition to visit.
 
         Returns:
             None
         """
         gate_name = definition.name.name
         if gate_name in self._custom_gates:
-            self._print_err_location(definition.span)
-            raise Qasm3ConversionError(f"Duplicate gate definition for {gate_name}")
+            raise_qasm3_error(f"Duplicate gate definition for {gate_name}", span=definition.span)
         self._custom_gates[gate_name] = definition
 
-    def _visit_basic_gate_operation(self, operation: QuantumGate, inverse: bool = False) -> None:
+    def _visit_basic_gate_operation(
+        self, operation: qasm3_ast.QuantumGate, inverse: bool = False
+    ) -> None:
         """Visit a gate operation element.
 
         Args:
-            operation (QuantumGate): The gate operation to visit.
+            operation (qasm3_ast.QuantumGate): The gate operation to visit.
             inverse (bool): Whether the operation is an inverse operation. Defaults to False.
 
                           - if inverse is True, we apply check for different cases in the
@@ -754,7 +625,7 @@ def _visit_basic_gate_operation(self, operation: QuantumGate, inverse: bool = Fa
 
         """
 
-        _log.debug("Visiting basic gate operation '%s'", str(operation))
+        logger.debug("Visiting basic gate operation '%s'", str(operation))
         op_name: str = operation.name.name
         op_qubits = self._get_op_qubits(operation, self._global_qreg_size_map)
         inverse_action = None
@@ -767,12 +638,12 @@ def _visit_basic_gate_operation(self, operation: QuantumGate, inverse: bool = Fa
         op_parameters = None
 
         if len(op_qubits) % op_qubit_count != 0:
-            self._print_err_location(operation.span)
-            raise Qasm3ConversionError(
-                f"Invalid number of qubits {len(op_qubits)} for operation {operation.name.name}"
+            raise_qasm3_error(
+                f"Invalid number of qubits {len(op_qubits)} for operation {operation.name.name}",
+                span=operation.span,
             )
 
-        if self._is_parametric_gate(operation):
+        if len(operation.arguments) > 0:  # parametric gate
             op_parameters = self._get_op_parameters(operation)
             if inverse_action == InversionOp.INVERT_ROTATION:
                 op_parameters = [-1 * param for param in op_parameters]
@@ -785,75 +656,13 @@ def _visit_basic_gate_operation(self, operation: QuantumGate, inverse: bool = Fa
             else:
                 qir_func(self._builder, *qubit_subset)
 
-    def _transform_gate_qubits(self, gate_op: QuantumGate, qubit_map: dict) -> None:
-        """Transform the qubits of a gate operation with a qubit map.
-
-        Args:
-            gate_op (QuantumGate): The gate operation to transform.
-            qubit_map (Dict[str, IndexedIdentifier]): The qubit map to use for transformation.
-
-        Returns:
-            None
-        """
-        for i, qubit in enumerate(gate_op.qubits):
-            if isinstance(qubit, IndexedIdentifier):
-                self._print_err_location(qubit.span)
-                raise Qasm3ConversionError(
-                    f"Indexing '{qubit.name.name}' not supported in gate definition"
-                )
-            gate_op.qubits[i] = qubit_map[qubit.name]
-
-    def _transform_gate_params(self, gate_op: QuantumGate, param_map: dict) -> None:
-        """Transform the parameters of a gate operation with a parameter map.
-
-        Args:
-            gate_op (QuantumGate): The gate operation to transform.
-            param_map (Dict[str, Union[FloatLiteral, IntegerLiteral]]): The parameter map to use
-                                                                        for transformation.
-
-        Returns:
-            None
-        """
-        for i, param in enumerate(gate_op.arguments):
-            if isinstance(param, Identifier):
-                gate_op.arguments[i] = param_map[param.name]
-            # TODO : update the arg value in expressions not just SINGLE identifiers
-
-    def _validate_gate_call(
-        self,
-        operation: QuantumGate,
-        gate_definition: QuantumGateDefinition,
-        qubits_in_op,
+    def _visit_custom_gate_operation(
+        self, operation: qasm3_ast.QuantumGate, inverse: bool = False
     ) -> None:
-        """Validate the call of a gate operation.
-
-        Args:
-            operation (QuantumGate): The gate operation to validate.
-            gate_definition (QuantumGateDefinition): The gate definition to validate against.
-            qubits_in_op (int): The number of qubits in the operation.
-
-        Raises:
-            Qasm3ConversionError: If the number of parameters or qubits is invalid.
-        """
-        if len(operation.arguments) != len(gate_definition.arguments):
-            self._print_err_location(operation.span)
-            raise Qasm3ConversionError(
-                f"""Parameter count mismatch for gate {operation.name.name}. Expected \
-{len(gate_definition.arguments)} but got {len(operation.arguments)} in operation"""
-            )
-
-        if qubits_in_op != len(gate_definition.qubits):
-            self._print_err_location(operation.span)
-            raise Qasm3ConversionError(
-                f"""Qubit count mismatch for gate {operation.name.name}. Expected \
-{len(gate_definition.qubits)} but got {qubits_in_op} in operation"""
-            )
-
-    def _visit_custom_gate_operation(self, operation: QuantumGate, inverse: bool = False) -> None:
         """Visit a custom gate operation element recursively.
 
         Args:
-            operation (QuantumGate): The gate operation to visit.
+            operation (qasm3_ast.QuantumGate): The gate operation to visit.
             inverse (bool): Whether the operation is an inverse operation. Defaults to False.
 
                             If True, the gate operation is applied in reverse order and the
@@ -864,12 +673,12 @@ def _visit_custom_gate_operation(self, operation: QuantumGate, inverse: bool = F
         Returns:
             None
         """
-        _log.debug("Visiting custom gate operation '%s'", str(operation))
+        logger.debug("Visiting custom gate operation '%s'", str(operation))
         gate_name: str = operation.name.name
-        gate_definition: QuantumGateDefinition = self._custom_gates[gate_name]
+        gate_definition: qasm3_ast.QuantumGateDefinition = self._custom_gates[gate_name]
         op_qubits = self._get_op_qubits(operation, self._global_qreg_size_map, qir_form=False)
 
-        self._validate_gate_call(operation, gate_definition, len(op_qubits))
+        Qasm3Validator.validate_gate_call(operation, gate_definition, len(op_qubits))
         # we need this because the gates applied inside a gate definition use the
         # VARIABLE names and not the qubits
 
@@ -892,37 +701,39 @@ def _visit_custom_gate_operation(self, operation: QuantumGate, inverse: bool = F
 
         for gate_op in gate_definition_ops:
             if gate_op.name.name == gate_name:
-                self._print_err_location(gate_op.span)
-                raise Qasm3ConversionError(
-                    f"Recursive definitions not allowed for gate {gate_name}"
+                raise_qasm3_error(
+                    f"Recursive definitions not allowed for gate {gate_name}", span=gate_op.span
                 )
 
             # necessary to avoid modifying the original gate definition
             # in case the gate is reapplied
             gate_op_copy = copy.deepcopy(gate_op)
-            if isinstance(gate_op, QuantumGate):
-                self._transform_gate_params(gate_op_copy, param_map)
-                self._transform_gate_qubits(gate_op_copy, qubit_map)
-                # need to trickle the inverse down to the child!
+            if isinstance(gate_op, qasm3_ast.QuantumGate):
+                Qasm3Transformer.transform_gate_params(gate_op_copy, param_map)
+                Qasm3Transformer.transform_gate_qubits(gate_op_copy, qubit_map)
+                # need to trickle the inverse down to the child gates
                 if inverse:
-                    # span doesn't matter as we don't analyse it
-                    gate_op_copy.modifiers.append(QuantumGateModifier(GateModifierName.inv, None))
+                    # span doesn't matter as we don't analyze it
+                    gate_op_copy.modifiers.append(
+                        qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None)
+                    )
                 self._visit_generic_gate_operation(gate_op_copy)
             else:
                 # TODO: add control flow support
-                self._print_err_location(gate_op.span)
-                raise Qasm3ConversionError(f"Unsupported gate definition statement {gate_op}")
+                raise_qasm3_error(
+                    f"Unsupported gate definition statement {gate_op}", span=gate_op.span
+                )
 
         self._restore_context()
 
-    def _collapse_gate_modifiers(self, operation: QuantumGate) -> tuple:
+    def _collapse_gate_modifiers(self, operation: qasm3_ast.QuantumGate) -> tuple:
         """Collapse the gate modifiers of a gate operation.
            Some analysis is required to get this result.
            The basic idea is that any power operation is multiplied and inversions are toggled.
            The placement of the inverse operation does not matter.
 
         Args:
-            operation (QuantumGate): The gate operation to collapse modifiers for.
+            operation (qasm3_ast.QuantumGate): The gate operation to collapse modifiers for.
 
         Returns:
             tuple[Any, Any]: The power and inverse values of the gate operation.
@@ -931,28 +742,29 @@ def _collapse_gate_modifiers(self, operation: QuantumGate) -> tuple:
 
         for modifier in operation.modifiers:
             modifier_name = modifier.modifier
-            if modifier_name == GateModifierName.pow and modifier.argument is not None:
-                current_power = self._evaluate_expression(modifier.argument)
+            if modifier_name == qasm3_ast.GateModifierName.pow and modifier.argument is not None:
+                current_power = Qasm3ExprEvaluator.evaluate_expression(modifier.argument)
                 if current_power < 0:
                     inverse_value = not inverse_value
                 power_value = power_value * abs(current_power)
-            elif modifier_name == GateModifierName.inv:
+            elif modifier_name == qasm3_ast.GateModifierName.inv:
                 inverse_value = not inverse_value
             elif modifier_name in [
-                GateModifierName.ctrl,
-                GateModifierName.negctrl,
+                qasm3_ast.GateModifierName.ctrl,
+                qasm3_ast.GateModifierName.negctrl,
             ]:
-                self._print_err_location(operation.span)
-                raise NotImplementedError(
-                    "Controlled modifier gates not yet supported in gate operation"
+                raise_qasm3_error(
+                    f"Controlled modifier gates not yet supported in gate operation {operation}",
+                    err_type=NotImplementedError,
+                    span=operation.span,
                 )
         return (power_value, inverse_value)
 
-    def _visit_generic_gate_operation(self, operation: QuantumGate) -> None:
+    def _visit_generic_gate_operation(self, operation: qasm3_ast.QuantumGate) -> None:
         """Visit a gate operation element.
 
         Args:
-            operation (QuantumGate): The gate operation to visit.
+            operation (qasm3_ast.QuantumGate): The gate operation to visit.
 
         Returns:
             None
@@ -962,10 +774,12 @@ def _visit_generic_gate_operation(self, operation: QuantumGate) -> None:
 
         # only needs to be done once for a gate operation
         if not self._in_gate_scope() and len(self._function_qreg_size_map) > 0:
-            # atleast in SOME function scope
+            # we are in SOME function scope
             # transform qubits to use the global qreg identifiers
-            operation.qubits = self._transform_function_qubits(
-                operation, self._function_qreg_size_map[-1], self._function_qreg_transform_map[-1]
+            operation.qubits = Qasm3Transformer.transform_function_qubits(
+                operation,
+                self._function_qreg_size_map[-1],
+                self._function_qreg_transform_map[-1],
             )
         # Applying the inverse first and then the power is same as
         # apply the power first and then inverting the result
@@ -975,104 +789,13 @@ def _visit_generic_gate_operation(self, operation: QuantumGate) -> None:
             else:
                 self._visit_basic_gate_operation(operation, inverse_value)
 
-    def _validate_variable_assignment_value(self, variable: Variable, value) -> None:
-        """Validate the assignment of a value to a variable.
-
-        Args:
-            variable (Variable): The variable to assign to.
-            value (Any): The value to assign.
-
-        Raises:
-            Qasm3ConversionError: If the value is not of the correct type.
-        """
-        # check 1 - type match
-        qasm_type = variable.base_type.__class__
-        base_size = variable.base_size
-
-        try:
-            type_to_match = VARIABLE_TYPE_MAP[qasm_type]
-        except KeyError as err:
-            raise Qasm3ConversionError(
-                f"Invalid type {qasm_type} for variable {variable.name}"
-            ) from err
-
-        # For each type we will have a "castable" type set and its corresponding cast operation
-        type_casted_value = qasm_variable_type_cast(qasm_type, variable.name, base_size, value)
-        # check 2 - range match , if bits mentioned in base size
-        if type_to_match == int:
-            base_size = variable.base_size
-            left, right = 0, 0
-            if qasm_type == Qasm3IntType:
-                left, right = (
-                    -1 * (2 ** (base_size - 1)),
-                    2 ** (base_size - 1) - 1,
-                )
-            else:
-                # would be uint only so we correctly get this
-                left, right = 0, 2**base_size - 1
-            if type_casted_value < left or type_casted_value > right:
-                raise Qasm3ConversionError(
-                    f"Value {type_casted_value} out of limits for variable {variable.name} "
-                    f"with base size {base_size}"
-                )
-
-        elif type_to_match == float:
-            base_size = variable.base_size
-            left, right = 0, 0
-
-            if base_size == 32:
-                left, right = -(LIMITS_MAP["float_32"]), (LIMITS_MAP["float_32"])
-            else:
-                left, right = -(LIMITS_MAP["float_64"]), (LIMITS_MAP["float_64"])
-
-            if type_casted_value < left or type_casted_value > right:
-                raise Qasm3ConversionError(
-                    f"Value {value} out of limits for variable {variable.name} "
-                    f"with base size {base_size}"
-                )
-        elif type_to_match == bool:
-            pass
-        else:
-            raise TypeError(f"Invalid type {type_to_match} for variable {variable.name}")
-
-        return type_casted_value
-
-    def _validate_array_assignment_values(
-        self, variable: Variable, dimensions: list[int], values: list
-    ) -> None:
-        """Validate the assignment of values to an array variable.
-
-        Args:
-            variable (Variable): The variable to assign to.
-            values (list[Any]): The values to assign.
-
-        Raises:
-            Qasm3ConversionError: If the values are not of the correct type.
-        """
-        # recursively check the array
-        if len(values) != dimensions[0]:
-            raise Qasm3ConversionError(
-                f"Invalid dimensions for array assignment to variable {variable.name}. "
-                f"Expected {dimensions[0]} but got {len(values)}"
-            )
-        for i, value in enumerate(values):
-            if isinstance(value, list):
-                self._validate_array_assignment_values(variable, dimensions[1:], value)
-            else:
-                if len(dimensions) != 1:
-                    raise Qasm3ConversionError(
-                        f"Invalid dimensions for array assignment to variable {variable.name}. "
-                        f"Expected {len(dimensions)} but got 1"
-                    )
-                values[i] = self._validate_variable_assignment_value(variable, value)
-
-    def _visit_constant_declaration(self, statement: ConstantDeclaration) -> None:
+    def _visit_constant_declaration(self, statement: qasm3_ast.ConstantDeclaration) -> None:
         """
         Visit a constant declaration element. Const can only be declared for scalar
         type variables and not arrays. Assignment is mandatory in constant declaration.
 
         Args:
-            statement (ConstantDeclaration): The constant declaration to visit.
+            statement (qasm3_ast.ConstantDeclaration): The constant declaration to visit.
 
         Returns:
             None
@@ -1081,35 +804,36 @@ def _visit_constant_declaration(self, statement: ConstantDeclaration) -> None:
         var_name = statement.identifier.name
 
         if var_name in CONSTANTS_MAP:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(f"Can not declare variable with keyword name {var_name}")
-
-        if self._check_in_scope(var_name, self._get_curr_scope()):
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(f"Re-declaration of variable {var_name}")
-
-        init_value = self._evaluate_expression(statement.init_expression, const_expr=True)
+            raise_qasm3_error(
+                f"Can not declare variable with keyword name {var_name}", span=statement.span
+            )
+        if self._check_in_scope(var_name):
+            raise_qasm3_error(f"Re-declaration of variable {var_name}", span=statement.span)
+        init_value = Qasm3ExprEvaluator.evaluate_expression(
+            statement.init_expression, const_expr=True
+        )
 
         base_type = statement.type
-        if isinstance(base_type, BoolType):
+        if isinstance(base_type, qasm3_ast.BoolType):
             base_size = 1
         elif base_type.size is None:
             base_size = 32  # default for now
         else:
-            base_size = self._evaluate_expression(base_type.size, const_expr=True)
+            base_size = Qasm3ExprEvaluator.evaluate_expression(base_type.size, const_expr=True)
             if not isinstance(base_size, int) or base_size <= 0:
-                self._print_err_location(statement.span)
-                raise Qasm3ConversionError(f"Invalid base size {base_size} for variable {var_name}")
+                raise_qasm3_error(
+                    f"Invalid base size {base_size} for variable {var_name}", span=statement.span
+                )
 
         variable = Variable(var_name, base_type, base_size, [], init_value, is_constant=True)
 
         # cast + validation
-        variable.value = self._validate_variable_assignment_value(variable, init_value)
+        variable.value = Qasm3Validator.validate_variable_assignment_value(variable, init_value)
 
         self._add_var_in_scope(variable)
 
     # pylint: disable=too-many-branches
-    def _visit_classical_declaration(self, statement: ClassicalDeclaration) -> None:
+    def _visit_classical_declaration(self, statement: qasm3_ast.ClassicalDeclaration) -> None:
         """Visit a classical operation element.
 
         Args:
@@ -1120,9 +844,10 @@ def _visit_classical_declaration(self, statement: ClassicalDeclaration) -> None:
         """
         var_name = statement.identifier.name
         if var_name in CONSTANTS_MAP:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(f"Can not declare variable with keyword name {var_name}")
-        if self._check_in_scope(var_name, self._get_curr_scope()):
+            raise_qasm3_error(
+                f"Can not declare variable with keyword name {var_name}", span=statement.span
+            )
+        if self._check_in_scope(var_name):
             if self._in_block_scope() and var_name not in self._get_curr_scope():
                 # we can re-declare variables once in block scope even if they are
                 # present in the parent scope
@@ -1132,31 +857,30 @@ def _visit_classical_declaration(self, statement: ClassicalDeclaration) -> None:
                 # }
                 pass
             else:
-                self._print_err_location(statement.span)
-                raise Qasm3ConversionError(f"Re-declaration of variable {var_name}")
+                raise_qasm3_error(f"Re-declaration of variable {var_name}", span=statement.span)
 
         init_value = None
         base_type = statement.type
         final_dimensions = []
 
-        if isinstance(base_type, ArrayType):
+        if isinstance(base_type, qasm3_ast.ArrayType):
             dimensions = base_type.dimensions
 
             if len(dimensions) > MAX_ARRAY_DIMENSIONS:
-                self._print_err_location(statement.span)
-                raise Qasm3ConversionError(
+                raise_qasm3_error(
                     f"Invalid dimensions {len(dimensions)} for array declaration for {var_name}. "
-                    f"Max allowed dimensions is {MAX_ARRAY_DIMENSIONS}"
+                    f"Max allowed dimensions is {MAX_ARRAY_DIMENSIONS}",
+                    span=statement.span,
                 )
 
             base_type = base_type.base_type
             num_elements = 1
             for dim in dimensions:
-                dim_value = self._evaluate_expression(dim)
+                dim_value = Qasm3ExprEvaluator.evaluate_expression(dim)
                 if not isinstance(dim_value, int) or dim_value <= 0:
-                    self._print_err_location(statement.span)
-                    raise Qasm3ConversionError(
-                        f"Invalid dimension size {dim_value} in array declaration for {var_name}"
+                    raise_qasm3_error(
+                        f"Invalid dimension size {dim_value} in array declaration for {var_name}",
+                        span=statement.span,
                     )
                 final_dimensions.append(dim_value)
                 num_elements *= dim_value
@@ -1166,122 +890,47 @@ def _visit_classical_declaration(self, statement: ClassicalDeclaration) -> None:
                 init_value = [init_value for _ in range(dim)]
 
         if statement.init_expression:
-            if isinstance(statement.init_expression, ArrayLiteral):
+            if isinstance(statement.init_expression, qasm3_ast.ArrayLiteral):
                 init_value = self._evaluate_array_initialization(
                     statement.init_expression, final_dimensions, base_type
                 )
             else:
-                init_value = self._evaluate_expression(statement.init_expression)
+                init_value = Qasm3ExprEvaluator.evaluate_expression(statement.init_expression)
         base_size = 1
-        if not isinstance(base_type, BoolType):
-            base_size = 32 if base_type.size is None else self._evaluate_expression(base_type.size)
+        if not isinstance(base_type, qasm3_ast.BoolType):
+            base_size = (
+                32
+                if base_type.size is None
+                else Qasm3ExprEvaluator.evaluate_expression(base_type.size)
+            )
 
         if not isinstance(base_size, int) or base_size <= 0:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(f"Invalid base size {base_size} for variable {var_name}")
+            raise_qasm3_error(
+                f"Invalid base size {base_size} for variable {var_name}", span=statement.span
+            )
 
-        if isinstance(base_type, Qasm3FloatType) and base_size not in [32, 64]:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(
-                f"Invalid base size {base_size} for float variable {var_name}"
+        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)
 
         if statement.init_expression:
             if isinstance(init_value, list):
-                self._validate_array_assignment_values(variable, variable.dims, init_value)
+                Qasm3Validator.validate_array_assignment_values(variable, variable.dims, init_value)
             else:
-                variable.value = self._validate_variable_assignment_value(variable, init_value)
-
-        self._add_var_in_scope(variable)
-
-    def _analyse_classical_indices(self, indices: list[IntegerLiteral], var_name: str) -> None:
-        """Validate the indices for a classical variable.
-
-        Args:
-            indices (list[list[Any]]): The indices to validate.
-            var_name (str): The name of the variable.
-
-        Raises:
-            Qasm3ConversionError: If the indices are invalid.
-
-        Returns:
-            list: The list of indices.
-        """
-        indices_list = []
-        var_dimensions = self._get_from_visible_scope(var_name).dims
-
-        if not var_dimensions:
-            self._print_err_location(indices[0].span)
-            raise Qasm3ConversionError(f"Indexing error. Variable {var_name} is not an array")
-
-        if len(indices) != len(var_dimensions):
-            self._print_err_location(indices[0].span)
-            raise Qasm3ConversionError(
-                f"Invalid number of indices for variable {var_name}. "
-                f"Expected {len(var_dimensions)} but got {len(indices)}"
-            )
-
-        for i, index in enumerate(indices):
-            if isinstance(index, RangeDefinition):
-                self._print_err_location(index.span)
-                raise Qasm3ConversionError(
-                    f"Range based indexing {index} not supported for classical variable {var_name}"
-                )
-            if not isinstance(index, IntegerLiteral):
-                self._print_err_location(index.span)
-                raise Qasm3ConversionError(
-                    f"Unsupported index type {type(index)} for classical variable {var_name}"
+                variable.value = Qasm3Validator.validate_variable_assignment_value(
+                    variable, init_value
                 )
-            index_value = index.value
-            curr_dimension = var_dimensions[i]
-
-            if index_value < 0 or index_value >= curr_dimension:
-                self._print_err_location(index.span)
-                raise Qasm3ConversionError(
-                    f"Index {index_value} out of bounds for dimension {i+1} of variable {var_name}"
-                )
-            indices_list.append(index_value)
-
-        return indices_list
-
-    def _update_array_element(self, multi_dim_list, indices, value):
-        """Update the value of an array at the specified indices.
 
-        Args:
-            multi_dim_list (list): The multi-dimensional list to update.
-            indices (list[int]): The indices to update.
-            value (Any): The value to update.
-
-        Returns:
-            None
-        """
-        temp = multi_dim_list
-        for index in indices[:-1]:
-            temp = temp[index]
-        temp[indices[-1]] = value
-
-    def _find_array_element(self, multi_dim_list, indices):
-        """Find the value of an array at the specified indices.
-
-        Args:
-            multi_dim_list (list): The multi-dimensional list to search.
-            indices (list[int]): The indices to search.
-
-        Returns:
-            Any: The value at the specified indices.
-        """
-        temp = multi_dim_list
-        for index in indices:
-            temp = temp[index]
-        return temp
+        self._add_var_in_scope(variable)
 
-    def _visit_classical_assignment(self, statement: ClassicalAssignment) -> None:
+    def _visit_classical_assignment(self, statement: qasm3_ast.ClassicalAssignment) -> None:
         """Visit a classical assignment element.
 
         Args:
-            statement (ClassicalAssignment): The classical assignment to visit.
+            statement (qasm3_ast.ClassicalAssignment): The classical assignment to visit.
 
         Returns:
             None
@@ -1289,49 +938,51 @@ def _visit_classical_assignment(self, statement: ClassicalAssignment) -> None:
         lvalue = statement.lvalue
         var_name = lvalue.name
 
-        if isinstance(lvalue, IndexedIdentifier):
+        if isinstance(lvalue, qasm3_ast.IndexedIdentifier):
             var_name = var_name.name
 
         var = self._get_from_visible_scope(var_name)
 
         if var is None:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(f"Undefined variable {var_name} in assignment")
+            raise_qasm3_error(f"Undefined variable {var_name} in assignment", span=statement.span)
 
         if var.is_constant:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(f"Assignment to constant variable {var_name} not allowed")
+            raise_qasm3_error(
+                f"Assignment to constant variable {var_name} not allowed", span=statement.span
+            )
 
-        var_value = self._evaluate_expression(statement.rvalue)
+        var_value = Qasm3ExprEvaluator.evaluate_expression(statement.rvalue)
 
         # currently we support single array assignment only
         # range based assignment not supported yet
 
         # cast + validation
-        var_value = self._validate_variable_assignment_value(var, var_value)
+        var_value = Qasm3Validator.validate_variable_assignment_value(var, var_value)
 
         # handle assignment for arrays
-        if isinstance(lvalue, IndexedIdentifier):
+        if isinstance(lvalue, qasm3_ast.IndexedIdentifier):
             # stupid indices structure in openqasm :/
             if len(lvalue.indices[0]) > 1:
                 indices = lvalue.indices[0]
             else:
                 indices = [idx[0] for idx in lvalue.indices]
 
-            validated_indices = self._analyse_classical_indices(indices, var_name)
-            self._update_array_element(var.value, validated_indices, var_value)
+            validated_indices = Qasm3Analyzer.analyze_classical_indices(
+                indices, self._get_from_visible_scope(var_name)
+            )
+            Qasm3Transformer.update_array_element(var.value, validated_indices, var_value)
         else:
             var.value = var_value
 
         self._update_var_in_scope(var)
 
     def _evaluate_array_initialization(
-        self, array_literal: ArrayLiteral, dimensions: list[int], base_type
+        self, array_literal: qasm3_ast.ArrayLiteral, dimensions: list[int], base_type: any
     ) -> list:
         """Evaluate an array initialization.
 
         Args:
-            array_literal (ArrayLiteral): The array literal to evaluate.
+            array_literal (qasm3_ast.ArrayLiteral): The array literal to evaluate.
             dimensions (list[int]): The dimensions of the array.
             base_type (Any): The base type of the array.
 
@@ -1341,227 +992,21 @@ def _evaluate_array_initialization(
         init_values = []
 
         for value in array_literal.values:
-            if isinstance(value, ArrayLiteral):
+            if isinstance(value, qasm3_ast.ArrayLiteral):
                 init_values.append(
                     self._evaluate_array_initialization(value, dimensions[1:], base_type)
                 )
             else:
-                eval_value = self._evaluate_expression(value)
+                eval_value = Qasm3ExprEvaluator.evaluate_expression(value)
                 init_values.append(eval_value)
 
         return init_values
 
-    def _analyse_index_expression(self, index_expr: IndexExpression) -> tuple[str, list[list]]:
-        """Analyse an index expression to get the variable name and indices.
-
-        Args:
-            index_expr (IndexExpression): The index expression to analyse.
-
-        Returns:
-            tuple[str, list[list]]: The variable name and indices.
-
-        """
-        indices = []
-        var_name = None
-        comma_separated = False
-
-        if isinstance(index_expr.collection, IndexExpression):
-            while isinstance(index_expr, IndexExpression):
-                indices.append(index_expr.index[0])
-                index_expr = index_expr.collection
-        else:
-            comma_separated = True
-            indices = index_expr.index
-
-        var_name = index_expr.collection.name if comma_separated else index_expr.name
-        if not comma_separated:
-            indices = indices[::-1]
-
-        return var_name, indices
-
-    # pylint: disable-next=too-many-return-statements, too-many-statements
-    def _evaluate_expression(self, expression, const_expr: bool = False, reqd_type=None):
-        """Evaluate an expression. Scalar types are assigned by value.
-
-        Args:
-            expression (Any): The expression to evaluate.
-            const_expr (bool): Whether the expression is a constant. Defaults to False.
-            reqd_type (Any): The required type of the expression. Defaults to None.
-
-        Returns:
-            bool: The result of the evaluation.
-
-        Raises:
-            Qasm3ConversionError: If the expression is not supported.
-        """
-        if expression is None:
-            return None
-
-        if isinstance(expression, (ImaginaryLiteral, DurationLiteral)):
-            self._print_err_location(expression.span)
-            raise Qasm3ConversionError(f"Unsupported expression type {type(expression)}")
-
-        def _check_var_in_scope(var_name):
-            if not self._check_in_scope(var_name, self._get_curr_scope()):
-                self._print_err_location(expression.span)
-                raise Qasm3ConversionError(f"Undefined identifier {var_name} in expression")
-
-        def _check_var_constant(var_name):
-            const_var = self._get_from_visible_scope(var_name).is_constant
-            if const_expr and not const_var:
-                self._print_err_location(expression.span)
-                raise Qasm3ConversionError(
-                    f"Variable '{var_name}' is not a constant in given expression"
-                )
-
-        def _check_var_type(var_name, reqd_type):
-            if not self._validate_variable_type(var_name, reqd_type):
-                self._print_err_location(expression.span)
-                raise Qasm3ConversionError(
-                    f"Invalid type of variable {var_name} for required type {reqd_type}"
-                )
-
-        def _check_var_initialized(var_name, var_value):
-            if var_value is None:
-                self._print_err_location(expression.span)
-                raise Qasm3ConversionError(f"Uninitialized variable {var_name} in expression")
-
-        def _get_var_value(var_name, indices=None):
-            var_value = None
-            if isinstance(expression, Identifier):
-                var_value = self._get_from_visible_scope(var_name).value
-            else:
-                validated_indices = self._analyse_classical_indices(indices, var_name)
-                var_value = self._find_array_element(
-                    self._get_from_visible_scope(var_name).value, validated_indices
-                )
-            return var_value
-
-        def process_variable(var_name, indices=None):
-            _check_var_in_scope(var_name)
-            _check_var_constant(var_name)
-            _check_var_type(var_name, reqd_type)
-            var_value = _get_var_value(var_name, indices)
-            _check_var_initialized(var_name, var_value)
-            return var_value
-
-        if isinstance(expression, Identifier):
-            var_name = expression.name
-            if var_name in CONSTANTS_MAP:
-                if not reqd_type or reqd_type == Qasm3FloatType:
-                    return CONSTANTS_MAP[var_name]
-                self._print_err_location(expression.span)
-                raise Qasm3ConversionError(
-                    f"Constant {var_name} not allowed in non-float expression"
-                )
-            return process_variable(var_name)
-
-        if isinstance(expression, IndexExpression):
-            var_name, indices = self._analyse_index_expression(expression)
-            return process_variable(var_name, indices)
-
-        if isinstance(expression, (BooleanLiteral, IntegerLiteral, FloatLiteral)):
-            if reqd_type:
-                if reqd_type == BoolType and isinstance(expression, BooleanLiteral):
-                    return expression.value
-                if reqd_type == Qasm3IntType and isinstance(expression, IntegerLiteral):
-                    return expression.value
-                if reqd_type == Qasm3FloatType and isinstance(expression, FloatLiteral):
-                    return expression.value
-                self._print_err_location(expression.span)
-                raise Qasm3ConversionError(
-                    f"Invalid type {type(expression)} for required type {reqd_type}"
-                )
-            return expression.value
-
-        if isinstance(expression, UnaryExpression):
-            operand = self._evaluate_expression(expression.expression, const_expr, reqd_type)
-            if expression.op.name == "~" and not isinstance(operand, int):
-                self._print_err_location(expression.span)
-                raise Qasm3ConversionError(
-                    f"Unsupported expression type {type(operand)} in ~ operation"
-                )
-            return qasm3_expression_op_map(
-                "UMINUS" if expression.op.name == "-" else expression.op.name, operand
-            )
-        if isinstance(expression, BinaryExpression):
-            lhs = self._evaluate_expression(expression.lhs, const_expr, reqd_type)
-            rhs = self._evaluate_expression(expression.rhs, const_expr, reqd_type)
-            return qasm3_expression_op_map(expression.op.name, lhs, rhs)
-
-        if isinstance(expression, FunctionCall):
-            # function will not return a reqd / const type
-            # Reference : https://openqasm.com/language/types.html#compile-time-constants
-            # para      : 5
-            return self._visit_function_call(expression)
-
-        self._print_err_location(expression.span)
-        raise Qasm3ConversionError(f"Unsupported expression type {type(expression)}")
-
-    def _analyse_branch_condition(self, condition) -> bool:
-        """
-        Analyse the branching condition to determine the branch to take
-
-        Args:
-            condition (Any): The condition to analyse
-
-        Returns:
-            bool: The branch to take
-        """
-
-        if isinstance(condition, UnaryExpression):
-            if condition.op.name != "!":
-                self._print_err_location(condition.span)
-                raise Qasm3ConversionError(
-                    f"Unsupported unary expression '{condition.op.name}' in if condition"
-                )
-            return False
-        if isinstance(condition, BinaryExpression):
-            if condition.op.name != "==":
-                self._print_err_location(condition.span)
-                raise Qasm3ConversionError(
-                    f"Unsupported binary expression '{condition.op.name}' in if condition"
-                )
-            if not isinstance(condition.lhs, IndexExpression):
-                self._print_err_location(condition.span)
-                raise Qasm3ConversionError(
-                    f"Unsupported expression type '{type(condition.lhs)}' in if condition"
-                )
-            return condition.rhs.value != 0
-        if not isinstance(condition, IndexExpression):
-            self._print_err_location(condition.span)
-            raise Qasm3ConversionError(
-                f"Unsupported expression type '{type(condition)}' in if condition. "
-                "Can only be a simple comparison"
-            )
-        return True
-
-    def _get_branch_params(self, condition) -> tuple[Union[int, None], Union[str, None]]:
-        """
-        Get the branch parameters from the branching condition
-
-        Args:
-            condition (Any): The condition to analyse
-
-        Returns:
-            tuple[Union[int, None], Union[str, None]]: The branch parameters
-        """
-        if isinstance(condition, UnaryExpression):
-            return (
-                condition.expression.index[0].value,
-                condition.expression.collection.name,
-            )
-        if isinstance(condition, BinaryExpression):
-            return condition.lhs.index[0].value, condition.lhs.collection.name
-        if isinstance(condition, IndexExpression):
-            return condition.index[0].value, condition.collection.name
-        return None, None
-
-    def _visit_branching_statement(self, statement: BranchingStatement) -> None:
+    def _visit_branching_statement(self, statement: qasm3_ast.BranchingStatement) -> None:
         """Visit a branching statement element.
 
         Args:
-            statement (BranchingStatement): The branching statement to visit.
+            statement (qasm3_ast.BranchingStatement): The branching statement to visit.
 
         Returns:
             None
@@ -1572,23 +1017,25 @@ def _visit_branching_statement(self, statement: BranchingStatement) -> None:
         self._label_scope_level[self._curr_scope] = set()
 
         condition = statement.condition
-        positive_branching = self._analyse_branch_condition(condition)
+        positive_branching = Qasm3Analyzer.analyse_branch_condition(condition)
 
         if_block = statement.if_block
         if not statement.if_block:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError("Missing if block")
+            raise_qasm3_error("Missing if block", span=statement.span)
         else_block = statement.else_block
         if not positive_branching:
             if_block, else_block = else_block, if_block
 
-        reg_id, reg_name = self._get_branch_params(condition)
+        reg_id, reg_name = Qasm3Transformer.get_branch_params(condition)
 
         if reg_name not in self._global_creg_size_map:
-            raise Qasm3ConversionError(
-                f"Missing register declaration for {reg_name} in {condition}"
+            raise_qasm3_error(
+                f"Missing register declaration for {reg_name} in {condition}",
+                span=statement.span,
             )
-        self._validate_register_index(reg_id, self._global_creg_size_map[reg_name], qubit=False)
+        Qasm3Validator.validate_register_index(
+            reg_id, self._global_creg_size_map[reg_name], qubit=False
+        )
 
         def _visit_statement_block(block):
             for stmt in block:
@@ -1607,18 +1054,25 @@ def _visit_statement_block(block):
         self._pop_scope()
         self._restore_context()
 
-    def _visit_forin_loop(self, statement: ForInLoop) -> None:
+    def _visit_forin_loop(self, statement: qasm3_ast.ForInLoop) -> None:
         # Compute loop variable values
-        if isinstance(statement.set_declaration, RangeDefinition):
+        if isinstance(statement.set_declaration, qasm3_ast.RangeDefinition):
             init_exp = statement.set_declaration.start
-            startval = self._evaluate_expression(init_exp)
+            startval = Qasm3ExprEvaluator.evaluate_expression(init_exp)
             range_def = statement.set_declaration
-            stepval = 1 if range_def.step is None else self._evaluate_expression(range_def.step)
-            endval = self._evaluate_expression(range_def.end)
+            stepval = (
+                1
+                if range_def.step is None
+                else Qasm3ExprEvaluator.evaluate_expression(range_def.step)
+            )
+            endval = Qasm3ExprEvaluator.evaluate_expression(range_def.end)
             irange = list(range(startval, endval + stepval, stepval))
-        elif isinstance(statement.set_declaration, DiscreteSet):
+        elif isinstance(statement.set_declaration, qasm3_ast.DiscreteSet):
             init_exp = statement.set_declaration.values[0]
-            irange = [self._evaluate_expression(exp) for exp in statement.set_declaration.values]
+            irange = [
+                Qasm3ExprEvaluator.evaluate_expression(exp)
+                for exp in statement.set_declaration.values
+            ]
         else:
             raise Qasm3ConversionError(
                 f"Unexpected type {type(statement.set_declaration)} of set_declaration in loop."
@@ -1634,7 +1088,7 @@ def _visit_forin_loop(self, statement: ForInLoop) -> None:
             # need to re-declare as we discard the block scope in subsequent
             # iterations of the loop
             self._visit_classical_declaration(
-                ClassicalDeclaration(statement.type, statement.identifier, init_exp)
+                qasm3_ast.ClassicalDeclaration(statement.type, statement.identifier, init_exp)
             )
             i = self._get_from_visible_scope(statement.identifier.name)
 
@@ -1649,12 +1103,12 @@ def _visit_forin_loop(self, statement: ForInLoop) -> None:
             self._pop_scope()
             self._restore_context()
 
-    def _visit_subroutine_definition(self, statement: SubroutineDefinition) -> None:
+    def _visit_subroutine_definition(self, statement: qasm3_ast.SubroutineDefinition) -> None:
         """Visit a subroutine definition element.
            Reference: https://openqasm.com/language/subroutines.html#subroutines
 
         Args:
-            statement (SubroutineDefinition): The subroutine definition to visit.
+            statement (qasm3_ast.SubroutineDefinition): The subroutine definition to visit.
 
         Returns:
             None
@@ -1662,100 +1116,22 @@ def _visit_subroutine_definition(self, statement: SubroutineDefinition) -> None:
         fn_name = statement.name.name
 
         if fn_name in CONSTANTS_MAP:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(f"Subroutine name '{fn_name}' is a reserved keyword")
+            raise_qasm3_error(
+                f"Subroutine name '{fn_name}' is a reserved keyword", span=statement.span
+            )
 
         if fn_name in self._subroutine_defns:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(f"Redefinition of subroutine '{fn_name}'")
+            raise_qasm3_error(f"Redefinition of subroutine '{fn_name}'", span=statement.span)
 
-        if self._check_in_scope(fn_name, self._get_curr_scope()):
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(
-                f"Can not declare subroutine with name '{fn_name}' "
-                "as it is already declared as a variable"
+        if self._check_in_scope(fn_name):
+            raise_qasm3_error(
+                f"Can not declare subroutine with name '{fn_name}' as "
+                "it is already declared as a variable",
+                span=statement.span,
             )
 
         self._subroutine_defns[fn_name] = statement
 
-    # pylint: disable=inconsistent-return-statements
-    def _validate_return_statement(
-        self,
-        subroutine_def: SubroutineDefinition,
-        return_statement: ReturnStatement,
-        return_value: any,
-    ):
-        """Validate the return type of a function.
-
-        Args:
-            subroutine_def (SubroutineDefinition): The subroutine definition.
-            return_statement (ReturnStatement): The return statement.
-            return_value (Any): The return value.
-
-        Raises:
-            Qasm3ConversionError: If the return type is invalid.
-        """
-
-        if subroutine_def.return_type is None:
-            if return_value is not None:
-                self._print_err_location(return_statement.span)
-                raise Qasm3ConversionError(
-                    f"Return type mismatch for subroutine '{subroutine_def.name.name}'."
-                    f" Expected void but got {type(return_value)}"
-                )
-        else:
-            if return_value is None:
-                self._print_err_location(return_statement.span)
-                raise Qasm3ConversionError(
-                    f"Return type mismatch for subroutine '{subroutine_def.name.name}'."
-                    f" Expected {subroutine_def.return_type} but got void"
-                )
-            base_size = 1
-            if hasattr(subroutine_def.return_type, "size"):
-                base_size = subroutine_def.return_type.size.value
-
-            return self._validate_variable_assignment_value(
-                Variable(
-                    subroutine_def.name.name + "_return",
-                    subroutine_def.return_type,
-                    base_size,
-                    None,
-                    None,
-                ),
-                return_value,
-            )
-
-    def _transform_function_qubits(
-        self, q_op, formal_qreg_sizes: dict[str:int], qubit_map: dict[tuple:tuple]
-    ) -> list:
-        """Transform the qubits of a function call to the actual qubits.
-
-        Args:
-            gate_op : The quantum operation to transform.
-            formal_qreg_sizes (dict[str: int]): The formal qubit register sizes.
-            qubit_map (dict[tuple: tuple]): The mapping of formal qubits to actual qubits.
-
-        Returns:
-            None
-        """
-        expanded_op_qubits = self._get_op_qubits(q_op, formal_qreg_sizes, qir_form=False)
-
-        transformed_qubits = []
-        for qubit in expanded_op_qubits:
-            formal_qreg_name = qubit.name.name
-            formal_qreg_idx = qubit.indices[0][0].value
-
-            # replace the formal qubit with the actual qubit
-            actual_qreg_name, actual_qreg_idx = qubit_map[(formal_qreg_name, formal_qreg_idx)]
-            transformed_qubits.append(
-                IndexedIdentifier(
-                    Identifier(actual_qreg_name),
-                    [[IntegerLiteral(actual_qreg_idx)]],
-                )
-            )
-
-        return transformed_qubits
-
     def _get_target_qubits(self, target, qreg_size_map, target_name):
         """Get the target qubits of a statement.
 
@@ -1770,24 +1146,28 @@ def _get_target_qubits(self, target, qreg_size_map, target_name):
         target_qids = None
         target_qubits_size = None
 
-        if isinstance(target, Identifier):  # "(q);"
+        if isinstance(target, qasm3_ast.Identifier):  # "(q);"
             target_qids = list(range(qreg_size_map[target_name]))
             target_qubits_size = qreg_size_map[target_name]
 
-        elif isinstance(target, IndexExpression):
-            if isinstance(target.index, DiscreteSet):  # "(q[{0,1}]);"
-                target_qids = self._extract_values_from_discrete_set(target.index)
+        elif isinstance(target, qasm3_ast.IndexExpression):
+            if isinstance(target.index, qasm3_ast.DiscreteSet):  # "(q[{0,1}]);"
+                target_qids = Qasm3Transformer.extract_values_from_discrete_set(target.index)
                 for qid in target_qids:
-                    self._validate_register_index(qid, qreg_size_map[target_name], qubit=True)
+                    Qasm3Validator.validate_register_index(
+                        qid, qreg_size_map[target_name], qubit=True
+                    )
                 target_qubits_size = len(target_qids)
-            elif isinstance(target.index[0], (IntegerLiteral, Identifier)):  # "(q[0]); OR (q[i]);"
-                target_qids = [self._evaluate_expression(target.index[0])]
-                self._validate_register_index(
+            elif isinstance(
+                target.index[0], (qasm3_ast.IntegerLiteral, qasm3_ast.Identifier)
+            ):  # "(q[0]); OR (q[i]);"
+                target_qids = [Qasm3ExprEvaluator.evaluate_expression(target.index[0])]
+                Qasm3Validator.validate_register_index(
                     target_qids[0], qreg_size_map[target_name], qubit=True
                 )
                 target_qubits_size = 1
-            elif isinstance(target.index[0], RangeDefinition):  # "(q[0:1:2]);"
-                target_qids = self._get_qubits_from_range_definition(
+            elif isinstance(target.index[0], qasm3_ast.RangeDefinition):  # "(q[0:1:2]);"
+                target_qids = Qasm3Transformer.get_qubits_from_range_definition(
                     target.index[0],
                     qreg_size_map[target_name],
                     is_qubit_reg=True,
@@ -1796,27 +1176,26 @@ def _get_target_qubits(self, target, qreg_size_map, target_name):
         return target_qids, target_qubits_size
 
     # pylint: disable=too-many-locals, too-many-statements
-    def _visit_function_call(self, statement: FunctionCall) -> None:
+    def _visit_function_call(self, statement: qasm3_ast.FunctionCall) -> None:
         """Visit a function call element.
 
         Args:
-            statement (FunctionCall): The function call to visit.
+            statement (qasm3_ast.FunctionCall): The function call to visit.
         Returns:
             None
 
         """
         fn_name = statement.name.name
         if fn_name not in self._subroutine_defns:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(f"Undefined subroutine '{fn_name}' was called")
+            raise_qasm3_error(f"Undefined subroutine '{fn_name}' was called", span=statement.span)
 
         subroutine_def = self._subroutine_defns[fn_name]
 
         if len(statement.arguments) != len(subroutine_def.arguments):
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(
+            raise_qasm3_error(
                 f"Parameter count mismatch for subroutine '{fn_name}'. Expected "
-                f"{len(subroutine_def.arguments)} but got {len(statement.arguments)} in call"
+                f"{len(subroutine_def.arguments)} but got {len(statement.arguments)} in call",
+                span=statement.span,
             )
 
         duplicate_qubit_detect_map = {}
@@ -1825,29 +1204,6 @@ def _visit_function_call(self, statement: FunctionCall) -> None:
 
         quantum_vars, classical_vars = [], []
 
-        def _validate_unique_qubits(reg_name, indices):
-            """
-            Validates that the qubits in the given register are unique.
-
-            Args:
-                reg_name (str): The name of the register.
-                indices (list): A list of indices representing the qubits.
-
-            Raises:
-                Qasm3ConversionError: If duplicate qubits are found in the function call.
-            """
-            if reg_name not in duplicate_qubit_detect_map:
-                duplicate_qubit_detect_map[reg_name] = set(indices)
-            else:
-                for idx in indices:
-                    if idx in duplicate_qubit_detect_map[reg_name]:
-                        self._print_err_location(statement.span)
-                        raise Qasm3ConversionError(
-                            f"Duplicate qubit argument '{reg_name}[{idx}]' "
-                            f"in function call for '{fn_name}'"
-                        )
-                    duplicate_qubit_detect_map[reg_name].add(idx)
-
         def _process_classical_arg(formal_arg, actual_arg, actual_arg_name):
             """
             Process the classical argument for a function call.
@@ -1861,53 +1217,37 @@ def _process_classical_arg(formal_arg, actual_arg, actual_arg_name):
                 Qasm3ConversionError: If the actual argument is a qubit register instead
                                     of a classical argument.
                 Qasm3ConversionError: If the actual argument is an undefined variable.
-
-            Notes:
-                - This method is responsible for validating and processing the classical argument
-                  for a function call.
-                - It checks if the actual argument is a qubit register instead of a classical
-                  argument, and raises an error if so.
-                - It also checks if the actual argument is an undefined variable, and raises
-                  an error if so.
-                - Silent casting is performed during assignment validation.
-                - The parent scope must have a well-defined actual argument for the function
-                  call to reach this stage.
-                - The method adds a copy of the actual argument from the parent scope to the
-                  current scope for the declaration of the formal argument.
-                - After the assignment, the method removes the copy of the actual argument
-                  from the current scope.
             """
             # 1. variable mapping is equivalent to declaring the variable
             #     with the formal argument name and doing classical assignment
             #     in the scope of the function
-
-            # NOTE: actual_argument can also be an EXPRESSION
-            # Better to just evaluate that expression and assign that value later to
-            # the formal argument
             if actual_arg_name:
                 if actual_arg_name in self._global_qreg_size_map:
-                    self._print_err_location(statement.span)
-                    raise Qasm3ConversionError(
+                    raise_qasm3_error(
                         f"Expecting classical argument for '{formal_arg.name.name}'. "
-                        f"Qubit register '{actual_arg_name}' found for function '{fn_name}'"
+                        f"Qubit register '{actual_arg_name}' found for function '{fn_name}'",
+                        span=statement.span,
                     )
 
                 # 2. as we have pushed the scope for fn, we need to check in parent
                 #    scope for argument validation
-                if not self._check_in_scope(actual_arg_name, self._get_curr_scope()):
-                    self._print_err_location(statement.span)
-                    raise Qasm3ConversionError(
-                        f"Undefined variable '{actual_arg_name}' used for function '{fn_name}'"
+                if not self._check_in_scope(actual_arg_name):
+                    raise_qasm3_error(
+                        f"Undefined variable '{actual_arg_name}' used for function '{fn_name}'",
+                        span=statement.span,
                     )
 
-            actual_arg_value = self._evaluate_expression(actual_arg)
+            # NOTE: actual_argument can also be an EXPRESSION
+            # Better to just evaluate that expression and assign that value later to
+            # the formal argument
+            actual_arg_value = Qasm3ExprEvaluator.evaluate_expression(actual_arg)
 
             # save this value to be updated later in scope
             classical_vars.append(
                 Variable(
                     formal_arg.name.name,
                     formal_arg.type,
-                    self._evaluate_expression(formal_arg.type.size),
+                    Qasm3ExprEvaluator.evaluate_expression(formal_arg.type.size),
                     None,
                     actual_arg_value,
                     False,
@@ -1932,19 +1272,21 @@ def _process_quantum_arg(formal_arg, actual_arg, formal_reg_name, actual_arg_nam
                                       if the actual argument is not a qubit register.
 
             """
-            formal_qubit_size = self._evaluate_expression(
-                formal_arg.size, reqd_type=Qasm3IntType, const_expr=True
+            formal_qubit_size = Qasm3ExprEvaluator.evaluate_expression(
+                formal_arg.size, reqd_type=qasm3_ast.IntType, const_expr=True
             )
             if formal_qubit_size is None:
                 formal_qubit_size = 1
             formal_qreg_size_map[formal_reg_name] = formal_qubit_size
 
             # we expect that actual arg is qubit type only
+            # note that we ONLY check in global scope as
+            # we always map the qubit arguments to the global scope
             if actual_arg_name not in self._global_qreg_size_map:
-                self._print_err_location(statement.span)
-                raise Qasm3ConversionError(
-                    f"Expecting qubit argument for '{formal_reg_name}'."
-                    f" Qubit register '{actual_arg_name}' not found for function '{fn_name}'"
+                raise_qasm3_error(
+                    f"Expecting qubit argument for '{formal_reg_name}'. "
+                    f"Qubit register '{actual_arg_name}' not found for function '{fn_name}'",
+                    span=statement.span,
                 )
             self._label_scope_level[self._curr_scope].add(formal_reg_name)
 
@@ -1953,29 +1295,44 @@ def _process_quantum_arg(formal_arg, actual_arg, formal_reg_name, actual_arg_nam
             )
 
             if formal_qubit_size != actual_qubits_size:
-                self._print_err_location(statement.span)
-                raise Qasm3ConversionError(
+                raise_qasm3_error(
                     f"Qubit register size mismatch for function '{fn_name}'. "
                     f"Expected {formal_qubit_size} in variable '{formal_reg_name}' "
-                    f"but got {actual_qubits_size}"
+                    f"but got {actual_qubits_size}",
+                    span=statement.span,
                 )
 
             quantum_vars.append(
-                Variable(formal_reg_name, QubitDeclaration, formal_qubit_size, None, None, False)
+                Variable(
+                    formal_reg_name,
+                    qasm3_ast.QubitDeclaration,
+                    formal_qubit_size,
+                    None,
+                    None,
+                    False,
+                )
             )
 
-            _validate_unique_qubits(actual_arg_name, actual_qids)
+            if not Qasm3Validator.validate_unique_qubits(
+                duplicate_qubit_detect_map, actual_arg_name, actual_qids
+            ):
+                raise_qasm3_error(
+                    f"Duplicate qubit argument for register '{actual_arg_name}' "
+                    f"in function call for '{fn_name}'",
+                    span=statement.span,
+                )
+
             for idx, qid in enumerate(actual_qids):
                 qubit_transform_map[(formal_reg_name, idx)] = (actual_arg_name, qid)
 
         for actual_arg, formal_arg in zip(statement.arguments, subroutine_def.arguments):
             actual_arg_name = None
-            if isinstance(actual_arg, Identifier):
+            if isinstance(actual_arg, qasm3_ast.Identifier):
                 actual_arg_name = actual_arg.name
-            elif isinstance(actual_arg, IndexExpression):
+            elif isinstance(actual_arg, qasm3_ast.IndexExpression):
                 actual_arg_name = actual_arg.collection.name
 
-            if isinstance(formal_arg, ClassicalArgument):
+            if isinstance(formal_arg, qasm3_ast.ClassicalArgument):
                 # TODO: add the handling for access : mutable / readonly arrays
                 _process_classical_arg(formal_arg, actual_arg, actual_arg_name)
             else:
@@ -1997,17 +1354,17 @@ def _process_quantum_arg(formal_arg, actual_arg, formal_reg_name, actual_arg_nam
         self._function_qreg_transform_map.append(qubit_transform_map)
 
         for function_op in subroutine_def.body:
-            if isinstance(function_op, ReturnStatement):
+            if isinstance(function_op, qasm3_ast.ReturnStatement):
                 return_statement = copy.deepcopy(function_op)
                 break
             self.visit_statement(copy.deepcopy(function_op))
 
-        return_value = self._evaluate_expression(return_statement.expression)
-        return_value = self._validate_return_statement(
+        return_value = Qasm3ExprEvaluator.evaluate_expression(return_statement.expression)
+        return_value = Qasm3Validator.validate_return_statement(
             subroutine_def, return_statement, return_value
         )
 
-        # remove qubit transformations
+        # remove qubit transformation map
         self._function_qreg_transform_map.pop()
         self._function_qreg_size_map.pop()
 
@@ -2018,33 +1375,14 @@ def _process_quantum_arg(formal_arg, actual_arg, formal_reg_name, actual_arg_nam
 
         return return_value if subroutine_def.return_type is not None else None
 
-    def _visit_while_loop(self, statement: WhileLoop) -> None:
+    def _visit_while_loop(self, statement: qasm3_ast.WhileLoop) -> None:
         pass
 
-    def _extract_values_from_discrete_set(self, discrete_set: DiscreteSet) -> list[int]:
-        """Extract the values from a discrete set.
-
-        Args:
-            discrete_set (DiscreteSet): The discrete set to extract values from.
-
-        Returns:
-            list[int]: The extracted values.
-        """
-        values = []
-        for value in discrete_set.values:
-            if not isinstance(value, IntegerLiteral):
-                self._print_err_location(discrete_set.span)
-                raise Qasm3ConversionError(
-                    f"Unsupported discrete set value {value} in discrete set"
-                )
-            values.append(value.value)
-        return values
-
-    def _visit_alias_statement(self, statement: AliasStatement) -> None:
+    def _visit_alias_statement(self, statement: qasm3_ast.AliasStatement) -> None:
         """Visit an alias statement element.
 
         Args:
-            statement (AliasStatement): The alias statement to visit.
+            statement (qasm3_ast.AliasStatement): The alias statement to visit.
 
         Returns:
             None
@@ -2059,38 +1397,33 @@ def _visit_alias_statement(self, statement: AliasStatement) -> None:
         aliased_reg_size = None
 
         # Alias should not be redeclared earlier as a variable or a constant
-        if self._check_in_scope(alias_reg_name, self._get_curr_scope()):
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(f"Re-declaration of variable '{alias_reg_name}'")
-
+        if self._check_in_scope(alias_reg_name):
+            raise_qasm3_error(f"Re-declaration of variable '{alias_reg_name}'", span=statement.span)
         self._label_scope_level[self._curr_scope].add(alias_reg_name)
 
-        if isinstance(value, Identifier):
+        if isinstance(value, qasm3_ast.Identifier):
             aliased_reg_name = value.name
-        elif isinstance(value, IndexExpression):
+        elif isinstance(value, qasm3_ast.IndexExpression):
             aliased_reg_name = value.collection.name
         else:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(
-                f"Unsupported aliasing {statement} not supported at the moment"
-            )
+            raise_qasm3_error(f"Unsupported aliasing {statement}", span=statement.span)
 
         if aliased_reg_name not in self._global_qreg_size_map:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(f"Qubit register {aliased_reg_name} not found for aliasing")
-
+            raise_qasm3_error(
+                f"Qubit register {aliased_reg_name} not found for aliasing", span=statement.span
+            )
         aliased_reg_size = self._global_qreg_size_map[aliased_reg_name]
-        if isinstance(value, Identifier):  # "let alias = q;"
+        if isinstance(value, qasm3_ast.Identifier):  # "let alias = q;"
             for i in range(aliased_reg_size):
                 self._qubit_labels[f"{alias_reg_name}_{i}"] = self._qubit_labels[
                     f"{aliased_reg_name}_{i}"
                 ]
             alias_reg_size = aliased_reg_size
-        elif isinstance(value, IndexExpression):
-            if isinstance(value.index, DiscreteSet):  # "let alias = q[{0,1}];"
-                qids = self._extract_values_from_discrete_set(value.index)
+        elif isinstance(value, qasm3_ast.IndexExpression):
+            if isinstance(value.index, qasm3_ast.DiscreteSet):  # "let alias = q[{0,1}];"
+                qids = Qasm3Transformer.extract_values_from_discrete_set(value.index)
                 for i, qid in enumerate(qids):
-                    self._validate_register_index(
+                    Qasm3Validator.validate_register_index(
                         qid, self._global_qreg_size_map[aliased_reg_name], qubit=True
                     )
                     self._qubit_labels[f"{alias_reg_name}_{i}"] = self._qubit_labels[
@@ -2098,21 +1431,21 @@ def _visit_alias_statement(self, statement: AliasStatement) -> None:
                     ]
                 alias_reg_size = len(qids)
             elif len(value.index) != 1:  # like "let alias = q[0,1];"?
-                self._print_err_location(statement.span)
-                raise Qasm3ConversionError(
+                raise_qasm3_error(
                     "An index set can be specified by a single integer (signed or unsigned), "
                     "a comma-separated list of integers contained in braces {a,b,c,…}, "
-                    "or a range"
+                    "or a range",
+                    span=statement.span,
                 )
-            elif isinstance(value.index[0], IntegerLiteral):  # "let alias = q[0];"
+            elif isinstance(value.index[0], qasm3_ast.IntegerLiteral):  # "let alias = q[0];"
                 qid = value.index[0].value
-                self._validate_register_index(
+                Qasm3Validator.validate_register_index(
                     qid, self._global_qreg_size_map[aliased_reg_name], qubit=True
                 )
                 self._qubit_labels[f"{alias_reg_name}_0"] = value.index[0].value
                 alias_reg_size = 1
-            elif isinstance(value.index[0], RangeDefinition):  # "let alias = q[0:1:2];"
-                qids = self._get_qubits_from_range_definition(
+            elif isinstance(value.index[0], qasm3_ast.RangeDefinition):  # "let alias = q[0:1:2];"
+                qids = Qasm3Transformer.get_qubits_from_range_definition(
                     value.index[0],
                     aliased_reg_size,
                     is_qubit_reg=True,
@@ -2123,36 +1456,37 @@ def _visit_alias_statement(self, statement: AliasStatement) -> None:
 
         self._global_qreg_size_map[alias_reg_name] = alias_reg_size
 
-        _log.debug("Added labels for aliasing '%s'", target)
+        logger.debug("Added labels for aliasing '%s'", target)
 
-    def _visit_switch_statement(self, statement: SwitchStatement) -> None:
+    def _visit_switch_statement(self, statement: qasm3_ast.SwitchStatement) -> None:
         """Visit a switch statement element.
 
         Args:
-            statement (SwitchStatement): The switch statement to visit.
+            statement (qasm3_ast.SwitchStatement): The switch statement to visit.
 
         Returns:
             None
         """
-
-        # 1. analyse the target - it should ONLY be int, not casted
+        # 1. analyze the target - it should ONLY be int, not casted
         switch_target = statement.target
 
         # either identifier or indexed expression
-        if isinstance(switch_target, Identifier):
+        if isinstance(switch_target, qasm3_ast.Identifier):
             switch_target_name = switch_target.name
         else:
-            switch_target_name, _ = self._analyse_index_expression(switch_target)
+            switch_target_name, _ = Qasm3Analyzer.analyze_index_expression(switch_target)
 
-        if not self._validate_variable_type(switch_target_name, Qasm3IntType):
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(f"Switch target {switch_target_name} must be of type int")
+        if not Qasm3Validator.validate_variable_type(
+            self._get_from_visible_scope(switch_target_name), qasm3_ast.IntType
+        ):
+            raise_qasm3_error(
+                f"Switch target {switch_target_name} must be of type int", span=statement.span
+            )
 
-        switch_target_val = self._evaluate_expression(switch_target)
+        switch_target_val = Qasm3ExprEvaluator.evaluate_expression(switch_target)
 
         if len(statement.cases) == 0:
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError("Switch statement must have at least one case")
+            raise_qasm3_error("Switch statement must have at least one case", span=statement.span)
 
         # 2. handle the cases of the switch stmt
         #    each element in the list of the values
@@ -2165,7 +1499,7 @@ def _evaluate_case(statements):
             self._push_scope({})
 
             for stmt in statements:
-                self._validate_statement_type(SWITCH_BLACKLIST_STMTS, stmt, "switch")
+                Qasm3Validator.validate_statement_type(SWITCH_BLACKLIST_STMTS, stmt, "switch")
                 self.visit_statement(stmt)
 
             self._pop_scope()
@@ -2179,14 +1513,13 @@ def _evaluate_case(statements):
                 # 3. evaluate and verify that it is a const_expression
                 # using vars only within the scope AND each component is either a
                 # literal OR type int
-                case_val = self._evaluate_expression(
-                    case_expr, const_expr=True, reqd_type=Qasm3IntType
+                case_val = Qasm3ExprEvaluator.evaluate_expression(
+                    case_expr, const_expr=True, reqd_type=qasm3_ast.IntType
                 )
 
                 if case_val in seen_values:
-                    self._print_err_location(case_expr.span)
-                    raise Qasm3ConversionError(
-                        f"Duplicate case value {case_val} in switch statement"
+                    raise_qasm3_error(
+                        f"Duplicate case value {case_val} in switch statement", span=case_expr.span
                     )
 
                 seen_values.add(case_val)
@@ -2203,53 +1536,46 @@ def _evaluate_case(statements):
             default_stmts = statement.default.statements
             _evaluate_case(default_stmts)
 
-    # pylint: disable-next=too-many-branches
-    def visit_statement(self, statement: Statement) -> None:
+    def visit_statement(self, statement: qasm3_ast.Statement) -> None:
         """Visit a statement element.
 
         Args:
-            statement (Statement): The statement to visit.
+            statement (qasm3_ast.Statement): The statement to visit.
 
         Returns:
             None
         """
-        _log.debug("Visiting statement '%s'", str(statement))
-        if isinstance(statement, Include):
-            pass
-        elif isinstance(statement, QuantumMeasurementStatement):
-            self._visit_measurement(statement)
-        elif isinstance(statement, QuantumReset):
-            self._visit_reset(statement)
-        elif isinstance(statement, QuantumBarrier):
-            self._visit_barrier(statement)
-        elif isinstance(statement, QuantumGateDefinition):
-            self._visit_gate_definition(statement)
-        elif isinstance(statement, QuantumGate):
-            self._visit_generic_gate_operation(statement)
-        elif isinstance(statement, ClassicalDeclaration):
-            self._visit_classical_declaration(statement)
-        elif isinstance(statement, ClassicalAssignment):
-            self._visit_classical_assignment(statement)
-        elif isinstance(statement, ConstantDeclaration):
-            self._visit_constant_declaration(statement)
-        elif isinstance(statement, BranchingStatement):
-            self._visit_branching_statement(statement)
-        elif isinstance(statement, ForInLoop):
-            self._visit_forin_loop(statement)
-        elif isinstance(statement, AliasStatement):
-            self._visit_alias_statement(statement)
-        elif isinstance(statement, SwitchStatement):
-            self._visit_switch_statement(statement)
-        elif isinstance(statement, SubroutineDefinition):
-            self._visit_subroutine_definition(statement)
-        elif isinstance(statement, ExpressionStatement):
-            self._visit_function_call(statement.expression)
-        elif isinstance(statement, IODeclaration):
-            raise NotImplementedError("OpenQASM 3 IO declarations not yet supported")
+        logger.debug("Visiting statement '%s'", str(statement))
+
+        visit_map = {
+            qasm3_ast.Include: lambda x: None,  # No operation
+            qasm3_ast.QuantumMeasurementStatement: self._visit_measurement,
+            qasm3_ast.QuantumReset: self._visit_reset,
+            qasm3_ast.QuantumBarrier: self._visit_barrier,
+            qasm3_ast.QuantumGateDefinition: self._visit_gate_definition,
+            qasm3_ast.QuantumGate: self._visit_generic_gate_operation,
+            qasm3_ast.ClassicalDeclaration: self._visit_classical_declaration,
+            qasm3_ast.ClassicalAssignment: self._visit_classical_assignment,
+            qasm3_ast.ConstantDeclaration: self._visit_constant_declaration,
+            qasm3_ast.BranchingStatement: self._visit_branching_statement,
+            qasm3_ast.ForInLoop: self._visit_forin_loop,
+            qasm3_ast.AliasStatement: self._visit_alias_statement,
+            qasm3_ast.SwitchStatement: self._visit_switch_statement,
+            qasm3_ast.SubroutineDefinition: self._visit_subroutine_definition,
+            qasm3_ast.ExpressionStatement: lambda x: self._visit_function_call(x.expression),
+            qasm3_ast.IODeclaration: lambda x: (_ for _ in ()).throw(
+                NotImplementedError("OpenQASM 3 IO declarations not yet supported")
+            ),
+        }
+
+        visitor_function = visit_map.get(type(statement))
+
+        if visitor_function:
+            visitor_function(statement)
         else:
-            # TODO : extend this
-            self._print_err_location(statement.span)
-            raise Qasm3ConversionError(f"Unsupported statement of type {type(statement)}")
+            raise_qasm3_error(
+                f"Unsupported statement of type {type(statement)}", span=statement.span
+            )
 
     def ir(self) -> str:
         return str(self._module)


Test patch:
diff --git a/tests/qasm3_qir/converter/test_switch.py b/tests/qasm3_qir/converter/test_switch.py
index 0e3f616..089aa0c 100644
--- a/tests/qasm3_qir/converter/test_switch.py
+++ b/tests/qasm3_qir/converter/test_switch.py
@@ -206,15 +206,16 @@ def test_nested_switch():
     case 1,3,5,7 {
         int j = 4; // definition inside scope
         switch(j) {
-        case 1,3,5,7 {
-            x q; 
-        }
-        case 2,4,6,8 {
-            y q; // this will be executed
-        }
-        default {
-            z q;
-        }
+            case 1,3,5,7 {
+                x q; 
+            }
+            case 2,4,6,8 {
+                j = 5; // assignment inside scope
+                y q; // this will be executed
+            }
+            default {
+                z q;
+            }
         }
     }
     case 2,4,6,8 {
diff --git a/tests/qasm3_qir/fixtures/gates.py b/tests/qasm3_qir/fixtures/gates.py
index 8e4cbb1..931b3a9 100644
--- a/tests/qasm3_qir/fixtures/gates.py
+++ b/tests/qasm3_qir/fixtures/gates.py
@@ -16,7 +16,7 @@
 
 import pytest
 
-from qbraid_qir.qasm3.oq3_maps import (
+from qbraid_qir.qasm3.maps import (
     PYQIR_ONE_QUBIT_OP_MAP,
     PYQIR_ONE_QUBIT_ROTATION_MAP,
     PYQIR_THREE_QUBIT_OP_MAP,
@@ -292,7 +292,7 @@ def test_fixture():
         qubit[2] q1;
         custom_gate(0.5) q1;  // parameter count mismatch
         """,
-        "Parameter count mismatch for gate custom_gate. Expected 2 but got 1 .*",
+        "Parameter count mismatch for gate custom_gate: expected 2 arguments, but got 1 instead.",
     ),
     "qubit_mismatch": (
         """
@@ -307,7 +307,7 @@ def test_fixture():
         qubit[3] q1;
         custom_gate(0.5, 0.5) q1;  // qubit count mismatch
         """,
-        "Qubit count mismatch for gate custom_gate. Expected 2 but got 3 .*",
+        "Qubit count mismatch for gate custom_gate: expected 2 qubits, but got 3 instead.",
     ),
     "indexing_not_supported": (
         """
diff --git a/tests/qasm3_qir/fixtures/subroutines.py b/tests/qasm3_qir/fixtures/subroutines.py
index e6a4044..a32a145 100644
--- a/tests/qasm3_qir/fixtures/subroutines.py
+++ b/tests/qasm3_qir/fixtures/subroutines.py
@@ -195,7 +195,7 @@ def my_function(qubit[3] p, qubit[1] q) {
         qubit[4] q;
         my_function(q[0:3], q[2]);
         """,
-        r"Duplicate qubit argument 'q\[2\]' in function call for 'my_function'",
+        r"Duplicate qubit argument for register 'q' in function call for 'my_function'",
     ),
     "undefined_variable_in_actual_arg": (
         """
diff --git a/tests/qir_utils.py b/tests/qir_utils.py
index 8af63bc..9ece70a 100644
--- a/tests/qir_utils.py
+++ b/tests/qir_utils.py
@@ -25,7 +25,7 @@
     required_num_results,
 )
 
-from qbraid_qir.qasm3.oq3_maps import CONSTANTS_MAP
+from qbraid_qir.qasm3.maps import CONSTANTS_MAP
 
 
 def double_to_hex(f):