# Copyright 2025 qBraid
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Defines a module for handling OpenQASM 2.0 programs.
"""
import re
from copy import deepcopy
import openqasm3.ast as qasm3_ast
from openqasm3.ast import Include, Program
from openqasm3.printer import dumps
from pyqasm.exceptions import ValidationError, raise_qasm3_error
from pyqasm.modules.base import QasmModule, QasmVisitor
from pyqasm.modules.qasm3 import Qasm3Module
# the QASM 2.0 <qop> production: a gate application, a measurement or a reset.
# only these may be the body of an 'if'.
_QOP_STATEMENTS = (
qasm3_ast.QuantumGate,
qasm3_ast.QuantumMeasurementStatement,
qasm3_ast.QuantumReset,
)
# statements the user can write in a conditional body that QASM 2.0 has no form for,
# named by the keyword they wrote rather than by the AST class they parsed into
_NON_QOP_KEYWORDS = {
qasm3_ast.QuantumBarrier: "barrier",
qasm3_ast.DelayInstruction: "delay",
qasm3_ast.Box: "box",
}
[docs]
class Qasm2Module(QasmModule):
"""
A module representing an openqasm2 quantum program.
Args:
name (str): Name of the module.
program (Program): The original openqasm2 program.
statements (list[Statement]): list of openqasm2 Statements.
"""
[docs]
def __init__(self, name: str, program: Program) -> None:
super().__init__(name, program)
self._unrolled_ast = Program(statements=[], version="2.0")
self._whitelist_statements = {
qasm3_ast.BranchingStatement,
qasm3_ast.QubitDeclaration,
qasm3_ast.ClassicalDeclaration,
qasm3_ast.Include,
qasm3_ast.QuantumGateDefinition,
qasm3_ast.QuantumGate,
qasm3_ast.QuantumMeasurement,
qasm3_ast.QuantumMeasurementStatement,
qasm3_ast.QuantumReset,
qasm3_ast.QuantumBarrier,
}
def _filter_statements(self) -> None:
"""Filter statements according to the whitelist."""
for stmt in self._statements:
stmt_type = type(stmt)
if stmt_type not in self._whitelist_statements:
raise ValidationError(f"Statement of type {stmt_type} not supported in QASM 2.0")
if isinstance(stmt, qasm3_ast.BranchingStatement):
self._filter_branch_body(stmt)
# TODO: add more filtering here if needed
def _filter_branch_body(self, statement: qasm3_ast.BranchingStatement) -> None:
"""Filter the body of a conditional against what QASM 2.0 allows there.
The QASM 2.0 grammar admits only a ``<qop>`` as the body of an ``if`` --
a gate application, a measurement or a reset. Everything else, ``barrier``
included, belongs to a different production and cannot be conditioned. The
parser does not enforce that, so it is enforced here as a whitelist: a
blacklist would let through whatever statement kinds it had not enumerated.
"""
for inner_stmt in [*statement.if_block, *statement.else_block]:
if isinstance(inner_stmt, _QOP_STATEMENTS):
continue
if isinstance(inner_stmt, qasm3_ast.BranchingStatement):
self._filter_branch_body(inner_stmt)
continue
if isinstance(inner_stmt, qasm3_ast.QuantumPhase):
# unroll-emitted phases are dropped in accept() (issue #351), so only a
# user-written gphase reaches this
raise_qasm3_error(
"Global phase is not representable in QASM 2.0, so it cannot appear in "
"a conditional body",
error_node=inner_stmt,
span=inner_stmt.span,
)
name = _NON_QOP_KEYWORDS.get(type(inner_stmt))
described = f"'{name}'" if name else f"statement of type {type(inner_stmt).__name__}"
raise_qasm3_error(
f"{described} is not supported as the body of an 'if' in QASM 2.0, which "
"allows only a gate, measurement or reset there",
error_node=inner_stmt,
span=inner_stmt.span,
)
def _format_declarations(self, qasm_str: str) -> str:
"""Format the unrolled qasm for declarations in openqasm 2.0 format"""
for declaration_type, replacement_type in [("qubit", "qreg"), ("bit", "creg")]:
pattern = rf"{declaration_type}\[(\d+)\]\s+(\w+);"
replacement = rf"{replacement_type} \2[\1];"
qasm_str = re.sub(pattern, replacement, qasm_str)
return qasm_str
def _qasm_ast_to_str(self, qasm_ast: Program) -> str:
"""Convert the qasm AST to a string."""
# set the version to 2.0
qasm_ast.version = "2.0"
raw_qasm = dumps(qasm_ast, old_measurement=True)
return self._format_declarations(raw_qasm)
def to_qasm3(self, as_str: bool = False) -> str | Qasm3Module:
"""Convert the module to openqasm3 format.
Args:
as_str (bool): Flag to indicate if the conversion should be to a string
or to a Qasm3Module object. Default is False.
Returns:
str | Qasm3Module: The module in openqasm3 format.
"""
qasm_program = deepcopy(self._original_program)
# replace the include with stdgates.inc
for stmt in qasm_program.statements:
if isinstance(stmt, Include) and stmt.filename == "qelib1.inc":
stmt.filename = "stdgates.inc"
break
qasm_program.version = "3.0"
return dumps(qasm_program) if as_str else Qasm3Module(self._name, qasm_program)
def finalize(self, statements: list[qasm3_ast.Statement]) -> list[qasm3_ast.Statement]:
"""Apply the QASM 2 transformations the finalized statement list needs.
Args:
statements (list[Statement]): The finalized statements.
Returns:
list[Statement]: The statements to store as the unrolled AST.
"""
return self._drop_global_phase(statements)
def _drop_global_phase(
self, statements: list[qasm3_ast.Statement]
) -> list[qasm3_ast.Statement]:
"""Remove QuantumPhase statements the unroller emitted (e.g. from the rzz/rxx
decompositions), descending into conditional bodies. OpenQASM 2 has no
global-phase syntax, and a global phase is unobservable, so dropping it is
semantically safe (issue #351)."""
filtered = []
for stmt in statements:
if isinstance(stmt, qasm3_ast.QuantumPhase):
# a controlled phase is relative, not global, and is observable; the
# visitor rewrites those to 'p' gates, so none should reach here
if stmt.modifiers:
raise_qasm3_error(
"Modified global phase cannot be dropped for a QASM 2 target",
error_node=stmt,
span=stmt.span,
)
continue
if isinstance(stmt, qasm3_ast.BranchingStatement):
stmt.if_block = self._drop_global_phase(stmt.if_block)
stmt.else_block = self._drop_global_phase(stmt.else_block)
if not stmt.if_block and not stmt.else_block:
# the body was nothing but global phase, and QASM 2 has no
# form for a conditional without a qop
continue
filtered.append(stmt)
return filtered
def accept(self, visitor: QasmVisitor) -> None:
"""Accept a visitor for the module.
Args:
visitor (QasmVisitor): The visitor to accept.
"""
self._filter_statements()
unrolled_stmt_list = visitor.visit_basic_block(self._statements)
final_stmt_list = visitor.finalize(unrolled_stmt_list)
self.unrolled_ast.statements = self.finalize(final_stmt_list) # type: ignore[assignment]