Source code for blueqat.circuit_funcs.qasm_parser

# Copyright 2019-2026 The Blueqat Developers
#
# 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.
"""Parser for a practical subset of OpenQASM 2.0 (the qelib1.inc gate set) into a Circuit.

This is the reverse of `Circuit.to_qasm()`.
"""

import ast
import math
import operator
import re
from typing import List

from ..circuit import Circuit

# name -> (blueqat gate name, number of numeric args)
_GATES = {
    # no-arg 1-qubit
    'x': ('x', 0), 'y': ('y', 0), 'z': ('z', 0), 'h': ('h', 0),
    's': ('s', 0), 'sdg': ('sdg', 0), 't': ('t', 0), 'tdg': ('tdg', 0),
    'sx': ('sx', 0), 'sxdg': ('sxdg', 0), 'id': ('i', 0), 'reset': ('reset', 0),
    # 1-arg 1-qubit
    'rx': ('rx', 1), 'ry': ('ry', 1), 'rz': ('rz', 1),
    'p': ('phase', 1), 'u1': ('phase', 1),
    # 3-arg 1-qubit
    'u': ('u', 3), 'u3': ('u', 3),
    # no-arg 2-qubit
    'cx': ('cx', 0), 'cz': ('cz', 0), 'cy': ('cy', 0), 'ch': ('ch', 0), 'swap': ('swap', 0),
    # 1-arg 2-qubit
    'cp': ('cphase', 1), 'cu1': ('cphase', 1),
    'crx': ('crx', 1), 'cry': ('cry', 1), 'crz': ('crz', 1),
    'rxx': ('rxx', 1), 'ryy': ('ryy', 1), 'rzz': ('rzz', 1),
    # blueqat's zz gate is the fixed diag(1, i, i, 1) -- it takes no angle
    'zz': ('zz', 0),
    # 4-arg 2-qubit
    'cu': ('cu', 4),
    # no-arg 3-qubit
    'ccx': ('ccx', 0), 'cswap': ('cswap', 0),
}

_CONSTS = {'pi': math.pi}
_BINOPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
           ast.Div: operator.truediv, ast.Pow: operator.pow}
_UNARYOPS = {ast.USub: operator.neg, ast.UAdd: operator.pos}


def _eval_angle_expr(node: ast.AST) -> float:
    """Safely evaluate a numeric angle expression (numbers, +-*/, unary -, and `pi`)
    without falling back to `eval`, since this parses (potentially untrusted) QASM text."""
    if isinstance(node, ast.Expression):
        return _eval_angle_expr(node.body)
    if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
        return node.value
    if isinstance(node, ast.Name) and node.id in _CONSTS:
        return _CONSTS[node.id]
    if isinstance(node, ast.BinOp) and type(node.op) in _BINOPS:
        return _BINOPS[type(node.op)](_eval_angle_expr(node.left), _eval_angle_expr(node.right))
    if isinstance(node, ast.UnaryOp) and type(node.op) in _UNARYOPS:
        return _UNARYOPS[type(node.op)](_eval_angle_expr(node.operand))
    raise ValueError(f"Unsupported expression in QASM angle argument: {ast.dump(node)}")


def _parse_args(args_str: str) -> List[float]:
    args = []
    for a in args_str.split(','):
        try:
            tree = ast.parse(a.strip(), mode='eval')
        except SyntaxError as e:
            raise ValueError(f"Could not parse QASM argument {a!r}: {e}") from e
        args.append(_eval_angle_expr(tree))
    return args


def _parse_targets(targets_str: str) -> List[int]:
    return [int(m.group(1)) for m in re.finditer(r'\[(\d+)\]', targets_str)]


[docs] def from_qasm(qasm: str) -> Circuit: """Parse an OpenQASM 2.0 program (the qelib1.inc gate set) into a Circuit.""" c = Circuit() # strip line (//) and block (/* */) comments text = re.sub(r'/\*.*?\*/', '', qasm, flags=re.DOTALL) text = re.sub(r'//.*', '', text) for raw_stmt in text.split(';'): stmt = raw_stmt.strip() if not stmt: continue if stmt.startswith(('OPENQASM', 'include', 'qreg', 'creg', 'gate ', 'opaque ')): continue barrier_match = re.match(r'barrier\s+(.+)$', stmt) if barrier_match: targets = _parse_targets(barrier_match.group(1)) if targets: c.barrier[tuple(targets) if len(targets) > 1 else targets[0]] # "barrier q;" (whole register, no explicit indices) is dropped: # the register size isn't tracked here. continue measure_match = re.match(r'measure\s+q\[(\d+)\]\s*->\s*c\[(\d+)\]$', stmt) if measure_match: c.m[int(measure_match.group(1))] continue gate_match = re.match(r'(\w+)\s*(?:\(([^)]*)\))?\s+(.+)$', stmt) if not gate_match: raise ValueError(f"Could not parse QASM statement: {stmt!r}") name, args_str, targets_str = gate_match.groups() if name not in _GATES: raise ValueError(f"Unsupported QASM gate: {name!r}") gate_name, n_args = _GATES[name] args = _parse_args(args_str) if args_str else [] if len(args) != n_args: raise ValueError(f"Gate {name!r} expects {n_args} argument(s), got {len(args)}") targets = _parse_targets(targets_str) op = getattr(c, gate_name) if args: op = op(*args) target = targets[0] if len(targets) == 1 else tuple(targets) op[target] return c