# Copyright 2026 Qilimanjaro Quantum Tech
#
# 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.
"""QIR (Quantum Intermediate Representation) Base-Profile import / export.
Bridges :class:`qilisdk.digital.Circuit` and the `QIR Base Profile
<https://github.com/qir-alliance/qir-spec/blob/main/specification/under_development/profiles/Base_Profile.md>`_
via Microsoft's `pyqir <https://pypi.org/project/pyqir/>`_ library.
The Base Profile is a flat, statically-allocated subset of QIR: a single entry
point with no classical control flow, all qubits and result registers declared
up front, measurements (typically) at the end. This maps cleanly onto the
existing :class:`Circuit` / :class:`Gate` model.
The :mod:`pyqir` dependency is optional; install with ``qilisdk[qir]``.
"""
from __future__ import annotations
from pathlib import Path
from pyqir import (
BasicQisBuilder,
Call,
Context,
FloatConstant,
Module,
SimpleModule,
Value,
is_entry_point,
ptr_id,
required_num_qubits,
)
from pyqir.qis import swap as qis_swap
from qilisdk.digital import (
CNOT,
CZ,
RX,
RY,
RZ,
SWAP,
Adjoint,
Circuit,
Gate,
H,
I,
M,
S,
T,
X,
Y,
Z,
)
# === Gate ↔ QIR intrinsic mapping =================================================
#
# Each entry maps a qilisdk gate class to the matching method name on pyqir's
# BasicQisBuilder. The Adjoint-of-S/T case is handled inline in `_emit_gate`
# because it pattern-matches on the inner gate.
_SIMPLE_GATE_EMITTERS: dict[type[Gate], str] = {
X: "x",
Y: "y",
Z: "z",
H: "h",
S: "s",
T: "t",
}
def _qubit_index(gate: Gate) -> int:
"""Return the single target qubit of ``gate``.
Args:
gate (Gate): A single-qubit gate.
Returns:
int: The target qubit index.
Raises:
ValueError: If the gate targets a different number of qubits.
"""
if len(gate.qubits) != 1:
raise ValueError(f"Expected a single-qubit gate, got {gate} on qubits {gate.qubits}")
return gate.qubits[0]
def _scalar_param(gate: Gate, name: str) -> float:
"""Return the numeric value of a parameterized gate's angle.
Args:
gate (Gate): The parameterized gate.
name (str): Parameter name (``theta`` / ``phi`` / ``lam``).
Returns:
float: The angle value in radians.
"""
return float(getattr(gate, name))
def _produce_gate(
module: SimpleModule,
qis: BasicQisBuilder,
qubits: list[Value],
results: list[Value],
gate: Gate,
) -> None:
"""Translate a single qilisdk :class:`Gate` into one (or more) QIS calls.
Args:
module (SimpleModule): The pyqir module whose builder backs ``qis``;
used for intrinsics like ``swap`` that only have a function-style
binding.
qis (BasicQisBuilder): A QIS builder bound to the entry point.
qubits (list[Value]): The module's statically allocated qubit values.
results (list[Value]): The module's statically allocated result values.
gate (Gate): The gate to produce.
Raises:
NotImplementedError: If the gate has no QIR Base-Profile mapping.
"""
cls = type(gate)
if cls is I:
return # identity is a no-op in QIR
if cls in _SIMPLE_GATE_EMITTERS:
getattr(qis, _SIMPLE_GATE_EMITTERS[cls])(qubits[_qubit_index(gate)])
return
if cls is RX:
qis.rx(_scalar_param(gate, "theta"), qubits[_qubit_index(gate)])
return
if cls is RY:
qis.ry(_scalar_param(gate, "theta"), qubits[_qubit_index(gate)])
return
if cls is RZ:
qis.rz(_scalar_param(gate, "phi"), qubits[_qubit_index(gate)])
return
if cls is CNOT:
qis.cx(qubits[gate.control_qubits[0]], qubits[gate.target_qubits[0]])
return
if cls is CZ:
qis.cz(qubits[gate.control_qubits[0]], qubits[gate.target_qubits[0]])
return
if cls is SWAP:
qis_swap(module.builder, qubits[gate.qubits[0]], qubits[gate.qubits[1]])
return
if isinstance(gate, Adjoint):
inner_cls = type(gate.basic_gate)
if inner_cls is S:
qis.s_adj(qubits[_qubit_index(gate.basic_gate)]) # ty:ignore[invalid-argument-type]
return
if inner_cls is T:
qis.t_adj(qubits[_qubit_index(gate.basic_gate)]) # ty:ignore[invalid-argument-type]
return
if cls is M:
for q in gate.qubits:
qis.mz(qubits[q], results[q])
return
raise NotImplementedError(
f"Gate {gate.name!r} (type {cls.__name__}) has no QIR Base-Profile "
"intrinsic. Decompose it into supported primitives before export."
)
def _populate_module(circuit: Circuit, module: SimpleModule) -> None:
"""Walk ``circuit`` and emit each gate into ``module``'s entry point.
Args:
circuit (Circuit): The source circuit.
module (SimpleModule): A pyqir :class:`SimpleModule` pre-allocated with
enough qubits and results for the circuit.
"""
qis = BasicQisBuilder(module.builder)
qubits = list(module.qubits)
results = list(module.results)
for gate in circuit.gates:
_produce_gate(module, qis, qubits, results, gate)
# === QIR intrinsic → Gate factories (import side) =================================
def _qid(arg: Value) -> int:
"""Return the qubit/result pointer ID for a QIR call argument.
``pyqir.ptr_id`` returns ``None`` for the implicit zero pointer (``null``),
which QIR uses to encode qubit/result index ``0``.
Args:
arg (Value): A pyqir value representing a qubit or result pointer.
Returns:
int: The numeric ID; ``0`` for the null pointer.
"""
qid_value = ptr_id(arg)
return 0 if qid_value is None else qid_value
def _float_value(arg: Value) -> float:
"""Extract the numeric value of a :class:`FloatConstant` rotation angle.
Args:
arg (Value): The first argument of a QIS rotation call.
Returns:
float: The rotation angle.
Raises:
TypeError: If ``arg`` is not a constant floating-point value.
"""
if not isinstance(arg, FloatConstant):
raise TypeError(f"Expected FloatConstant rotation angle, got {type(arg).__name__}")
return arg.value
def _gate_from_call(callee: str, args: list[Value]) -> Gate:
"""Reconstruct a :class:`Gate` from a parsed QIS call.
Args:
callee (str): The mangled intrinsic name, e.g. ``__quantum__qis__h__body``.
args (list[Value]): The argument list; for rotations the first entry is
a :class:`FloatConstant` and the remaining entries are qubit /
result pointer constants.
Returns:
Gate: The reconstructed gate.
Raises:
NotImplementedError: If the intrinsic is not recognized.
"""
if callee == "__quantum__qis__x__body":
return X(_qid(args[0]))
if callee == "__quantum__qis__y__body":
return Y(_qid(args[0]))
if callee == "__quantum__qis__z__body":
return Z(_qid(args[0]))
if callee == "__quantum__qis__h__body":
return H(_qid(args[0]))
if callee == "__quantum__qis__s__body":
return S(_qid(args[0]))
if callee == "__quantum__qis__t__body":
return T(_qid(args[0]))
if callee == "__quantum__qis__s__adj":
return Adjoint(S(_qid(args[0])))
if callee == "__quantum__qis__t__adj":
return Adjoint(T(_qid(args[0])))
if callee == "__quantum__qis__rx__body":
return RX(_qid(args[1]), theta=_float_value(args[0]))
if callee == "__quantum__qis__ry__body":
return RY(_qid(args[1]), theta=_float_value(args[0]))
if callee == "__quantum__qis__rz__body":
return RZ(_qid(args[1]), phi=_float_value(args[0]))
if callee == "__quantum__qis__cnot__body":
return CNOT(_qid(args[0]), _qid(args[1]))
if callee == "__quantum__qis__cz__body":
return CZ(_qid(args[0]), _qid(args[1]))
if callee == "__quantum__qis__swap__body":
return SWAP(_qid(args[0]), _qid(args[1]))
if callee == "__quantum__qis__mz__body":
return M(_qid(args[0]))
raise NotImplementedError(
f"QIS intrinsic {callee!r} is not supported. Add a mapping in "
"`qilisdk.utils.qir._gate_from_call` to extend the supported set."
)
# === Internal: module → Circuit walk =============================================
def _circuit_from_module(module: Module) -> Circuit:
"""Walk a parsed QIR module's entry point and rebuild a :class:`Circuit`.
Args:
module (Module): A pyqir :class:`Module` produced by
:meth:`Module.from_ir` or :meth:`Module.from_bitcode`.
Returns:
Circuit: The reconstructed circuit.
Raises:
ValueError: If no Base-Profile entry-point function is present.
"""
entry = next((f for f in module.functions if is_entry_point(f)), None)
if entry is None:
raise ValueError(
"QIR module has no entry-point function; expected a Base-Profile "
"module with one `entry_point`-attributed function."
)
nqubits = required_num_qubits(entry) or 0
circuit = Circuit(nqubits=nqubits)
for block in entry.basic_blocks:
for instr in block.instructions:
if isinstance(instr, Call):
circuit.add(_gate_from_call(instr.callee.name, list(instr.args)))
return circuit
# === Public API ===================================================================
[documentos]
def to_qir(circuit: Circuit, *, name: str = "circuit") -> str:
"""Serialize a :class:`Circuit` to a QIR Base-Profile textual LLVM IR string.
Every qubit in the circuit gets its own pre-allocated result register, so
measurements can target any qubit. The module advertises itself as a Base
Profile module via the standard ``required_num_qubits`` /
``required_num_results`` attributes that QIR consumers expect.
Args:
circuit (Circuit): The circuit to serialize.
name (str): Module name embedded in the IR. Defaults to ``"circuit"``.
Returns:
str: The QIR textual LLVM IR (``.ll`` syntax).
Raises:
NotImplementedError: If the circuit contains a gate with no Base-Profile
mapping (e.g. ``U1`` / ``U2`` / ``U3``, three-qubit gates).
"""
module = SimpleModule(name=name, num_qubits=circuit.nqubits, num_results=circuit.nqubits)
_populate_module(circuit, module)
return module.ir()
[documentos]
def to_qir_file(circuit: Circuit, filename: str, *, name: str | None = None) -> None:
"""Serialize a :class:`Circuit` to a QIR file.
Dispatches on the file extension: ``.ll`` is written as textual LLVM IR and
``.bc`` as LLVM bitcode. Any other suffix is treated as textual.
Args:
circuit (Circuit): The circuit to serialize.
filename (str): Destination path.
name (str | None): Module name; defaults to the file stem.
"""
path = Path(filename)
module_name = name if name is not None else path.stem
if path.suffix.lower() == ".bc":
module = SimpleModule(
name=module_name,
num_qubits=circuit.nqubits,
num_results=circuit.nqubits,
)
_populate_module(circuit, module)
path.write_bytes(module.bitcode())
return
path.write_text(to_qir(circuit, name=module_name), encoding="utf-8")
[documentos]
def from_qir(qir_text: str) -> Circuit:
"""Parse a QIR Base-Profile textual LLVM IR string into a :class:`Circuit`.
Args:
qir_text (str): The QIR textual LLVM IR.
Returns:
Circuit: The reconstructed circuit.
Raises:
ValueError: If the module has no Base-Profile entry-point function.
NotImplementedError: If the IR uses an unsupported QIS intrinsic.
"""
module = Module.from_ir(Context(), qir_text)
return _circuit_from_module(module)
[documentos]
def from_qir_file(filename: str) -> Circuit:
"""Read a QIR file (``.ll`` or ``.bc``) and parse it into a :class:`Circuit`.
Args:
filename (str): Path to a ``.ll`` or ``.bc`` QIR file.
Returns:
Circuit: The reconstructed circuit.
"""
path = Path(filename)
ctx = Context()
if path.suffix.lower() == ".bc":
module = Module.from_bitcode(ctx, path.read_bytes())
else:
module = Module.from_ir(ctx, path.read_text(encoding="utf-8"))
return _circuit_from_module(module)