Source code for uniqc.compile.originir.originir_base_parser

"""OriginIR base parser module.

This module provides the base parser for OriginIR quantum circuit representation,
including parsing QINIT, CREG statements and quantum operations.

OriginIR-ext supports **named quantum/classical registers**. ``QINIT`` and
``CREG`` accept either the classic bare-integer form (``QINIT 6`` ≡
``QINIT q[6]``, ``CREG 6`` ≡ ``CREG c[6]``) or one or more named-register
declarations, written either as multiple lines or as a single comma-separated
line (``QINIT q[6], q1[6]``). Registers are laid into a single flat physical
index space in declaration order (so ``QINIT q[6]`` followed by
``QINIT q1[6]`` is equivalent to ``QINIT 12`` with ``q`` → 0–5 and ``q1`` →
6–11). Register-qualified references (``q1[0]``, ``c1[2]``) are resolved to
physical indices at parse time; the parser (and ``to_extended_originir``) always
emit a flat single ``QINIT``/``CREG`` header with physical ``q[i]``/``c[i]``
operands — register names are not preserved on export.

``DEF`` subroutines reuse the named-register declaration syntax for their
formal signature (``DEF name(q[2], anc[1]) (theta1, theta2)``) with an optional
trailing list of **scalar** parameter names. Calls (``name(a[3], a[5]) (0.5)``)
are expanded inline into the flat program at parse time.

Key exports:
    OriginIR_BaseParser: Base parser class for OriginIR circuits.
"""

__all__ = ["OriginIR_BaseParser"]

import re
from copy import deepcopy

from uniqc.circuit_builder import opcode_to_line_originir
from uniqc.circuit_builder.qcircuit import Circuit

from .originir_line_parser import OriginIR_LineParser

# A register reference/declaration item: ``name[index]`` (or ``name[size]``).
_REG_ITEM_RE = re.compile(r"([A-Za-z_][A-Za-z0-9_]*) *\[ *(\d+) *\]")
_REG_ITEM_FULL_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*) *\[ *(\d+) *\] *$")
_IDENT_FULL_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
_INT_FULL_RE = re.compile(r"^\d+$")


class _DefInfo:
    """A parsed ``DEF`` subroutine definition.

    Attributes:
        name: Subroutine name.
        formal_base: Mapping of formal register name -> ``(local_base, size)``,
            laid out contiguously in declaration order.
        total_qubits: Total number of formal qubits (sum of register sizes).
        params: Ordered list of scalar parameter names.
        body_lines: Raw (unresolved) body lines, verbatim from the source.
    """

    __slots__ = ("name", "formal_base", "total_qubits", "params", "body_lines")

    def __init__(self, name, formal_base, total_qubits, params, body_lines):
        self.name = name
        self.formal_base = formal_base
        self.total_qubits = total_qubits
        self.params = params
        self.body_lines = body_lines


