Repository: qBraid/pyqasm

Issue:
[FEATURE] Add `ctrl` modifier support in external gates
### Feature Description

Add `ctrl` modifier support in external gates

### Implementation (Optional)

Modify the `_visit_external_gate_operation` implementation to fix [this](https://github.com/qBraid/pyqasm/pull/121/files/6ec412a0debe53ed3d0095568d14d7f8307c430c#diff-802ca57af29fc92ca8eb2f536fb5217c61c874c4c35bd39b7fd82512ee0e9a84R900) todo

Discussion:

Solution patch:
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..4c78e9a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,4 @@
+# Ref : https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings#per-repository-settings
+
+# Set line endings to lf 
+* text eol=lf
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b64dc6c..731cb81 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -86,6 +86,7 @@ if (c[0] == false) {
   }
 }
 ```
+- Add formatting check for Unix style line endings i.e. `\n`. For any other line endings, errors are raised. ([#130](https://github.com/qBraid/pyqasm/pull/130))
 
 ### Improved / Modified
  - Refactored the initialization of `QasmModule` to remove default include statements. Only user supplied include statements are now added to the generated QASM code ([#86](https://github.com/qBraid/pyqasm/pull/86))
diff --git a/pyproject.toml b/pyproject.toml
index 739e976..dbcbaaf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -41,7 +41,7 @@ dependencies = ["numpy", "openqasm3[parser]>=1.0.0,<2.0.0"]
 [project.optional-dependencies]
 cli = ["typer>=0.12.1", "rich>=10.11.0", "typing-extensions"]
 test = ["pytest", "pytest-cov"]
-lint = ["black", "isort", "pylint", "mypy", "qbraid-cli>=0.8.5"]
+lint = ["black", "isort>=6.0.0", "pylint", "mypy", "qbraid-cli>=0.8.5"]
 docs = ["sphinx>=7.3.7,<8.2.0", "sphinx-autodoc-typehints>=1.24,<3.1", "sphinx-rtd-theme>=2.0.0,<4.0.0", "docutils<0.22", "sphinx-copybutton"]
 
 [tool.setuptools_scm]
diff --git a/src/pyqasm/decomposer.py b/src/pyqasm/decomposer.py
new file mode 100644
index 0000000..fd9b337
--- /dev/null
+++ b/src/pyqasm/decomposer.py
@@ -0,0 +1,161 @@
+# Copyright (C) 2025 qBraid
+#
+# This file is part of PyQASM
+#
+# PyQASM 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 PyQASM, as per Section 15 of the GPL v3.
+
+"""
+Definition of the Decomposer class
+"""
+
+import openqasm3.ast as qasm3_ast
+from openqasm3.ast import BranchingStatement, QuantumGate
+
+from pyqasm.exceptions import RebaseError
+from pyqasm.maps.decomposition_rules import DECOMPOSITION_RULES, AppliedQubit
+from pyqasm.maps.gates import BASIS_GATE_MAP
+
+
+class Decomposer:
+    """
+    Class to decompose the gates based on the target basis set.
+    """
+
+    @classmethod
+    def process_gate_statement(cls, gate_name, statement, target_basis_set):
+        """Process the gate statement based on the target basis set.
+
+        Args:
+            gate_name: The name of the gate to process.
+            statement: The statement to process.
+            target_basis_set: The target basis set to rebase the module to.
+
+        Returns:
+            list: The processed gates based on the target basis set.
+        """
+        decomposition_rules = DECOMPOSITION_RULES[target_basis_set]
+        target_basis_gate_list = BASIS_GATE_MAP[target_basis_set]
+
+        processed_gates_list = []
+
+        if gate_name in target_basis_gate_list:
+            # Keep the gate as is
+            processed_gates_list = [statement]
+        elif gate_name in decomposition_rules:
+            # Decompose the gates
+            processed_gates_list = cls._get_decomposed_gates(
+                decomposition_rules, statement, gate_name
+            )
+        elif gate_name in {"rx", "ry", "rz"}:
+            # Approximate parameterized gates using Solovay-Kitaev
+            # Example -
+            # approx_gates = solovay_kitaev_algo(
+            #     gate_name, statement.arguments[0].value, accuracy=0.01
+            # )
+            # return approx_gates
+            pass
+        else:
+            # Raise an error if the gate is not supported in the target basis set
+            error = f"Gate '{gate_name}' is not supported in the '{target_basis_set} set'."
+            raise RebaseError(error)
+
+        return processed_gates_list
+
+    @classmethod
+    def process_branching_statement(cls, branching_statement, target_basis_set):
+        """Process the branching statement based on the target basis set.
+
+        Args:
+            branching_statement: The branching statement to process.
+            target_basis_set: The target basis set to rebase the module to.
+
+        Returns:
+            BranchingStatement: The processed branching statement based on the target basis set.
+        """
+        if_block = []
+        else_block = []
+
+        for statement in branching_statement.if_block:
+            if isinstance(statement, QuantumGate):
+                gate_name = statement.name.name
+                processed_gates_list = cls.process_gate_statement(
+                    gate_name, statement, target_basis_set
+                )
+                if_block.extend(processed_gates_list)
+            elif isinstance(statement, BranchingStatement):
+                if_block.append(cls.process_branching_statement(statement, target_basis_set))
+            else:
+                if_block.append(statement)
+
+        for statement in branching_statement.else_block:
+            if isinstance(statement, QuantumGate):
+                gate_name = statement.name.name
+                processed_gates_list = cls.process_gate_statement(
+                    gate_name, statement, target_basis_set
+                )
+                else_block.extend(processed_gates_list)
+            elif isinstance(statement, BranchingStatement):
+                else_block.append(cls.process_branching_statement(statement, target_basis_set))
+            else:
+                else_block.append(statement)
+
+        return BranchingStatement(
+            condition=branching_statement.condition, if_block=if_block, else_block=else_block
+        )
+
+    @classmethod
+    def _get_decomposed_gates(cls, decomposition_rules, statement, gate):
+        """Apply the decomposed gates based on the decomposition rules.
+
+        Args:
+            decomposition_rules: The decomposition rules to apply.
+            statement: The statement to apply the decomposition rules to.
+            gate: The name of the gate to apply the decomposition rules to.
+
+        Returns:
+            list: The decomposed gates to be applied.
+        """
+        decomposed_gates = []
+
+        for rule in decomposition_rules[gate]:
+            qubits = cls._get_qubits_for_gate(statement.qubits, rule)
+            arguments = [qasm3_ast.FloatLiteral(value=rule["param"])] if "param" in rule else []
+
+            new_gate = qasm3_ast.QuantumGate(
+                modifiers=[],
+                name=qasm3_ast.Identifier(name=rule["gate"]),
+                arguments=arguments,
+                qubits=qubits,
+            )
+
+            decomposed_gates.append(new_gate)
+        return decomposed_gates
+
+    @classmethod
+    def _get_qubits_for_gate(cls, qubits, rule):
+        """
+        Determines the order of qubits to be used for a gate operation based on the provided rule.
+
+        Args:
+            qubits: The qubits to be used for the gate operation.
+            rule: The decomposition rule to apply.
+
+        Returns:
+            list: The ordered qubits to be used for the gate operation.
+        """
+        if "controll_bit" in rule:
+            if rule["controll_bit"] == AppliedQubit.QUBIT1:
+                qubits = [qubits[0], qubits[1]]
+            else:
+                qubits = [qubits[1], qubits[0]]
+
+        elif "target_bit" in rule:
+            if rule["target_bit"] == AppliedQubit.QUBIT1:
+                qubits = [qubits[0]]
+            else:
+                qubits = [qubits[1]]
+        return qubits
diff --git a/src/pyqasm/elements.py b/src/pyqasm/elements.py
index 2ce76b9..47d5fca 100644
--- a/src/pyqasm/elements.py
+++ b/src/pyqasm/elements.py
@@ -98,3 +98,13 @@ class Variable:  # pylint: disable=too-many-instance-attributes
     is_constant: bool = False
     is_register: bool = False
     readonly: bool = False
+
+
+class BasisSet(Enum):
+    """
+    Enum for the different basis sets in Qasm.
+    """
+
+    DEFAULT = 0
+    ROTATIONAL_CX = 1
+    CLIFFORD_T = 2
diff --git a/src/pyqasm/exceptions.py b/src/pyqasm/exceptions.py
index 31039d8..ee24507 100644
--- a/src/pyqasm/exceptions.py
+++ b/src/pyqasm/exceptions.py
@@ -32,6 +32,10 @@ class UnrollError(PyQasmError):
     """Exception raised when a OpenQASM program fails unrolling."""
 
 
+class RebaseError(PyQasmError):
+    """Exception raised when a OpenQASM program fails to rebase into target basis set."""
+
+
 class QasmParsingError(QASM3ParsingError):
     """An error raised by the AST visitor during the AST-generation phase.  This is raised in cases
     where the given program could not be correctly parsed."""
diff --git a/src/pyqasm/expressions.py b/src/pyqasm/expressions.py
index 705540c..7c6e75a 100644
--- a/src/pyqasm/expressions.py
+++ b/src/pyqasm/expressions.py
@@ -29,7 +29,11 @@
     IntegerLiteral,
 )
 from openqasm3.ast import IntType as Qasm3IntType
-from openqasm3.ast import SizeOf, Statement, UnaryExpression
+from openqasm3.ast import (
+    SizeOf,
+    Statement,
+    UnaryExpression,
+)
 
 from pyqasm.analyzer import Qasm3Analyzer
 from pyqasm.exceptions import ValidationError, raise_qasm3_error
diff --git a/src/pyqasm/maps/decomposition_rules.py b/src/pyqasm/maps/decomposition_rules.py
new file mode 100644
index 0000000..b2421eb
--- /dev/null
+++ b/src/pyqasm/maps/decomposition_rules.py
@@ -0,0 +1,102 @@
+# Copyright (C) 2025 qBraid
+#
+# This file is part of PyQASM
+#
+# PyQASM 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 PyQASM, as per Section 15 of the GPL v3.
+
+"""
+Definition of the decomposition rules for the gates in the basis sets.
+"""
+from enum import Enum
+
+from pyqasm.elements import BasisSet
+from pyqasm.maps.expressions import CONSTANTS_MAP
+
+
+class AppliedQubit(Enum):
+    """Enum to represent the qubits that are involved in the decomposition of a gate."""
+
+    QUBIT1 = 0  # Control qubit
+    QUBIT2 = 1  # Target qubit
+
+
+# Decomposition rules for the gates in the basis sets.
+# The rules are defined as a dictionary where the key is the gate name.
+# The value is a list of dictionaries.
+# Each dictionary in the list represents a step in the decomposition of the gate.
+# Each step is defined by a dictionary with the following keys:
+#     - gate: The name of the gate to be applied.
+#     - param: The parameter of the gate.
+#     - target_bit: The target qubit of the gate.
+#                   This key is only used for decomposition of gates that operate on two qubits.
+#     - controll_bit: The control qubit of the gate.
+#                   This key is only used for gates that operate on two qubits.
+#
+DECOMPOSITION_RULES = {
+    BasisSet.ROTATIONAL_CX: {
+        "x": [{"gate": "rx", "param": CONSTANTS_MAP["pi"]}],
+        "y": [{"gate": "ry", "param": CONSTANTS_MAP["pi"]}],
+        "z": [{"gate": "rz", "param": CONSTANTS_MAP["pi"]}],
+        "h": [
+            {"gate": "ry", "param": CONSTANTS_MAP["pi"] / 2},
+            {"gate": "rx", "param": CONSTANTS_MAP["pi"]},
+        ],
+        "s": [{"gate": "rz", "param": CONSTANTS_MAP["pi"] / 2}],
+        "t": [{"gate": "rz", "param": CONSTANTS_MAP["pi"] / 4}],
+        "sx": [{"gate": "rx", "param": CONSTANTS_MAP["pi"] / 2}],
+        "sdg": [{"gate": "rz", "param": -CONSTANTS_MAP["pi"] / 2}],
+        "tdg": [{"gate": "rz", "param": -CONSTANTS_MAP["pi"] / 4}],
+        "cz": [
+            {"gate": "ry", "param": CONSTANTS_MAP["pi"] / 2, "target_bit": AppliedQubit.QUBIT2},
+            {"gate": "rx", "param": CONSTANTS_MAP["pi"], "target_bit": AppliedQubit.QUBIT2},
+            {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT2},
+            {"gate": "ry", "param": CONSTANTS_MAP["pi"] / 2, "target_bit": AppliedQubit.QUBIT2},
+            {"gate": "rx", "param": CONSTANTS_MAP["pi"], "target_bit": AppliedQubit.QUBIT2},
+        ],
+        "swap": [
+            {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT2},
+            {"gate": "cx", "controll_bit": AppliedQubit.QUBIT2, "target_bit": AppliedQubit.QUBIT1},
+            {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT2},
+        ],
+    },
+    BasisSet.CLIFFORD_T: {
+        "x": [{"gate": "h"}, {"gate": "s"}, {"gate": "s"}, {"gate": "h"}],
+        "y": [
+            {"gate": "s"},
+            {"gate": "s"},
+            {"gate": "h"},
+            {"gate": "s"},
+            {"gate": "s"},
+            {"gate": "h"},
+        ],
+        "z": [{"gate": "s"}, {"gate": "s"}],
+        "sx": [
+            {"gate": "s"},
+            {"gate": "s"},
+            {"gate": "s"},
+            {"gate": "h"},
+            {"gate": "s"},
+            {"gate": "s"},
+            {"gate": "s"},
+        ],
+        "cz": [
+            {"gate": "h", "target_bit": AppliedQubit.QUBIT2},
+            {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT2},
+            {"gate": "h", "target_bit": AppliedQubit.QUBIT2},
+        ],
+        "swap": [
+            {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT2},
+            {"gate": "cx", "controll_bit": AppliedQubit.QUBIT2, "target_bit": AppliedQubit.QUBIT1},
+            {"gate": "cx", "controll_bit": AppliedQubit.QUBIT1, "target_bit": AppliedQubit.QUBIT2},
+        ],
+    },
+}
+
+# """TODO: Implement the Solovay-Kitaev algorithm"""
+#
+# def solovay_kitaev_algo(gate_name, param, accuracy):
+#     pass
diff --git a/src/pyqasm/maps/gates.py b/src/pyqasm/maps/gates.py
index fbf3d7e..8228916 100644
--- a/src/pyqasm/maps/gates.py
+++ b/src/pyqasm/maps/gates.py
@@ -21,7 +21,7 @@
 import numpy as np
 from openqasm3.ast import FloatLiteral, Identifier, IndexedIdentifier, QuantumGate, QuantumPhase
 
-from pyqasm.elements import InversionOp
+from pyqasm.elements import BasisSet, InversionOp
 from pyqasm.exceptions import ValidationError
 from pyqasm.linalg import kak_decomposition_angles
 from pyqasm.maps.expressions import CONSTANTS_MAP
@@ -1087,6 +1087,30 @@ def two_qubit_gate_op(
     "c4x": c4x_gate,
 }
 
+BASIS_GATE_MAP = {
+    # default basis set is the gate set of the stdgates.inc library file
+    BasisSet.DEFAULT: {
+        "id",
+        "rx",
+        "ry",
+        "rz",
+        "h",
+        "x",
+        "y",
+        "z",
+        "s",
+        "sx",
+        "t",
+        "sdg",
+        "tdg",
+        "cx",
+        "cz",
+        "swap",
+    },
+    BasisSet.ROTATIONAL_CX: {"rx", "ry", "rz", "cx"},
+    BasisSet.CLIFFORD_T: {"h", "t", "s", "cx", "tdg", "sdg"},
+}
+
 
 def map_qasm_op_to_callable(op_name: str) -> tuple[Callable, int]:
     """
@@ -1167,3 +1191,48 @@ def map_qasm_inv_op_to_callable(op_name: str):
             InversionOp.INVERT_ROTATION,
         )
     raise ValidationError(f"Unsupported / undeclared QASM operation: {op_name}")
+
+
+CTRL_GATE_MAP = {
+    "x": "cx",
+    "y": "cy",
+    "z": "cz",
+    "rx": "crx",
+    "ry": "cry",
+    "rz": "crz",
+    "p": "cp",
+    "h": "ch",
+    "u": "cu",
+    "swap": "cswap",
+    "cx": "ccx",
+}
+
+
+def map_qasm_ctrl_op_to_callable(op_name: str, ctrl_count: int):
+    """
+    Map a controlled QASM operation to a callable.
+
+    Args:
+        op_name (str): The QASM operation name.
+        ctrl_count (int): The number of control qubits.
+
+    Returns:
+        tuple: A tuple containing the callable and the number of qubits the operation acts on.
+    """
+
+    ctrl_op_name, c = op_name, ctrl_count
+    while c > 0 and ctrl_op_name in CTRL_GATE_MAP:
+        ctrl_op_name = CTRL_GATE_MAP[ctrl_op_name]
+        c -= 1
+    if c == 0:
+        if ctrl_op_name in ONE_QUBIT_OP_MAP:
+            return ONE_QUBIT_OP_MAP[ctrl_op_name], 1
+        if ctrl_op_name in TWO_QUBIT_OP_MAP:
+            return TWO_QUBIT_OP_MAP[ctrl_op_name], 2
+        if ctrl_op_name in THREE_QUBIT_OP_MAP:
+            return THREE_QUBIT_OP_MAP[ctrl_op_name], 3
+
+    # TODO: decompose controls if not built in
+    raise ValidationError(
+        f"Unsupported controlled QASM operation: {op_name} with {ctrl_count} controls"
+    )
diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py
index 0cca109..c0a796d 100644
--- a/src/pyqasm/modules/base.py
+++ b/src/pyqasm/modules/base.py
@@ -17,12 +17,14 @@
 from typing import Optional
 
 import openqasm3.ast as qasm3_ast
-from openqasm3.ast import Program
+from openqasm3.ast import BranchingStatement, Program, QuantumGate
 
 from pyqasm.analyzer import Qasm3Analyzer
+from pyqasm.decomposer import Decomposer
 from pyqasm.elements import ClbitDepthNode, QubitDepthNode
 from pyqasm.exceptions import UnrollError, ValidationError
 from pyqasm.maps import QUANTUM_STATEMENTS
+from pyqasm.maps.decomposition_rules import DECOMPOSITION_RULES
 from pyqasm.visitor import QasmVisitor
 
 
@@ -525,6 +527,55 @@ def unroll(self, **kwargs):
             self._unrolled_ast = Program(statements=[], version=self.original_program.version)
             raise err
 
+    def rebase(self, target_basis_set, in_place=True):
+        """Rebase the AST to use a specified target basis set.
+
+        Will unroll the module if not already done.
+
+        Args:
+            target_basis_set: The target basis set to rebase the module to.
+            in_place (bool): Flag to indicate if the rebase operation should be done in place.
+
+        Returns:
+            QasmModule: The module with the gates rebased to the target basis set.
+        """
+        if target_basis_set not in DECOMPOSITION_RULES:
+            raise ValueError(f"Target basis set '{target_basis_set}' is not defined.")
+
+        qasm_module = self if in_place else self.copy()
+
+        if len(qasm_module._unrolled_ast.statements) == 0:
+            qasm_module.unroll()
+
+        rebased_statements = []
+
+        for statement in qasm_module._unrolled_ast.statements:
+            if isinstance(statement, QuantumGate):
+                gate_name = statement.name.name
+
+                # Decompose the gate
+                processed_gates_list = Decomposer.process_gate_statement(
+                    gate_name, statement, target_basis_set
+                )
+                for processed_gate in processed_gates_list:
+                    rebased_statements.append(processed_gate)
+
+            elif isinstance(statement, BranchingStatement):
+                # Recursively process the if_block and else_block
+
+                rebased_statements.append(
+                    Decomposer.process_branching_statement(statement, target_basis_set)
+                )
+
+            else:
+                # Non-gate statements are directly appended
+                rebased_statements.append(statement)
+
+        # Replace the unrolled AST with the rebased one
+        qasm_module._unrolled_ast.statements = rebased_statements
+
+        return qasm_module
+
     def __str__(self) -> str:
         """Return the string representation of the QASM program
 
diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py
index c64d56e..8da003e 100644
--- a/src/pyqasm/visitor.py
+++ b/src/pyqasm/visitor.py
@@ -29,7 +29,11 @@
 from pyqasm.expressions import Qasm3ExprEvaluator
 from pyqasm.maps import SWITCH_BLACKLIST_STMTS
 from pyqasm.maps.expressions import ARRAY_TYPE_MAP, CONSTANTS_MAP, MAX_ARRAY_DIMENSIONS
-from pyqasm.maps.gates import map_qasm_inv_op_to_callable, map_qasm_op_to_callable
+from pyqasm.maps.gates import (
+    map_qasm_ctrl_op_to_callable,
+    map_qasm_inv_op_to_callable,
+    map_qasm_op_to_callable,
+)
 from pyqasm.subroutines import Qasm3SubroutineProcessor
 from pyqasm.transformer import Qasm3Transformer
 from pyqasm.validator import Qasm3Validator
@@ -630,7 +634,7 @@ def _unroll_multiple_target_qubits(
             The list of all targets that the unrolled gate should act on.
         """
         op_qubits = self._get_op_bits(operation, self._global_qreg_size_map)
-        if len(op_qubits) % gate_qubit_count != 0:
+        if len(op_qubits) <= 0 or len(op_qubits) % gate_qubit_count != 0:
             raise_qasm3_error(
                 f"Invalid number of qubits {len(op_qubits)} for operation {operation.name.name}",
                 span=operation.span,
@@ -642,7 +646,10 @@ def _unroll_multiple_target_qubits(
         return qubit_subsets
 
     def _broadcast_gate_operation(
-        self, gate_function: Callable, all_targets: list[list[qasm3_ast.IndexedIdentifier]]
+        self,
+        gate_function: Callable,
+        all_targets: list[list[qasm3_ast.IndexedIdentifier]],
+        ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None,
     ) -> list[qasm3_ast.QuantumGate]:
         """Broadcasts the application of a gate onto multiple sets of target qubits.
 
@@ -657,11 +664,17 @@ def _broadcast_gate_operation(
             List of all executed gates.
         """
         result = []
+        if ctrls is None:
+            ctrls = []
         for targets in all_targets:
-            result.extend(gate_function(*targets))
+            result.extend(gate_function(*ctrls, *targets))
         return result
 
-    def _update_qubit_depth_for_gate(self, all_targets: list[list[qasm3_ast.IndexedIdentifier]]):
+    def _update_qubit_depth_for_gate(
+        self,
+        all_targets: list[list[qasm3_ast.IndexedIdentifier]],
+        ctrls: list[qasm3_ast.IndexedIdentifier],
+    ):
         """Updates the depth of the circuit after applying a broadcasted gate.
 
         Args:
@@ -672,19 +685,26 @@ def _update_qubit_depth_for_gate(self, all_targets: list[list[qasm3_ast.IndexedI
         """
         for qubit_subset in all_targets:
             max_involved_depth = 0
-            for qubit in qubit_subset:
-                qubit_name, qubit_id = qubit.name.name, qubit.indices[0][0].value  # type: ignore
-                qubit_node = self._module._qubit_depths[(qubit_name, qubit_id)]
+            for qubit in qubit_subset + ctrls:
+                assert isinstance(qubit.indices[0], list)
+                _qid_ = qubit.indices[0][0]
+                qubit_id = Qasm3ExprEvaluator.evaluate_expression(_qid_)[0]  # type: ignore
+                qubit_node = self._module._qubit_depths[(qubit.name.name, qubit_id)]
                 qubit_node.num_gates += 1
                 max_involved_depth = max(max_involved_depth, qubit_node.depth + 1)
 
-            for qubit in qubit_subset:
-                qubit_name, qubit_id = qubit.name.name, qubit.indices[0][0].value  # type: ignore
-                qubit_node = self._module._qubit_depths[(qubit_name, qubit_id)]
+            for qubit in qubit_subset + ctrls:
+                assert isinstance(qubit.indices[0], list)
+                _qid_ = qubit.indices[0][0]
+                qubit_id = Qasm3ExprEvaluator.evaluate_expression(_qid_)[0]  # type: ignore
+                qubit_node = self._module._qubit_depths[(qubit.name.name, qubit_id)]
                 qubit_node.depth = max_involved_depth
 
     def _visit_basic_gate_operation(  # pylint: disable=too-many-locals
-        self, operation: qasm3_ast.QuantumGate, inverse: bool = False
+        self,
+        operation: qasm3_ast.QuantumGate,
+        inverse: bool = False,
+        ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None,
     ) -> list[qasm3_ast.QuantumGate]:
         """Visit a gate operation element.
 
@@ -710,8 +730,17 @@ def _visit_basic_gate_operation(  # pylint: disable=too-many-locals
         """
         logger.debug("Visiting basic gate operation '%s'", str(operation))
         inverse_action = None
+        if ctrls is None:
+            ctrls = []
+
         if not inverse:
-            qasm_func, op_qubit_count = map_qasm_op_to_callable(operation.name.name)
+            if len(ctrls) > 0:
+                qasm_func, op_qubit_total_count = map_qasm_ctrl_op_to_callable(
+                    operation.name.name, len(ctrls)
+                )
+                op_qubit_count = op_qubit_total_count - len(ctrls)
+            else:
+                qasm_func, op_qubit_count = map_qasm_op_to_callable(operation.name.name)
         else:
             # in basic gates, inverse action only affects the rotation gates
             qasm_func, op_qubit_count, inverse_action = map_qasm_inv_op_to_callable(
@@ -726,20 +755,37 @@ def _visit_basic_gate_operation(  # pylint: disable=too-many-locals
                 op_parameters = [-1 * param for param in op_parameters]
 
         result = []
-
         unrolled_targets = self._unroll_multiple_target_qubits(operation, op_qubit_count)
         unrolled_gate_function = partial(qasm_func, *op_parameters)
-        result.extend(self._broadcast_gate_operation(unrolled_gate_function, unrolled_targets))
 
-        self._update_qubit_depth_for_gate(unrolled_targets)
+        if inverse:
+            # for convenience, we recur and handle the ctrl @'s to the unrolled no inverse case
+            # instead of trying to map the inverted callable to a ctrl'd version
+            result.extend(
+                [
+                    g2
+                    for g in self._broadcast_gate_operation(
+                        unrolled_gate_function, unrolled_targets, None
+                    )
+                    for g2 in self._visit_basic_gate_operation(g, False, ctrls)
+                ]
+            )
+        else:
+            result.extend(
+                self._broadcast_gate_operation(unrolled_gate_function, unrolled_targets, ctrls)
+            )
 
+            self._update_qubit_depth_for_gate(unrolled_targets, ctrls)
         if self._check_only:
             return []
 
         return result
 
     def _visit_custom_gate_operation(
-        self, operation: qasm3_ast.QuantumGate, inverse: bool = False
+        self,
+        operation: qasm3_ast.QuantumGate,
+        inverse: bool = False,
+        ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None,
     ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]:
         """Visit a custom gate operation element recursively.
 
@@ -756,6 +802,8 @@ def _visit_custom_gate_operation(
             None
         """
         logger.debug("Visiting custom gate operation '%s'", str(operation))
+        if ctrls is None:
+            ctrls = []
         gate_name: str = operation.name.name
         gate_definition: qasm3_ast.QuantumGateDefinition = self._custom_gates[gate_name]
         op_qubits: list[qasm3_ast.IndexedIdentifier] = (
@@ -803,7 +851,7 @@ def _visit_custom_gate_operation(
                     gate_op_copy.modifiers.append(
                         qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None)
                     )
-                result.extend(self._visit_generic_gate_operation(gate_op_copy))
+                result.extend(self._visit_generic_gate_operation(gate_op_copy, ctrls))
             else:
                 # TODO: add control flow support
                 raise_qasm3_error(
@@ -818,7 +866,10 @@ def _visit_custom_gate_operation(
         return result
 
     def _visit_external_gate_operation(
-        self, operation: qasm3_ast.QuantumGate, inverse: bool = False
+        self,
+        operation: qasm3_ast.QuantumGate,
+        inverse: bool = False,
+        ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None,
     ) -> list[qasm3_ast.QuantumGate]:
         """Visit an external gate operation element.
 
@@ -837,15 +888,17 @@ def _visit_external_gate_operation(
 
         logger.debug("Visiting external gate operation '%s'", str(operation))
         gate_name: str = operation.name.name
+        if ctrls is None:
+            ctrls = []
 
         if gate_name in self._custom_gates:
             # Ignore result, this is just for validation
-            self._visit_custom_gate_operation(operation, inverse=inverse)
+            self._visit_custom_gate_operation(operation, inverse, ctrls)
             # Don't need to check if custom gate exists, since we just validated the call
             gate_qubit_count = len(self._custom_gates[gate_name].qubits)
         else:
             # Ignore result, this is just for validation
-            self._visit_basic_gate_operation(operation, inverse=inverse)
+            self._visit_basic_gate_operation(operation)
             # Don't need to check if basic gate exists, since we just validated the call
             _, gate_qubit_count = map_qasm_op_to_callable(operation.name.name)
 
@@ -855,16 +908,23 @@ def _visit_external_gate_operation(
 
         self._push_context(Context.GATE)
 
+        # TODO: add ctrl @ support + testing
         modifiers = []
         if inverse:
-            modifiers = [qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None)]
+            modifiers.append(qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None))
+        if len(ctrls) > 0:
+            modifiers.append(
+                qasm3_ast.QuantumGateModifier(
+                    qasm3_ast.GateModifierName.ctrl, qasm3_ast.IntegerLiteral(len(ctrls))
+                )
+            )
 
         def gate_function(*qubits):
             return [
                 qasm3_ast.QuantumGate(
                     modifiers=modifiers,
                     name=qasm3_ast.Identifier(gate_name),
-                    qubits=list(qubits),
+                    qubits=ctrls + list(qubits),
                     arguments=list(op_parameters),
                 )
             ]
@@ -879,7 +939,10 @@ def gate_function(*qubits):
         return result
 
     def _visit_phase_operation(
-        self, operation: qasm3_ast.QuantumPhase, inverse: bool = False
+        self,
+        operation: qasm3_ast.QuantumPhase,
+        inverse: bool = False,
+        ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None,
     ) -> list[qasm3_ast.QuantumPhase]:
         """Visit a phase operation element.
 
@@ -891,6 +954,25 @@ def _visit_phase_operation(
             list[qasm3_ast.Statement]: The unrolled quantum phase operation.
         """
         logger.debug("Visiting phase operation '%s'", str(operation))
+        if ctrls is None:
+            ctrls = []
+
+        if len(ctrls) > 0:
+            return self._visit_basic_gate_operation(
+                qasm3_ast.QuantumGate(
+                    modifiers=[
+                        qasm3_ast.QuantumGateModifier(
+                            qasm3_ast.GateModifierName.ctrl,
+                            qasm3_ast.IntegerLiteral(len(ctrls) - 1),
+                        )
+                    ],
+                    name=qasm3_ast.Identifier("p"),
+                    qubits=ctrls[0:1],  # type: ignore
+                    arguments=[operation.argument],
+                ),  # type: ignore
+                inverse,
+                ctrls[:-1],
+            )
 
         evaluated_arg = Qasm3ExprEvaluator.evaluate_expression(operation.argument)[0]
         if inverse:
@@ -914,44 +996,10 @@ def _visit_phase_operation(
 
         return [operation]
 
-    def _collapse_gate_modifiers(
-        self, operation: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]
-    ) -> 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 (qasm3_ast.QuantumGate): The gate operation to collapse modifiers for.
-
-        Returns:
-            tuple[Any, Any]: The power and inverse values of the gate operation.
-        """
-        power_value, inverse_value = 1, False
-
-        for modifier in operation.modifiers:
-            modifier_name = modifier.modifier
-            if modifier_name == qasm3_ast.GateModifierName.pow and modifier.argument is not None:
-                current_power = Qasm3ExprEvaluator.evaluate_expression(modifier.argument)[0]
-                if current_power < 0:
-                    inverse_value = not inverse_value
-                power_value = power_value * abs(current_power)
-            elif modifier_name == qasm3_ast.GateModifierName.inv:
-                inverse_value = not inverse_value
-            elif modifier_name in [
-                qasm3_ast.GateModifierName.ctrl,
-                qasm3_ast.GateModifierName.negctrl,
-            ]:
-                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: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]
+    def _visit_generic_gate_operation(  # pylint: disable=too-many-branches
+        self,
+        operation: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase],
+        ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None,
     ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]:
         """Visit a gate operation element.
 
@@ -961,8 +1009,10 @@ def _visit_generic_gate_operation(
         Returns:
             None
         """
-        power_value, inverse_value = self._collapse_gate_modifiers(operation)
-        operation = copy.deepcopy(operation)
+        operation, ctrls = copy.deepcopy(operation), copy.deepcopy(ctrls)
+        negctrls = []
+        if ctrls is None:
+            ctrls = []
 
         # only needs to be done once for a gate operation
         if (
@@ -979,18 +1029,89 @@ def _visit_generic_gate_operation(
                     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
+
+        operation.qubits = self._get_op_bits(  # type: ignore
+            operation, reg_size_map=self._global_qreg_size_map, qubits=True
+        )
+
+        # ctrl / pow / inv modifiers commute. so group them.
+        exponent = 1
+        ctrl_arg_ind = 0
+        for modifier in operation.modifiers:
+            modifier_name = modifier.modifier
+            if modifier_name == qasm3_ast.GateModifierName.pow and modifier.argument is not None:
+                try:
+                    current_power = Qasm3ExprEvaluator.evaluate_expression(
+                        modifier.argument, reqd_type=qasm3_ast.IntType
+                    )[0]
+                except ValidationError:
+                    raise_qasm3_error(
+                        f"Power modifier argument must be an integer in gate operation {operation}",
+                        span=operation.span,
+                    )
+                exponent *= current_power
+            elif modifier_name == qasm3_ast.GateModifierName.inv:
+                exponent *= -1
+            elif modifier_name in [
+                qasm3_ast.GateModifierName.ctrl,
+                qasm3_ast.GateModifierName.negctrl,
+            ]:
+                try:
+                    count = Qasm3ExprEvaluator.evaluate_expression(
+                        modifier.argument, const_expr=True
+                    )[0]
+                except ValidationError:
+                    raise_qasm3_error(
+                        "Controlled modifier arguments must be compile-time constants "
+                        f"in gate operation {operation}",
+                        span=operation.span,
+                    )
+                if count is None:
+                    count = 1
+                if not isinstance(count, int) or count <= 0:
+                    raise_qasm3_error(
+                        "Controlled modifier argument must be a positive integer "
+                        f"in gate operation {operation}",
+                        span=operation.span,
+                    )
+                ctrl_qubits = operation.qubits[ctrl_arg_ind : ctrl_arg_ind + count]
+
+                # TODO: assert ctrl_qubits are single qubits
+                ctrl_arg_ind += count
+                ctrls.extend(ctrl_qubits)  # type: ignore
+                if modifier_name == qasm3_ast.GateModifierName.negctrl:
+                    negctrls.extend(ctrl_qubits)
+
+        power_value, inverse_value = abs(exponent), exponent < 0
+
+        operation.qubits = operation.qubits[ctrl_arg_ind:]
+        operation.modifiers = []
+
+        # apply pow(int) via duplication
+        if not isinstance(power_value, int):
+            raise_qasm3_error(
+                "Power modifiers with non-integer arguments are unsupported in gate "
+                f"operation {operation}",
+                span=operation.span,
+            )
+
+        # get controlled? inverted? operation x power times
         result: list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]] = []
         for _ in range(power_value):
             if isinstance(operation, qasm3_ast.QuantumPhase):
-                result.extend(self._visit_phase_operation(operation, inverse_value))
+                result.extend(self._visit_phase_operation(operation, inverse_value, ctrls))
             elif operation.name.name in self._external_gates:
-                result.extend(self._visit_external_gate_operation(operation, inverse_value))
+                result.extend(self._visit_external_gate_operation(operation, inverse_value, ctrls))
             elif operation.name.name in self._custom_gates:
-                result.extend(self._visit_custom_gate_operation(operation, inverse_value))
+                result.extend(self._visit_custom_gate_operation(operation, inverse_value, ctrls))
             else:
-                result.extend(self._visit_basic_gate_operation(operation, inverse_value))
+                result.extend(self._visit_basic_gate_operation(operation, inverse_value, ctrls))
+
+        # negctrl -> ctrl conversion
+        negs = [
+            qasm3_ast.QuantumGate([], qasm3_ast.Identifier("x"), [], [ctrl]) for ctrl in negctrls
+        ]
+        result = negs + result + negs  # type: ignore
 
         if self._check_only:
             return []
diff --git a/tox.ini b/tox.ini
index 2d8a794..be4f5d5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -29,9 +29,9 @@ commands =
 [testenv:isort]
 envdir = .tox/linters
 skip_install = true
-deps = isort
+deps = isort>=6.0.0
 commands =
-    isort . {posargs} src tests bin examples
+    isort {posargs} src tests bin examples 
 
 [testenv:pylint]
 envdir = .tox/linters


Test patch:
diff --git a/tests/qasm2/test_rotation_gates.py b/tests/qasm2/test_rotation_gates.py
index e08aabb..05ba4a3 100644
--- a/tests/qasm2/test_rotation_gates.py
+++ b/tests/qasm2/test_rotation_gates.py
@@ -9,7 +9,7 @@
 # THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3.
 
 """
-Module containing unit tests for parsing and unrolling programs that contain quantum 
+Module containing unit tests for parsing and unrolling programs that contain quantum
 rotations in qasm2 format.
 
 """
diff --git a/tests/qasm3/conftest.py b/tests/qasm3/conftest.py
index a81d6a0..68ed065 100644
--- a/tests/qasm3/conftest.py
+++ b/tests/qasm3/conftest.py
@@ -8,8 +8,7 @@
 #
 # THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3.
 
-"""Module containing imports for unit test resources
-"""
+"""Module containing imports for unit test resources"""
 
 # pylint: disable=wildcard-import, unused-wildcard-import
 
diff --git a/tests/qasm3/test_depth.py b/tests/qasm3/test_depth.py
index eeb8a31..0cc17bc 100644
--- a/tests/qasm3/test_depth.py
+++ b/tests/qasm3/test_depth.py
@@ -109,6 +109,19 @@ def test_inv_gate_depth():
     assert result.depth() == 5
 
 
+def test_ctrl_depth():
+    qasm3_string = """
+    OPENQASM 3;
+    include "stdgates.inc";
+    qubit[3] q;
+    ctrl @ x q[0], q[1]; 
+    ctrl @ x q[0], q[2];
+    """
+    result = loads(qasm3_string)
+    result.unroll()
+    assert result.depth() == 2
+
+
 def test_qubit_depth_with_unrelated_measure_op():
     qasm3_string = """
     OPENQASM 3;
diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py
index 92a3863..3a6feb3 100644
--- a/tests/qasm3/test_gates.py
+++ b/tests/qasm3/test_gates.py
@@ -185,6 +185,24 @@ def test_qasm_u3_gates_external_with_multiple_qubits():
     check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 1], "u3")
 
 
+def test_qasm_u3_gates_external_with_ctrl():
+    qasm3_string = """
+    OPENQASM 3;
+    include "stdgates.inc";
+    qubit[2] q;
+    ctrl @ u3(0.5, 0.5, 0.5) q[0], q[1];
+    """
+    expected_qasm = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    ctrl(1) @ u3(0.5, 0.5, 0.5) q[0], q[1];
+    """
+    result = loads(qasm3_string)
+    result.unroll(external_gates=["u3"])
+    check_unrolled_qasm(dumps(result), expected_qasm)
+
+
 def test_qasm_u2_gates():
     qasm3_string = """
     OPENQASM 3;
@@ -370,44 +388,221 @@ def test_inv_gate_modifier():
     check_three_qubit_gate_op(result.unrolled_ast, 1, [[0, 0, 1]], "ccx")
 
 
+def test_ctrl_gate_modifier():
+    qasm3_string = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[4] q;
+    ctrl @ z q[0], q[1];
+    ctrl @ ctrl @ x q[0], q[1], q[2];
+    ctrl(2) @ x q[1], q[2], q[3];
+    """
+    result = loads(qasm3_string)
+    result.unroll()
+    assert result.num_qubits == 4
+    check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz")
+    check_three_qubit_gate_op(result.unrolled_ast, 2, [[0, 1, 2], [1, 2, 3]], "ccx")
+
+
+def test_negctrl_gate_modifier():
+    qasm3_string = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    negctrl @ z q[0], q[1];
+    """
+    result = loads(qasm3_string)
+    result.unroll()
+    assert result.num_qubits == 2
+    check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "x")
+    check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz")
+
+
+def test_ctrl_in_custom_gate():
+    qasm3_string = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[3] q;
+    gate custom a, b, c {
+        ctrl @ x a, b;
+        ctrl(2) @ x a, b, c;
+    }
+    custom q[0], q[1], q[2];
+    """
+    result = loads(qasm3_string)
+    result.unroll()
+    assert result.num_qubits == 3
+    assert result.num_clbits == 0
+    check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cx")
+    check_three_qubit_gate_op(result.unrolled_ast, 1, [[0, 1, 2]], "ccx")
+
+
+def test_ctrl_in_subroutine():
+    qasm3_string = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    def f(qubit a, qubit b) {
+        ctrl @ x a, b;
+        return;
+    }
+    qubit[2] q;
+    f(q[0], q[1]);
+    """
+
+    result = loads(qasm3_string)
+    result.unroll()
+    assert result.num_qubits == 2
+    assert result.num_clbits == 0
+    check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cx")
+
+
+def test_ctrl_in_if_block():
+    qasm3_string = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    bit b;
+    b = measure q[0];
+    if(b == 1) {
+        ctrl @ x q[0], q[1];
+    }
+    """
+    expected_qasm = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    bit[1] b;
+    b[0] = measure q[0];
+    if (b[0] == true) {
+        cx q[0], q[1];
+    }
+    """
+    result = loads(qasm3_string)
+    result.unroll()
+    check_unrolled_qasm(dumps(result), expected_qasm)
+
+
+def test_ctrl_in_for_loop():
+    qasm3_string = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[4] q;
+
+    for int i in [0:2]{
+        ctrl @ x q[i], q[i+1];
+    }
+    """
+    result = loads(qasm3_string)
+    result.unroll()
+    assert result.num_qubits == 4
+    check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx")
+
+
+def test_ctrl_unroll():
+    qasm3_string = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] a;
+    qubit b;
+    ctrl (2) @ x a, b[0];
+    """
+    expected_qasm = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] a;
+    qubit[1] b;
+    ccx a[0], a[1], b[0];
+    """
+    result = loads(qasm3_string)
+    result.unroll()
+    check_unrolled_qasm(dumps(result), expected_qasm)
+
+
+def test_ctrl_gphase_eq_p():
+    qasm3_str_gphase = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit a;
+    ctrl @ gphase(1) a;
+    """
+    qasm3_str_p = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit a;
+    p(1) a;
+    """
+    result_gphase, result_p = loads(qasm3_str_gphase), loads(qasm3_str_p)
+    result_gphase.unroll()
+    result_p.unroll()
+    check_unrolled_qasm(dumps(result_gphase), dumps(result_p))
+
+
 def test_nested_gate_modifiers():
     qasm3_string = """
     OPENQASM 3;
     include "stdgates.inc";
-    qubit[2] q;
+    qubit[3] q;
     gate custom2 p, q{
-        y p;
+        x p;
         z q;
+        ctrl @ x q, p;
     }
     gate custom p, q {
         pow(1) @ custom2 p, q;
     }
-    pow(1) @ inv @ pow(2) @ custom q;
-    pow(-1) @ custom q;
+    pow(1) @ inv @ pow(2) @ custom q[0], q[1];
+    ctrl @ pow(-1) @ custom q[0], q[1], q[2];
     """
     result = loads(qasm3_string)
     result.unroll()
-    assert result.num_qubits == 2
+    assert result.num_qubits == 3
     assert result.num_clbits == 0
-    check_single_qubit_gate_op(result.unrolled_ast, 3, [1, 1, 1], "z")
-    check_single_qubit_gate_op(result.unrolled_ast, 3, [0, 0, 0], "y")
-
-
-def test_unsupported_modifiers():
-    # TO DO : add implementations, but till then we have tests
-    for modifier in ["ctrl", "negctrl"]:
-        with pytest.raises(
-            NotImplementedError,
-            match=r"Controlled modifier gates not yet supported .*",
-        ):
-            loads(
-                f"""
-                OPENQASM 3;
-                include "stdgates.inc";
-                qubit[2] q;
-                {modifier} @ h q[0], q[1];
-                """
-            ).validate()
+    check_single_qubit_gate_op(result.unrolled_ast, 2, [1, 1, 1], "z")
+    check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0, 0], "x")
+    check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 2]], "cz")
+    check_two_qubit_gate_op(result.unrolled_ast, 3, [[1, 0], [1, 0], [0, 1]], "cx")
+    check_three_qubit_gate_op(result.unrolled_ast, 1, [[0, 2, 1]], "ccx")
+
+
+@pytest.mark.parametrize(
+    "test",
+    [
+        (
+            """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    h q;
+    bit b;
+    b = measure q[0];
+    ctrl(b+1) @ x q[0], q[1];
+    """,
+            "Controlled modifier arguments must be compile-time constants.*",
+        ),
+        (
+            """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    ctrl(1.5) @ x q[0], q[1];
+    """,
+            "Controlled modifier argument must be a positive integer.*",
+        ),
+        (
+            """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit q;
+    pow(1.5) @ x q;
+    """,
+            "Power modifier argument must be an integer.*",
+        ),
+    ],
+)
+def test_modifier_arg_error(test):
+    qasm3_string, error_message = test
+    with pytest.raises(ValidationError, match=error_message):
+        loads(qasm3_string).validate()
 
 
 @pytest.mark.parametrize("test_name", CUSTOM_GATE_INCORRECT_TESTS.keys())
diff --git a/tests/qasm3/test_rebase.py b/tests/qasm3/test_rebase.py
new file mode 100644
index 0000000..7407ea9
--- /dev/null
+++ b/tests/qasm3/test_rebase.py
@@ -0,0 +1,336 @@
+# Copyright (C) 2025 qBraid
+#
+# This file is part of PyQASM
+#
+# PyQASM 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 PyQASM, as per Section 15 of the GPL v3.
+
+"""
+Module containing unit tests for rebasing programms
+
+"""
+import pytest
+
+from pyqasm.elements import BasisSet
+from pyqasm.entrypoint import dumps, loads
+from tests.utils import check_single_qubit_gate_op, check_unrolled_qasm
+
+
+@pytest.mark.parametrize(
+    "input_gates, decomposed_gates",
+    [
+        ("x q[0];", "rx(3.141592653589793) q[0];"),
+        ("y q[0];", "ry(3.141592653589793) q[0];"),
+        ("z q[0];", "rz(3.141592653589793) q[0];"),
+        (
+            "h q[0];",
+            """
+            ry(1.5707963267948966) q[0];
+            rx(3.141592653589793) q[0];
+            """,
+        ),
+        ("s q[0];", "rz(1.5707963267948966) q[0];"),
+        ("t q[0];", "rz(0.7853981633974483) q[0];"),
+        ("sx q[0];", "rx(1.5707963267948966) q[0];"),
+        ("sdg q[0];", "rz(-1.5707963267948966) q[0];"),
+        ("tdg q[0];", "rz(-0.7853981633974483) q[0];"),
+        (
+            "cz q[0], q[1];",
+            """
+            ry(1.5707963267948966) q[1];
+            rx(3.141592653589793) q[1];
+            cx q[0], q[1];
+            ry(1.5707963267948966) q[1];
+            rx(3.141592653589793) q[1];
+            """,
+        ),
+        (
+            "swap q[0], q[1];",
+            """
+            cx q[0], q[1];
+            cx q[1], q[0];
+            cx q[0], q[1];
+            """,
+        ),
+    ],
+)
+def test_rebase_rotational_cx(input_gates, decomposed_gates):
+    """Test that the rebasing gates to rotational-CX basis works as expected."""
+
+    qasm = f"""OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    bit[2] c;
+    {input_gates}
+    c[0] = measure q[0];
+    """
+
+    expected_qasm = f"""OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    bit[2] c;
+    {decomposed_gates}
+    c[0] = measure q[0];
+    """
+
+    result = loads(qasm)
+    result.rebase(BasisSet.ROTATIONAL_CX)
+    check_unrolled_qasm(dumps(result), expected_qasm)
+
+
+@pytest.mark.parametrize(
+    "input_gates, decomposed_gates",
+    [
+        (
+            "x q[0];",
+            """
+            h q[0];
+            s q[0];
+            s q[0];
+            h q[0];
+            """,
+        ),
+        (
+            "y q[0];",
+            """
+            s q[0];
+            s q[0];
+            h q[0];
+            s q[0];
+            s q[0];
+            h q[0];
+            """,
+        ),
+        (
+            "z q[0];",
+            """
+            s q[0];
+            s q[0];
+            """,
+        ),
+        (
+            "sx q[0];",
+            """
+            s q[0];
+            s q[0];
+            s q[0];
+            h q[0];
+            s q[0];
+            s q[0];
+            s q[0];
+            """,
+        ),
+        (
+            "cz q[0], q[1];",
+            """
+            h q[1];
+            cx q[0], q[1];
+            h q[1];
+            """,
+        ),
+        (
+            "swap q[0], q[1];",
+            """
+            cx q[0], q[1];
+            cx q[1], q[0];
+            cx q[0], q[1];
+            """,
+        ),
+    ],
+)
+def test_rebase_clifford_t(input_gates, decomposed_gates):
+    """Test that the rebasing gates to clifford-T basis works as expected"""
+
+    qasm = f"""OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    bit[2] c;
+    {input_gates}
+    c[0] = measure q[0];
+    """
+
+    expected_qasm = f"""OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    bit[2] c;
+    {decomposed_gates}
+    c[0] = measure q[0];
+    """
+
+    result = loads(qasm)
+    result.rebase(BasisSet.CLIFFORD_T)
+    check_unrolled_qasm(dumps(result), expected_qasm)
+
+
+def test_rebase_if():
+    """Test converting a QASM3 program that contains if statements"""
+
+    qasm = """OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[1] q;
+    bit[4] c;
+    if(c == 3){
+        h q[0];
+    }
+    if(c >= 3){
+        h q[0];
+    } else {
+        x q[0];
+    }
+    if(c <= 3){
+        h q[0];
+    } else {
+        x q[0];
+    }
+    if(c < 4){
+        h q[0];
+    } else {
+        x q[0];
+    }
+    """
+    expected_qasm = """OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[1] q;
+    bit[4] c;
+    if (c[0] == false) {
+        if (c[1] == false) {
+            if (c[2] == true) {
+                if (c[3] == true) {
+                    ry(1.5707963267948966) q[0];
+                    rx(3.141592653589793) q[0];
+                }
+            }
+        }
+    }
+    if (c[2] == true) {
+        if (c[3] == true) {
+            ry(1.5707963267948966) q[0];
+            rx(3.141592653589793) q[0];
+        } else {
+            rx(3.141592653589793) q[0];
+        }
+    } else {
+        rx(3.141592653589793) q[0];
+    }
+    if (c[0] == false) {
+       if (c[1] == false) {
+           ry(1.5707963267948966) q[0];
+           rx(3.141592653589793) q[0];
+       } else {
+           rx(3.141592653589793) q[0];
+       }
+    } else {
+        rx(3.141592653589793) q[0];
+    }
+    if (c[0] == false) {
+       if (c[1] == false) {
+           ry(1.5707963267948966) q[0];
+           rx(3.141592653589793) q[0];
+       } else {
+           rx(3.141592653589793) q[0];
+       }
+    } else {
+        rx(3.141592653589793) q[0];
+    }
+    """
+
+    result = loads(qasm)
+    result.rebase(BasisSet.ROTATIONAL_CX)
+    check_unrolled_qasm(dumps(result), expected_qasm)
+
+
+def test_rebase_invalid_basis_set():
+    qasm = """
+        OPENQASM 3.0;
+        include "stdgates.inc";
+
+        qubit[2] q;
+        bit[2] c;
+
+        h q[0];
+        cx q[0], q[1];
+        measure q->c;
+        """
+
+    result = loads(qasm)
+
+    with pytest.raises(ValueError, match="Target basis set 'invalid_basis' is not defined."):
+        result.rebase("invalid_basis")
+
+
+def test_rebase_loop():
+    """Test converting a QASM3 program that contains a for loop"""
+
+    qasm = """
+        OPENQASM 3.0;
+        include "stdgates.inc";
+
+        qubit[4] q;
+        bit[4] c;
+
+        for int i in [0:3]{
+            z q[i];
+        }
+        measure q->c;
+        """
+
+    result = loads(qasm)
+    result = result.rebase(BasisSet.CLIFFORD_T)
+
+    check_single_qubit_gate_op(result.unrolled_ast, 8, [0, 0, 1, 1, 2, 2, 3, 3], "s")
+
+
+def test_rebase_qasm_module_methods():
+    """Test that all other methods of Qasm Modules works as expected after rebase"""
+
+    qasm = """
+        OPENQASM 3.0;
+        include "stdgates.inc";
+        
+        qubit[3] q;
+        bit[3] c;
+        
+        h q[0];
+        cx q[0], q[1];
+        
+        barrier q;
+        measure q[0]->c[0];
+        """
+
+    result = loads(qasm)
+    result = result.rebase(BasisSet.ROTATIONAL_CX)
+
+    # Check has and remove barrier
+    assert result.has_barriers() is True
+    result.remove_barriers()
+    assert result.has_barriers() is False
+
+    # Check har and remove measurement
+    assert result.has_measurements() is True
+    result.remove_measurements()
+    assert result.has_measurements() is False
+
+    # Check remove idle qubit
+    assert result.num_qubits == 3
+    result.remove_idle_qubits()
+    assert result.num_qubits == 2
+
+    # Check depth
+    assert result.depth() == 3
+
+    # Check reverse qubit order
+    expected_reverse_qubit_order_qasm = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    bit[3] c;
+    ry(1.5707963267948966) q[1];
+    rx(3.141592653589793) q[1];
+    cx q[1], q[0];
+    """
+    result.reverse_qubit_order()
+    print(dumps(result))
+    check_unrolled_qasm(dumps(result), expected_reverse_qubit_order_qasm)
diff --git a/tests/qasm3/test_transformations.py b/tests/qasm3/test_transformations.py
index 2313edc..ca42220 100644
--- a/tests/qasm3/test_transformations.py
+++ b/tests/qasm3/test_transformations.py
@@ -9,7 +9,7 @@
 # THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3.
 
 """
-Module containing unit tests for transformations on qasm3 programs 
+Module containing unit tests for transformations on qasm3 programs
 
 """