Skip to content

QASM Parser

parser

QASMCircuit dataclass

QASMCircuit(
    n_qubits: int = 0,
    n_cbits: int = 0,
    ops: List[Dict] = list(),
)

Parsed representation of an OpenQASM 2.0 / 3.0 circuit.

Attributes

n_qubits : total qubit count declared in qreg / qubit statements n_cbits : total classical bit count declared in creg / bit statements ops : list of gate dicts — each dict has keys: 'type' : 'gate' 'name' : lowercase gate name (aliases resolved) 'qubits' : list[int] — absolute qubit indices 'params' : list[float] — evaluated rotation angles

to_tuples

to_tuples() -> List[Tuple]

Convert ops to the tuple format expected by DenseSVSimulator.run_circuit: (name, qubit0, [qubit1, ...], [param0, ...])

BUG FIX (original): the original returned (name,) + tuple(qubits) + tuple(params) which placed params after qubits, but run_circuit expects params interleaved or trailing depending on gate type. For the standard (name, qubit, param) convention used throughout the simulator, this ordering is correct — preserved here but documented explicitly so callers know what to expect.

Source code in dense_evolution/parser.py
def to_tuples(self) -> List[Tuple]:
    """
    Convert ops to the tuple format expected by DenseSVSimulator.run_circuit:
    `(name, qubit0, [qubit1, ...], [param0, ...])`

    BUG FIX (original): the original returned
        (name,) + tuple(qubits) + tuple(params)
    which placed params *after* qubits, but run_circuit expects
    params interleaved or trailing depending on gate type.
    For the standard (name, qubit, param) convention used throughout
    the simulator, this ordering is correct — preserved here but
    documented explicitly so callers know what to expect.
    """
    out = []
    for op in self.ops:
        row = (op['name'],) + tuple(op['qubits']) + tuple(op['params'])
        out.append(row)
    return out

__iter__

__iter__()

Duck-type as an iterable of the same tuples to_tuples() returns, so a QASMCircuit works anywhere a plain circuit list is expected (QuantumTranspiler.transpile, Chunk.run_chunk, ...) without the caller having to remember to call .to_tuples() first. Verified this was a real gap, not a hypothetical one: for cmd in circuit inside QuantumTranspiler.transpile — reached via Chunk.run_chunk(circuit) — raised TypeError: 'QASMCircuit' object is not iterable when handed a QASMCircuit straight from QASMParser().parse(), instead of circuit.to_tuples().

Source code in dense_evolution/parser.py
def __iter__(self):
    """Duck-type as an iterable of the same tuples to_tuples() returns,
    so a QASMCircuit works anywhere a plain circuit list is expected
    (QuantumTranspiler.transpile, Chunk.run_chunk, ...) without the
    caller having to remember to call .to_tuples() first. Verified this
    was a real gap, not a hypothetical one: `for cmd in circuit` inside
    QuantumTranspiler.transpile — reached via Chunk.run_chunk(circuit)
    — raised `TypeError: 'QASMCircuit' object is not iterable` when
    handed a QASMCircuit straight from QASMParser().parse(), instead of
    circuit.to_tuples()."""
    return iter(self.to_tuples())

QASMParser

Robust OpenQASM 2.0 / 3.0 parser.

Supported features
  • qreg / creg (QASM 2.0)
  • qubit / bit (QASM 3.0)
  • Parametric gates: rx, ry, rz, p, u1, u2, u3, cp, crz, ...
  • Compound parameter expressions: pi/2, sqrt(2), cos(0.3), ...
  • Block comments / ... / and line comments // ...
  • Gate aliases: cu1→cp, u1→p, toffoli→ccx, cnot→cx, ...
  • Range syntax q[0:3] expanded to individual qubits
  • Bare register name (no index) resolved to qubit 0 of that register
  • Silent fallback (0.0) for unparseable parameter expressions

parse

parse(qasm_str: str) -> QASMCircuit

Parse an OpenQASM 2.0 or 3.0 string into a QASMCircuit.

BUG FIX 1 (original): the original joined all lines with a single space then split on ';'. Multi-line gate definitions (gate foo ...) were not stripped before joining, causing 'gate foo ...' to appear as a runnable instruction. Fixed by stripping comments before joining and by using the frozenset _SKIP check on the first token.

BUG FIX 2 (original): bare register names (e.g. 'h q' instead of 'h q[0]') were silently dropped if the register had more than one qubit, because qubit_map only stored 'name[0]' → 0 for size-1 registers. Fixed: bare names always map to qubit 0 of that register regardless of register size.

BUG FIX 3 (original): range syntax q[0:3] was never handled — such tokens fell through to the digit-extraction fallback which returned only the last digit. Fixed in _resolve_qubits.