[docs] class OriginIR_BaseParser: """Parser for OriginIR quantum circuit representation. Attributes: n_qubit: Total number of qubits (sum over all quantum registers). n_cbit: Total number of classical bits (sum over all classical registers). program_body: List of operation opcodes (flat, register-resolved). raw_originir: Raw OriginIR string. measure_qubits: List of measurement tuples (qubit, cbit). qram_declarations: Mapping of QRAM name -> (addr_size, data_size). qreg_map: Mapping of quantum register name -> (base_index, size). creg_map: Mapping of classical register name -> (base_index, size). gate_definitions: Mapping of DEF name -> :class:`_DefInfo`. """ def __init__(self): self.n_qubit = None self.n_cbit = None self.program_body = [] self.raw_originir = None self.measure_qubits: list[tuple[int, int]] = [] self.qram_declarations: dict[str, tuple[int, int]] = {} # Named-register maps: name -> (base_index, size). self.qreg_map: dict[str, tuple[int, int]] = {} self.creg_map: dict[str, tuple[int, int]] = {} # DEF subroutine definitions. self.gate_definitions: dict[str, _DefInfo] = {} # Symbolic-parameter arrays declared via ``PARAM name[size]``. self._param_arrays: dict[str, int] = {} # Running totals used while laying registers into the flat index space. self._qtotal = 0 self._ctotal = 0 # Parse-time control/dagger state. self._control_qubits_set: set[int] = set() self._dagger_stack: list[list] = [] self._dagger_count = 0 # ------------------------------------------------------------------ # Register declaration handling # ------------------------------------------------------------------ def _add_qreg(self, name: str, size: int) -> None: if name in self.qreg_map: raise ValueError(f"Duplicate quantum register '{name}'.") if name in self.creg_map: raise ValueError(f"Register name '{name}' is used for both a quantum and a classical register.") self.qreg_map[name] = (self._qtotal, size) self._qtotal += size def _add_creg(self, name: str, size: int) -> None: if name in self.creg_map: raise ValueError(f"Duplicate classical register '{name}'.") if name in self.qreg_map: raise ValueError(f"Register name '{name}' is used for both a quantum and a classical register.") self.creg_map[name] = (self._ctotal, size) self._ctotal += size @staticmethod def _parse_register_decl(arg_str: str, default_name: str, kind: str) -> list[tuple[str, int]]: """Parse the argument portion of a QINIT/CREG line. Supports the bare-integer form (``6`` -> default register) and the named / comma-separated form (``q[6], q1[6]``). """ s = arg_str.strip() if s == "": raise ValueError(f"{kind} declaration is empty.") if _INT_FULL_RE.match(s): return [(default_name, int(s))] decls: list[tuple[str, int]] = [] for item in s.split(","): item = item.strip() if not item: continue m = _REG_ITEM_FULL_RE.match(item) if not m: raise ValueError( f"Invalid {kind} register declaration item '{item}'. Expected a bare integer or 'name[size]'." ) decls.append((m.group(1), int(m.group(2)))) if not decls: raise ValueError(f"Invalid {kind} declaration: '{arg_str}'.") return decls def _extract_header(self, lines: list[str]) -> int: """Consume the QRAMDECL/QINIT/CREG header and return the body start index.""" idx = 0 n = len(lines) seen_qinit = False seen_creg = False while idx < n: raw = lines[idx].strip() if not raw: idx += 1 continue token = raw.split()[0] if token == "QRAMDECL": _op, name, addr_size, data_size = OriginIR_LineParser.handle_qramdecl(raw) if name in self.qram_declarations: raise ValueError(f"QRAM '{name}' is declared more than once.") self.qram_declarations[name] = (addr_size, data_size) OriginIR_LineParser._declared_qram_names.add(name) idx += 1 continue if token == "QINIT": if seen_creg: raise ValueError("QINIT declarations must precede CREG declarations.") for reg_name, size in self._parse_register_decl(raw[len("QINIT") :], "q", "QINIT"): self._add_qreg(reg_name, size) seen_qinit = True idx += 1 continue if token == "CREG": for reg_name, size in self._parse_register_decl(raw[len("CREG") :], "c", "CREG"): self._add_creg(reg_name, size) seen_creg = True idx += 1 continue if token == "PARAM": self._handle_param_decl(raw) idx += 1 continue # First line that is not part of the header — the body starts here. break if not seen_qinit: raise ValueError("OriginIR input does not have correct QINIT statement.") if not seen_creg: raise ValueError("OriginIR input does not have correct CREG statement.") self.n_qubit = self._qtotal self.n_cbit = self._ctotal return idx def _handle_param_decl(self, raw: str) -> None: """Parse a ``PARAM name`` (scalar) or ``PARAM name[size]`` (array) line. Array declarations are recorded so the resulting circuit re-serializes with the same ``PARAM name[size]`` header and renders element symbols as ``name[i]``. Scalar declarations are validated but need no state — the symbol is created when the parameter is referenced in a gate line. """ arg = raw[len("PARAM") :].strip() array_match = _REG_ITEM_FULL_RE.match(arg) if array_match: name, size = array_match.group(1), int(array_match.group(2)) if name in self._param_arrays and self._param_arrays[name] != size: raise ValueError(f"Conflicting PARAM array declaration for '{name}'.") self._param_arrays[name] = size return if _IDENT_FULL_RE.match(arg): return raise ValueError(f"Invalid PARAM declaration: {raw!r}") # ------------------------------------------------------------------ # Reference resolution # ------------------------------------------------------------------ def _resolve_line_registers(self, line: str, lineno: int) -> str: """Rewrite register-qualified references in *line* to physical indices. Named registers (``regname[idx]``) are resolved to the canonical physical registers ``q[<physical>]`` / ``c[<physical>]`` with strict parse-time range checks. References to the canonical physical registers ``q`` / ``c`` themselves are passed through unchanged (identity resolution): they are already physical, and their range is validated by the standard operation-qubit check downstream — matching the historical (register-less) parser behaviour. """ def repl(m: "re.Match") -> str: name, idx = m.group(1), int(m.group(2)) # Canonical physical registers: pass through unchanged. if name == "q" or name == "c": return m.group(0) if name in self.qreg_map: base, size = self.qreg_map[name] if idx >= size: raise ValueError( f"Parse error at line {lineno}: {line}\n" f"Index {idx} is out of range for quantum register '{name}' (size {size})." ) return f"q[{base + idx}]" if name in self.creg_map: base, size = self.creg_map[name] if idx >= size: raise ValueError( f"Parse error at line {lineno}: {line}\n" f"Index {idx} is out of range for classical register '{name}' (size {size})." ) return f"c[{base + idx}]" # A declared symbolic-parameter array reference (``alpha[2]``): leave # it verbatim so the parameter parser can turn it into symbol # ``alpha_2`` — it is not a qubit/classical register. if name in self._param_arrays: return m.group(0) raise ValueError(f"Parse error at line {lineno}: {line}\nUnknown register '{name}'.") return _REG_ITEM_RE.sub(repl, line) def _resolve_qubit_ref(self, name: str, idx: int, lineno: int, physical: bool) -> int: if physical: if name != "q": raise ValueError(f"Parse error at line {lineno}: unexpected register '{name}' in expanded call.") return idx if name in self.qreg_map: base, size = self.qreg_map[name] if idx >= size: raise ValueError( f"Parse error at line {lineno}: index {idx} out of range for quantum register '{name}' " f"(size {size})." ) return base + idx # ``q`` is the canonical physical register even when only named # registers were declared: allow direct physical addressing. if name == "q": if self.n_qubit is not None and idx >= self.n_qubit: raise ValueError( f"Parse error at line {lineno}: physical qubit q[{idx}] exceeds the maximum (QINIT {self.n_qubit})." ) return idx raise ValueError(f"Parse error at line {lineno}: unknown quantum register '{name}'.") def _resolve_whole_register(self, name: str, lineno: int, physical: bool) -> list[int]: if physical: raise ValueError(f"Parse error at line {lineno}: whole-register argument '{name}' is not allowed here.") if name in self.qreg_map: base, size = self.qreg_map[name] return list(range(base, base + size)) raise ValueError(f"Parse error at line {lineno}: unknown quantum register '{name}'.") def _resolve_call_qubits(self, qargs: str, name: str, lineno: int, physical: bool) -> list[int]: result: list[int] = [] for item in qargs.split(","): item = item.strip() if not item: continue m = _REG_ITEM_FULL_RE.match(item) if m: result.append(self._resolve_qubit_ref(m.group(1), int(m.group(2)), lineno, physical)) elif _IDENT_FULL_RE.match(item): result.extend(self._resolve_whole_register(item, lineno, physical)) else: raise ValueError(f"Parse error at line {lineno}: invalid qubit argument '{item}' in call to '{name}'.") return result @staticmethod def _resolve_call_params(pargs, name: str, lineno: int) -> list[float]: if pargs is None or pargs.strip() == "": return [] values: list[float] = [] for tok in pargs.split(","): tok = tok.strip() if not tok: continue try: values.append(float(tok)) except ValueError as exc: raise ValueError( f"Parse error at line {lineno}: non-scalar parameter '{tok}' in call to '{name}'." ) from exc return values # ------------------------------------------------------------------ # DEF handling # ------------------------------------------------------------------ def _collect_def_block(self, lines: list[str], start: int) -> int: """Collect a ``DEF ... ENDDEF`` block and return the next line index.""" header = lines[start].strip() _op, formal_qregs, params, name = OriginIR_LineParser.handle_def(header) formal_base: dict[str, tuple[int, int]] = {} base = 0 for reg_name, size in formal_qregs: if reg_name in formal_base: raise ValueError(f"Duplicate register '{reg_name}' in DEF '{name}' signature.") formal_base[reg_name] = (base, size) base += size total = base for param in params: if param in formal_base: raise ValueError(f"DEF '{name}': parameter '{param}' collides with a register name.") body: list[str] = [] i = start + 1 closed = False while i < len(lines): body_line = lines[i].strip() if body_line == "ENDDEF": closed = True break if body_line.split()[:1] == ["DEF"]: raise ValueError(f"Nested DEF definitions are not supported (in DEF '{name}').") if body_line: body.append(body_line) i += 1 if not closed: raise ValueError(f"DEF '{name}' block is not closed with ENDDEF.") if name in self.gate_definitions: raise ValueError(f"Duplicate DEF definition '{name}'.") if name in self.qram_declarations or name in OriginIR_LineParser._declared_qram_names: raise ValueError(f"DEF name '{name}' collides with a QRAM declaration.") self.gate_definitions[name] = _DefInfo(name, formal_base, total, params, body) return i + 1 # skip the ENDDEF line def _instantiate_def_line(self, body_line, definition, actual_qubits, param_values): line = body_line # 1) Substitute scalar parameters (whole-word) with numeric literals. for pname, pval in param_values.items(): line = re.sub(rf"\b{re.escape(pname)}\b", repr(float(pval)), line) # 2) Rewrite formal register references to physical ``q[<actual>]``. def repl(m: "re.Match") -> str: reg_name, idx = m.group(1), int(m.group(2)) if reg_name not in definition.formal_base: raise ValueError( f"Parse error in DEF '{definition.name}': unknown register '{reg_name}' in body: {body_line}" ) base, size = definition.formal_base[reg_name] if idx >= size: raise ValueError( f"Parse error in DEF '{definition.name}': index {idx} out of range for register " f"'{reg_name}' (size {size})." ) return f"q[{actual_qubits[base + idx]}]" return _REG_ITEM_RE.sub(repl, line) def _expand_def_call(self, name: str, qubits: list[int], params: list[float], lineno: int) -> None: definition = self.gate_definitions[name] if len(qubits) != definition.total_qubits: raise ValueError( f"Parse error at line {lineno}: DEF '{name}' expects {definition.total_qubits} qubit(s), " f"got {len(qubits)}." ) if len(params) != len(definition.params): raise ValueError( f"Parse error at line {lineno}: DEF '{name}' expects {len(definition.params)} parameter(s), " f"got {len(params)}." ) param_values = dict(zip(definition.params, params, strict=True)) for body_line in definition.body_lines: expanded = self._instantiate_def_line(body_line, definition, qubits, param_values) self._process_statement(expanded, lineno, physical=True) # ------------------------------------------------------------------ # Statement processing # ------------------------------------------------------------------ def _process_statement(self, line: str, lineno: int, physical: bool) -> None: """Resolve and dispatch a single (non-DEF-header) statement.""" # DEF subroutine call? call_match = OriginIR_LineParser.regexp_defcall.match(line) if call_match and call_match.group(1) in self.gate_definitions: name = call_match.group(1) qubits = self._resolve_call_qubits(call_match.group(2), name, lineno, physical) params = self._resolve_call_params(call_match.group(3), name, lineno) self._expand_def_call(name, qubits, params, lineno) return # Ordinary statement: resolve register references unless already physical. if not physical: line = self._resolve_line_registers(line, lineno) operation, qubits, cbit, parameter, dagger_flag, control_qubits = OriginIR_LineParser.parse_line(line) if operation is None: return if operation in ("QINIT", "CREG"): raise ValueError(f"Parse error at line {lineno}: '{operation}' may only appear in the header.") self._apply_op(operation, qubits, cbit, parameter, dagger_flag, control_qubits, line, lineno) def _apply_op(self, operation, qubits, cbit, parameter, dagger_flag, control_qubits, line, lineno) -> None: # QRAMDECL encountered in the body — register it and continue. if operation == "QRAMDECL": name, addr_size, data_size = qubits if name in self.qram_declarations: raise ValueError(f"Parse error at line {lineno}: QRAM '{name}' is declared more than once.") self.qram_declarations[name] = (addr_size, data_size) OriginIR_LineParser._declared_qram_names.add(name) return # Range checks for operational qubit(s) and cbit. if isinstance(qubits, list): for qubit in qubits: if qubit >= self.n_qubit: raise ValueError( f"Parse error at line {lineno}: {line}\nQubit exceeds the maximum (QINIT {self.n_qubit})." ) elif qubits and qubits >= self.n_qubit: raise ValueError(f"Parse error at line {lineno}: {line}\nQubit exceeds the maximum (QINIT {self.n_qubit}).") for control_qubit in control_qubits or []: if control_qubit >= self.n_qubit: raise ValueError( f"Parse error at line {lineno}: {line}\n" f"Control qubit exceeds the maximum (QINIT {self.n_qubit})." ) if cbit and cbit >= self.n_cbit: raise ValueError(f"Parse error at line {lineno}: {line}\nCbit exceeds the maximum (CBIT {self.n_cbit}).") if operation == "CONTROL": self._control_qubits_set.update(qubits) elif operation == "ENDCONTROL": for qubit in qubits: self._control_qubits_set.discard(qubit) elif operation == "DAGGER": self._dagger_stack.append([]) self._dagger_count += 1 elif operation == "ENDDAGGER": if self._dagger_stack: reversed_ops = self._dagger_stack.pop() if not self._dagger_stack: self.program_body.extend(reversed_ops[::-1]) else: self._dagger_stack[-1].extend(reversed_ops[::-1]) else: raise ValueError( f"Parse error at line {lineno}: {line}\nEncounter ENDDAGGER operation before any DAGGER." ) self._dagger_count -= 1 else: if operation == "MEASURE": if self._control_qubits_set: raise ValueError( f"Parse error at line {lineno}: {line}\nMEASURE operation is inside a CONTROL block." ) if self._dagger_stack: raise ValueError( f"Parse error at line {lineno}: {line}\nMEASURE operation is inside a DAGGER block." ) self.measure_qubits.append((qubits, cbit)) else: dagger_flag = dagger_flag ^ bool(self._dagger_count % 2) ctrl_qubits = deepcopy(self._control_qubits_set) for qubit in control_qubits: if qubit in ctrl_qubits: raise ValueError( f"Parse error at line {lineno}: {line}\n" f"Qubit {qubit} is duplicated in the CONTROL statement." ) ctrl_qubits.add(qubit) qubits_used = deepcopy(ctrl_qubits) if isinstance(qubits, int): if qubits in ctrl_qubits: raise ValueError( f"Parse error at line {lineno}: {line}\n" f"Qubit {qubits} is duplicated in the CONTROL statement." ) else: for qubit in qubits: if qubit in ctrl_qubits: raise ValueError( f"Parse error at line {lineno}: {line}\n" f"Qubit {qubit} is duplicated in the CONTROL statement." ) qubits_used.add(qubit) ctrl_list = sorted(ctrl_qubits) if ctrl_qubits else None if self._dagger_stack: self._dagger_stack[-1].append((operation, qubits, cbit, parameter, dagger_flag, ctrl_list)) else: self.program_body.append((operation, qubits, cbit, parameter, dagger_flag, ctrl_list)) # ------------------------------------------------------------------ # Top-level parse # ------------------------------------------------------------------
[docs] def parse(self, originir_str): """Parse an OriginIR string and populate internal state. Args: originir_str: OriginIR string to parse. """ self.raw_originir = originir_str lines = originir_str.strip().splitlines() if not lines: raise ValueError("Parse error. Input is empty.") # Reset QRAM name registry for this parse session. OriginIR_LineParser._declared_qram_names = set() current_lineno = self._extract_header(lines) # Re-register declared QRAM names for this parse session. OriginIR_LineParser._declared_qram_names = set(self.qram_declarations.keys()) self._control_qubits_set = set() self._dagger_stack = [] self._dagger_count = 0 lineno = current_lineno while lineno < len(lines): raw = lines[lineno].strip() if not raw: lineno += 1 continue token = raw.split()[0] if token == "DEF": lineno = self._collect_def_block(lines, lineno) continue if raw == "ENDDEF": raise ValueError(f"Parse error at line {lineno}: {raw}\nENDDEF without a matching DEF.") self._process_statement(raw, lineno, physical=False) lineno += 1 # Finally, check if all dagger and control operations are closed. if self._control_qubits_set: raise ValueError("Parse error at end.\nThe CONTROL operation is not closed at the end of the OriginIR.") if self._dagger_stack: raise ValueError("Parse error at end.\nThe DAGGER operation is not closed at the end of the OriginIR.")
[docs] def to_extended_originir(self): """Convert parsed data back to extended OriginIR string. The output is always **flat**: a single ``QINIT``/``CREG`` header with physical ``q[i]``/``c[i]`` operands (named registers and DEF blocks are resolved/inlined at parse time). Returns: str: Extended OriginIR string representation. """ ret = "" for name, (addr_size, data_size) in self.qram_declarations.items(): ret += f"QRAMDECL {name} {addr_size},{data_size}\n" ret += f"QINIT {self.n_qubit}\n" ret += f"CREG {self.n_cbit}\n" body_lines = [opcode_to_line_originir(opcode) for opcode in self.program_body] ret += "\n".join(body_lines) if body_lines: ret += "\n" for qubit, cbit in sorted(self.measure_qubits, key=lambda item: item[1]): ret += f"MEASURE q[{qubit}], c[{cbit}]\n" return ret
@property def originir(self): """OriginIR string representation (alias for to_extended_originir). Returns: str: Extended OriginIR string. """ return self.to_extended_originir() def __str__(self): return self.to_extended_originir()
[docs] def to_circuit(self) -> Circuit: """ The function coverts OriginIR string into uniqc.Circuit object. Returns: uniqc.Circuit object. """ circuit = Circuit() # Transfer QRAM declarations for name, (addr_size, data_size) in self.qram_declarations.items(): circuit.qram_declarations[name] = (addr_size, data_size) # Transfer symbolic-parameter array declarations (PARAM name[size]). circuit._param_arrays = dict(self._param_arrays) for opcode in self.program_body: operation, qubits, cbit, parameter, dagger_flag, control_qubits = opcode circuit.add_gate(operation, qubits, cbit, parameter, dagger_flag, control_qubits) if self.measure_qubits: measured_qubits = [qubit for qubit, _ in sorted(self.measure_qubits, key=lambda item: item[1])] circuit.measure(*measured_qubits) if self.n_cbit is not None: circuit.cbit_num = self.n_cbit return circuit
[docs] def to_qasm(self): """ The function coverts OriginIR string into OpenQASM string. Returns: OpenQASM string. """ circuit = self.to_circuit() return circuit.qasm