Source code in dense_evolution/parser.py
def parse(self, qasm_str: str) -> QASMCircuit:
    """
    Parse an OpenQASM 2.0 or 3.0 string into a QASMCircuit.

    BUG FIX 1 (original): the original joined all lines with a single
    space then split on ';'.  Multi-line gate definitions (gate foo ...)
    were not stripped before joining, causing 'gate foo ...' to appear
    as a runnable instruction.  Fixed by stripping comments *before*
    joining and by using the frozenset _SKIP check on the first token.

    BUG FIX 2 (original): bare register names (e.g. 'h q' instead of
    'h q[0]') were silently dropped if the register had more than one
    qubit, because qubit_map only stored 'name[0]' → 0 for size-1
    registers.  Fixed: bare names always map to qubit 0 of that register
    regardless of register size.

    BUG FIX 3 (original): range syntax q[0:3] was never handled —
    such tokens fell through to the digit-extraction fallback which
    returned only the last digit.  Fixed in _resolve_qubits.
    """
    qubit_map: Dict[str, int] = {}
    cbit_map:  Dict[str, int] = {}
    n_qubits = 0
    n_cbits  = 0
    ops: List[Dict] = []

    # ── strip comments ───────────────────────────────────────────
    cleaned = self._RE_BLOCK_CMT.sub(' ', qasm_str)
    cleaned = self._RE_LINE_CMT.sub(' ', cleaned)

    # ── unroll for-loops / strip if-while-def blocks ────────────────
    # Must run before the ';'-split below: brace-delimited blocks are
    # not single ';'-terminated statements, and left alone they corrupt
    # whatever real statement follows them on the same line.
    cleaned = self._process_block_constructs(cleaned)

    # ── split into statements ─────────────────────────────────────
    statements = [s.strip() for s in cleaned.split(';') if s.strip()]

    for instr in statements:
        # collapse internal whitespace runs to a single space
        instr = re.sub(r'\s+', ' ', instr).strip()
        if not instr:
            continue

        # first token (before any space or '(') for keyword detection
        first_token = re.split(r'[\s(]', instr)[0].lower()
        if first_token in self._SKIP:
            continue

        # ── qreg (QASM 2.0) ─────────────────────────────────────
        m = self._RE_QREG2.match(instr)
        if m:
            reg_name, sz = m.group(1), int(m.group(2))
            for i in range(sz):
                qubit_map[f'{reg_name}[{i}]'] = n_qubits + i
            qubit_map[reg_name] = n_qubits   # bare name → first qubit
            n_qubits += sz
            continue

        # ── creg (QASM 2.0) ─────────────────────────────────────
        m = self._RE_CREG2.match(instr)
        if m:
            reg_name, sz = m.group(1), int(m.group(2))
            for i in range(sz):
                cbit_map[f'{reg_name}[{i}]'] = n_cbits + i
            cbit_map[reg_name] = n_cbits
            n_cbits += sz
            continue

        # ── qubit (QASM 3.0) ─────────────────────────────────────
        m = self._RE_QREG3.match(instr)
        if m:
            sz_s, reg_name = m.group(1), m.group(2)
            sz = int(sz_s) if sz_s else 1
            for i in range(sz):
                qubit_map[f'{reg_name}[{i}]'] = n_qubits + i
            qubit_map[reg_name] = n_qubits
            n_qubits += sz
            continue

        # ── bit (QASM 3.0) ───────────────────────────────────────
        m = self._RE_CREG3.match(instr)
        if m:
            sz_s, reg_name = m.group(1), m.group(2)
            sz = int(sz_s) if sz_s else 1
            for i in range(sz):
                cbit_map[f'{reg_name}[{i}]'] = n_cbits + i
            cbit_map[reg_name] = n_cbits
            n_cbits += sz
            continue

        # ── gate application ─────────────────────────────────────
        op = self._parse_gate(instr, qubit_map)
        if op is not None:
            if op['name'] in self._SINGLE_QUBIT_GATES and len(op['qubits']) > 1:
                # range syntax on an inherently single-qubit gate
                # (e.g. `h q[0:3]`) — expand into one op per qubit.
                for q in op['qubits']:
                    ops.append({
                        'type': op['type'], 'name': op['name'],
                        'qubits': [q], 'params': list(op['params']),
                    })
                    n_qubits = max(n_qubits, q + 1)
            else:
                ops.append(op)
                # update n_qubits from seen qubit indices
                # (handles circuits without explicit qreg declarations)
                if op['qubits']:
                    n_qubits = max(n_qubits, max(op['qubits']) + 1)

    return QASMCircuit(n_qubits, n_cbits, ops)

validate

validate(circ: QASMCircuit) -> Tuple[bool, str]

Light structural validation — does not verify gate semantics.

Source code in dense_evolution/parser.py
def validate(self, circ: QASMCircuit) -> Tuple[bool, str]:
    """Light structural validation — does not verify gate semantics."""
    if circ.n_qubits <= 0:
        return False, 'n_qubits must be > 0.'
    if not circ.ops:
        return False, 'No gate operations found in circuit.'
    # check for out-of-range qubit references
    for i, op in enumerate(circ.ops):
        for q in op.get('qubits', []):
            if not (0 <= q < circ.n_qubits):
                return False, (
                    f"Gate '{op['name']}' at op[{i}] references "
                    f"qubit {q} but n_qubits={circ.n_qubits}.")
    return True, 'OK'