diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04817ed5bb..501a6c00d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -186,6 +186,8 @@ jobs: uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@e7f84f39ce2d3b6c5d1d04526b8f94f98e455143 # v2.2.0 with: runs-on: ${{ matrix.runs-on }} + setup-mlir: true + llvm-version: 22.1.7 python-coverage: name: 🐍 Coverage @@ -208,6 +210,8 @@ jobs: uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@e7f84f39ce2d3b6c5d1d04526b8f94f98e455143 # v2.2.0 with: runs-on: ${{ matrix.runs-on }} + setup-mlir: true + llvm-version: 22.1.7 python-linter: name: 🐍 Lint @@ -218,6 +222,8 @@ jobs: check-stubs: true enable-ty: true enable-mypy: false + setup-mlir: true + llvm-version: 22.1.7 build-sdist: name: 🚀 CD @@ -244,6 +250,8 @@ jobs: uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-wheel-cibuildwheel.yml@e7f84f39ce2d3b6c5d1d04526b8f94f98e455143 # v2.2.0 with: runs-on: ${{ matrix.runs-on }} + setup-mlir: true + llvm-version: 22.1.7 # this job does nothing and is only used for branch protection required-checks-pass: diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b75a7f5fed..2e4d7ff138 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -32,7 +32,7 @@ build: - asdf install uv latest - asdf global uv latest # Install MLIR - - curl -LsSf https://github.com/munich-quantum-software/setup-mlir/releases/latest/download/setup-mlir.sh | bash -s -- -v 22.1.0 -p $HOME/mlir + - curl -LsSf https://github.com/munich-quantum-software/setup-mlir/releases/download/v1.4.1/setup-mlir.sh | bash -s -- -v 22.1.7 -p $HOME/mlir # Build the MLIR documentation - uvx cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_MQT_CORE_MLIR=ON -DMLIR_DIR=$HOME/mlir/lib/cmake/mlir - uvx cmake --build build --target mlir-doc diff --git a/CMakeLists.txt b/CMakeLists.txt index 017d91b5f0..cf84fe5017 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,11 +119,11 @@ if(MQT_CORE_INSTALL) endif() cmake_dependent_option(BUILD_MQT_CORE_QIR_RUNNER "Build the QIR runner of the MQT Core project" ON - "BUILD_MQT_CORE_MLIR" OFF) + "BUILD_MQT_CORE_MLIR;NOT BUILD_MQT_CORE_BINDINGS" OFF) cmake_dependent_option( BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR "Enable QIR program format support for the DDSIM QDMI Device" - ON "BUILD_MQT_CORE_MLIR" OFF) + ON "BUILD_MQT_CORE_MLIR;NOT BUILD_MQT_CORE_BINDINGS" OFF) # add main library code add_subdirectory(src) diff --git a/UPGRADING.md b/UPGRADING.md index e33dfb48b3..29304f0a20 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,7 +6,7 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] -### MLIR enabled by default for C++ builds +### MLIR enabled by default for C++ and Python package builds The MLIR-based functionality within MQT Core has long been experimental and opt-in. Starting with this release, MLIR is enabled by default for C++ library @@ -20,11 +20,16 @@ Please follow the instructions there to install the distribution for your platform. You can then point CMake to the installation directory using the `-DMLIR_DIR=/path/to/mlir/installation/lib/cmake/mlir` option. +As of this release, MLIR is also enabled for Python package builds, since the +package now exposes an MLIR-based compiler entry point in `mqt.core.mlir`. + +For local development, you can configure `MLIR_DIR` once in a repository-local +`.env` file (for example, `MLIR_DIR=/path/to/installation/lib/cmake/mlir`). MQT +Core's CMake setup will pick this up automatically when `MLIR_DIR` is not +otherwise provided. + The MLIR components can still be manually disabled by passing -`-DBUILD_MQT_CORE_MLIR=OFF` to CMake. MLIR is also not enabled for the Python -package builds because no functionality depends on it yet. This is expected to -change in the future, when we expose the MLIR-based functionality via the Python -package. +`-DBUILD_MQT_CORE_MLIR=OFF` to CMake. Known limitations: @@ -34,9 +39,6 @@ Known limitations: - AppleClang 17+ is required to build MQT Core with MLIR enabled due to some C++20 features being used that are not yet properly supported by older versions. -- Our pre-built distributions are compiled in Release mode. On Windows, this - leads to ABI incompatibilities with debug builds. Either build in Release mode - or build LLVM from source in Debug mode to resolve this. ### Removal of the density matrix support from the DD package diff --git a/bindings/CMakeLists.txt b/bindings/CMakeLists.txt index 2ba92c4400..117f19e477 100644 --- a/bindings/CMakeLists.txt +++ b/bindings/CMakeLists.txt @@ -10,3 +10,7 @@ add_subdirectory(ir) add_subdirectory(dd) add_subdirectory(fomac) add_subdirectory(na) + +if(BUILD_MQT_CORE_MLIR) + add_subdirectory(mlir) +endif() diff --git a/bindings/mlir/CMakeLists.txt b/bindings/mlir/CMakeLists.txt new file mode 100644 index 0000000000..fc239e40b3 --- /dev/null +++ b/bindings/mlir/CMakeLists.txt @@ -0,0 +1,35 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(TARGET_NAME "${MQT_CORE_TARGET_NAME}-mlir-bindings") + +if(NOT TARGET ${TARGET_NAME}) + # collect source files + file(GLOB_RECURSE SOURCES *.cpp) + + # declare the Python module + add_mqt_python_binding_nanobind( + CORE + ${TARGET_NAME} + ${SOURCES} + MODULE_NAME + mlir + INSTALL_DIR + . + LINK_LIBS + MQTCompilerPipeline + MQT::CoreIR) + + # install the Python stub file in editable mode for better IDE support + if(SKBUILD_STATE STREQUAL "editable") + install( + FILES ${PROJECT_SOURCE_DIR}/python/mqt/core/mlir.pyi + DESTINATION . + COMPONENT ${MQT_CORE_TARGET_NAME}_Python) + endif() +endif() diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp new file mode 100644 index 0000000000..520191e258 --- /dev/null +++ b/bindings/mlir/register_mlir.cpp @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "ir/QuantumComputation.hpp" +#include "mlir/Compiler/Programs.h" + +#include +#include // NOLINT(misc-include-cleaner) + +#include +#include +#include +#include +#include +#include + +namespace mqt { + +namespace nb = nanobind; +using namespace nb::literals; + +namespace { + +/** + * @brief Resolve a Python path-like object to a filesystem path string. + */ +[[nodiscard]] std::string fspath(const nb::object& pathLike) { + return nb::cast( + nb::module_::import_("os").attr("fspath")(pathLike)); +} + +/** + * @brief Check whether @p input unambiguously looks like source text. + */ +[[nodiscard]] bool isSourceString(const std::string_view input) { + return input.find("\n") != std::string_view::npos || + input.find("OPENQASM") != std::string_view::npos || + input.starts_with("module"); +} + +/** + * @brief Construct a frontend program from a string containing source or path. + */ +[[nodiscard]] mlir::CompilerProgram +programFromString(const std::string& input) { + if (isSourceString(input)) { + if (input.find("OPENQASM") != std::string::npos) { + return mlir::QCProgram::fromQASMString(input); + } + return mlir::QCProgram::fromMLIRString(input); + } + + const std::filesystem::path path(input); + if (path.empty()) { + return mlir::QCProgram::fromMLIRString(input); + } + + std::error_code error; + const auto exists = std::filesystem::exists(path, error); + if (error) { + throw std::runtime_error("Failed to inspect path '" + input + + "': " + error.message()); + } + + const auto extension = path.extension().string(); + if (!exists) { + if (extension == ".jeff" || extension == ".mlir" || extension == ".qasm") { + throw std::runtime_error("Input file '" + input + "' does not exist."); + } + return mlir::QCProgram::fromMLIRString(input); + } + if (!std::filesystem::is_regular_file(path, error) || error) { + throw std::runtime_error("Input path '" + input + "' is not a file."); + } + + if (extension == ".jeff") { + return mlir::JeffProgram::fromFile(path); + } + if (extension == ".mlir") { + return mlir::QCProgram::fromMLIRFile(path); + } + if (extension == ".qasm") { + return mlir::QCProgram::fromQASMFile(path); + } + throw std::runtime_error("Input file '" + input + + "' has unsupported extension '" + extension + "'."); +} + +/** + * @brief Convert a Python object to a compiler program. + * + * @details Program objects are copied by default so the high-level entry point + * behaves like a conventional compiler function. Set @p inplace to transfer + * ownership from a program object instead. + */ +[[nodiscard]] mlir::CompilerProgram programFromInput(const nb::object& program, + const bool inplace) { + if (nb::isinstance(program)) { + return programFromString(nb::cast(program)); + } + if (nb::hasattr(program, "__fspath__")) { + return programFromString(fspath(program)); + } + if (nb::isinstance(program)) { + return mlir::QCProgram::fromQuantumComputation( + nb::cast(program)); + } + + if (nb::isinstance(program)) { + auto& value = nb::cast(program); + return inplace ? mlir::CompilerProgram(std::move(value)) + : mlir::CompilerProgram(value.copy()); + } + if (nb::isinstance(program)) { + auto& value = nb::cast(program); + return inplace ? mlir::CompilerProgram(std::move(value)) + : mlir::CompilerProgram(value.copy()); + } + if (nb::isinstance(program)) { + auto& value = nb::cast(program); + return inplace ? mlir::CompilerProgram(std::move(value)) + : mlir::CompilerProgram(value.copy()); + } + const auto programType = + nb::cast(program.type().attr("__name__")); + const auto programModule = + nb::cast(program.type().attr("__module__")); + if (programType == "QuantumCircuit" && programModule.starts_with("qiskit.")) { + const auto computation = nb::cast( + nb::module_::import_("mqt.core.load").attr("load")(program)); + return mlir::QCProgram::fromQuantumComputation(computation); + } + + throw std::runtime_error("Program type " + programType + + " is not supported."); +} + +/** + * @brief Convert a C++ compiler program variant into its Python object. + */ +[[nodiscard]] nb::object toPython(mlir::CompilerProgram&& program) { + return std::visit( + [](auto&& value) -> nb::object { return nb::cast(std::move(value)); }, + std::move(program)); +} + +/** + * @brief Run the coordinated default pipeline and return a typed program. + */ +[[nodiscard]] nb::object +compileProgram(const nb::object& program, const mlir::ProgramFormat output, + const bool inplace, + const bool disableMergeSingleQubitRotationGates, + const bool enableHadamardLifting, const bool enableTiming, + const bool enableStatistics) { + mlir::QuantumCompilerConfig config; + config.disableMergeSingleQubitRotationGates = + disableMergeSingleQubitRotationGates; + config.enableHadamardLifting = enableHadamardLifting; + config.enableTiming = enableTiming; + config.enableStatistics = enableStatistics; + + return toPython(mlir::runDefaultPipeline(programFromInput(program, inplace), + output, config)); +} + +template +[[nodiscard]] ProgramType copiedOrConsumed(ProgramType& program, + const bool copy) { + if (copy) { + return program.copy(); + } + return std::move(program); +} + +} // namespace + +NB_MODULE(MQT_CORE_MODULE_NAME, m) { + m.doc() = R"pb( +MQT Core MLIR compiler bindings. +)pb"; + + nb::module_::import_("mqt.core.ir"); + + nb::enum_(m, "QIRProfile", "QIR target profiles.") + .value("BASE", mlir::QIRProfile::Base, "The QIR Base Profile.") + .value("ADAPTIVE", mlir::QIRProfile::Adaptive, + "The QIR Adaptive Profile."); + nb::enum_(m, "OutputFormat", + "Default compiler output formats.") + .value("QC_IMPORT", mlir::ProgramFormat::QCImport, + "QC directly after frontend import.") + .value("QC", mlir::ProgramFormat::QC, + "QC after the optimized QCO round trip.") + .value("QCO", mlir::ProgramFormat::QCO, "Optimized QCO.") + .value("JEFF", mlir::ProgramFormat::Jeff, "Serializable Jeff MLIR.") + .value("QIR_BASE", mlir::ProgramFormat::QIRBase, + "QIR for the Base Profile.") + .value("QIR_ADAPTIVE", mlir::ProgramFormat::QIRAdaptive, + "QIR for the Adaptive Profile."); + + auto program = nb::class_(m, "Program"); + program.def_prop_ro("is_valid", &mlir::Program::isValid) + .def_prop_ro("ir", &mlir::Program::str) + .def("__str__", &mlir::Program::str); + + auto qcProgram = nb::class_(m, "QCProgram"); + qcProgram + .def_static("from_mlir_str", &mlir::QCProgram::fromMLIRString, "source"_a) + .def_static( + "from_mlir_file", + [](const nb::object& path) { + return mlir::QCProgram::fromMLIRFile(fspath(path)); + }, + "path"_a, + nb::sig("def from_mlir_file(path: str | os.PathLike[str]) " + "-> QCProgram")) + .def_static("from_qasm_str", &mlir::QCProgram::fromQASMString, "source"_a) + .def_static( + "from_qasm_file", + [](const nb::object& path) { + return mlir::QCProgram::fromQASMFile(fspath(path)); + }, + "path"_a, + nb::sig("def from_qasm_file(path: str | os.PathLike[str]) " + "-> QCProgram")) + .def_static("from_quantum_computation", + &mlir::QCProgram::fromQuantumComputation, "computation"_a, + nb::sig("def from_quantum_computation(computation: " + "mqt.core.ir.QuantumComputation) -> QCProgram")) + .def_static( + "from_qiskit", + [](const nb::object& circuit) { + const auto computation = nb::cast( + nb::module_::import_("mqt.core.load").attr("load")(circuit)); + return mlir::QCProgram::fromQuantumComputation(computation); + }, + "circuit"_a, + nb::sig("def from_qiskit(circuit: qiskit.circuit.QuantumCircuit) " + "-> QCProgram")) + .def("copy", &mlir::QCProgram::copy) + .def("cleanup", &mlir::QCProgram::cleanup) + .def( + "to_qco", + [](mlir::QCProgram& value, const bool copy) { + auto source = copiedOrConsumed(value, copy); + return std::move(source).intoQCO(); + }, + nb::kw_only(), "copy"_a = false) + .def( + "to_qir", + [](mlir::QCProgram& value, const mlir::QIRProfile profile, + const bool copy) { + auto source = copiedOrConsumed(value, copy); + return std::move(source).intoQIR(profile); + }, + "profile"_a, nb::kw_only(), "copy"_a = false); + + auto qcoProgram = + nb::class_(m, "QCOProgram"); + qcoProgram + .def_static("from_mlir_str", &mlir::QCOProgram::fromMLIRString, + "source"_a) + .def_static( + "from_mlir_file", + [](const nb::object& path) { + return mlir::QCOProgram::fromMLIRFile(fspath(path)); + }, + "path"_a, + nb::sig("def from_mlir_file(path: str | os.PathLike[str]) " + "-> QCOProgram")) + .def("copy", &mlir::QCOProgram::copy) + .def("cleanup", &mlir::QCOProgram::cleanup) + .def("optimize", &mlir::QCOProgram::optimize, nb::kw_only(), + "merge_single_qubit_rotations"_a = true, + "enable_hadamard_lifting"_a = false) + .def( + "to_qc", + [](mlir::QCOProgram& value, const bool copy) { + auto source = copiedOrConsumed(value, copy); + return std::move(source).intoQC(); + }, + nb::kw_only(), "copy"_a = false) + .def( + "to_jeff", + [](mlir::QCOProgram& value, const bool copy) { + auto source = copiedOrConsumed(value, copy); + return std::move(source).intoJeff(); + }, + nb::kw_only(), "copy"_a = false); + + auto jeffProgram = + nb::class_(m, "JeffProgram"); + jeffProgram + .def_static( + "from_file", + [](const nb::object& path) { + return mlir::JeffProgram::fromFile(fspath(path)); + }, + "path"_a, + nb::sig("def from_file(path: str | os.PathLike[str]) " + "-> JeffProgram")) + .def_static( + "from_bytes", + [](const nb::bytes& bytes) { + return mlir::JeffProgram::fromBytes(nb::cast(bytes)); + }, + "data"_a) + .def("copy", &mlir::JeffProgram::copy) + .def("cleanup", &mlir::JeffProgram::cleanup) + .def("to_bytes", + [](const mlir::JeffProgram& value) { + const auto bytes = value.toBytes(); + return nb::bytes(bytes.data(), bytes.size()); + }) + .def( + "write", + [](const mlir::JeffProgram& value, const nb::object& path) { + value.write(fspath(path)); + }, + "path"_a, + nb::sig("def write(self, path: str | os.PathLike[str]) -> None")) + .def( + "to_qco", + [](mlir::JeffProgram& value, const bool copy) { + auto source = copiedOrConsumed(value, copy); + return std::move(source).intoQCO(); + }, + nb::kw_only(), "copy"_a = false); + + auto qirProgram = + nb::class_(m, "QIRProgram"); + qirProgram.def("copy", &mlir::QIRProgram::copy) + .def("cleanup", &mlir::QIRProgram::cleanup) + .def_prop_ro("profile", &mlir::QIRProgram::profile) + .def_prop_ro("llvm_ir", &mlir::QIRProgram::llvmIR); + + m.def( + "compile_program", &compileProgram, "program"_a, nb::kw_only(), + "output"_a = mlir::ProgramFormat::QC, "inplace"_a = false, + "disable_merge_single_qubit_rotation_gates"_a = false, + "enable_hadamard_lifting"_a = false, "enable_timing"_a = false, + "enable_statistics"_a = false, + nb::sig("def compile_program(program: str | os.PathLike[str] | " + "mqt.core.ir.QuantumComputation | qiskit.circuit.QuantumCircuit " + "| QCProgram | QCOProgram | JeffProgram, *, " + "output: OutputFormat = OutputFormat.QC, inplace: bool = False, " + "disable_merge_single_qubit_rotation_gates: bool = False, " + "enable_hadamard_lifting: bool = False, " + "enable_timing: bool = False, enable_statistics: bool = False) " + "-> QCProgram | QCOProgram | JeffProgram | QIRProgram"), + R"pb( +Run the coordinated default MQT compiler pipeline. + +Input source strings, files, MQT `QuantumComputation` objects, Qiskit circuits, +and typed compiler programs can be combined with any supported output format. +Typed program inputs are copied by default; set `inplace=True` to consume them. +Use the typed programs directly to construct a custom pipeline stage by stage. +)pb"); +} + +} // namespace mqt diff --git a/cmake/SetupMLIR.cmake b/cmake/SetupMLIR.cmake index 8f2d049fb4..94fd768da8 100644 --- a/cmake/SetupMLIR.cmake +++ b/cmake/SetupMLIR.cmake @@ -13,6 +13,26 @@ set(MQT_MLIR_MIN_VERSION "22.1" CACHE STRING "Minimum required MLIR version") +# Attempt to load MLIR_DIR from a local .env file for developer convenience. +if(NOT DEFINED MLIR_DIR AND EXISTS "${PROJECT_SOURCE_DIR}/.env") + file(STRINGS "${PROJECT_SOURCE_DIR}/.env" MQT_CORE_DOTENV_LINES) + foreach(MQT_CORE_DOTENV_LINE IN LISTS MQT_CORE_DOTENV_LINES) + if(MQT_CORE_DOTENV_LINE MATCHES "^[ \t]*(#|$)") + continue() + endif() + + if(MQT_CORE_DOTENV_LINE MATCHES + "^[ \t]*(export[ \t]+)?MLIR_DIR[ \t]*=[ \t]*['\"]?([^'\"]+)['\"]?[ \t]*$") + file(TO_CMAKE_PATH "${CMAKE_MATCH_2}" MQT_CORE_DOTENV_MLIR_DIR) + set(MLIR_DIR + "${MQT_CORE_DOTENV_MLIR_DIR}" + CACHE PATH "Path to MLIRConfig.cmake" FORCE) + message(STATUS "Using MLIR_DIR from ${PROJECT_SOURCE_DIR}/.env: ${MLIR_DIR}") + break() + endif() + endforeach() +endif() + # MLIR must be installed on the system find_package(MLIR REQUIRED CONFIG) if(MLIR_VERSION VERSION_LESS MQT_MLIR_MIN_VERSION) diff --git a/docs/mlir/index.md b/docs/mlir/index.md index e0d48ab11b..e6cc4d55cf 100644 --- a/docs/mlir/index.md +++ b/docs/mlir/index.md @@ -25,6 +25,7 @@ interoperability, we provide {doc}`conversions ` between dialects. ```{toctree} :maxdepth: 2 +python_compiler QC QCO QTensor diff --git a/docs/mlir/python_compiler.md b/docs/mlir/python_compiler.md new file mode 100644 index 0000000000..ec48df19b7 --- /dev/null +++ b/docs/mlir/python_compiler.md @@ -0,0 +1,103 @@ +--- +file_format: mystnb +kernelspec: + name: python3 +mystnb: + number_source_lines: true +--- + +# Python Compiler Programs + +The {py:mod}`mqt.core.mlir` module exposes the compiler's intermediate +representations as move-aware Python objects. A {py:class}`QCProgram`, +{py:class}`QCOProgram`, {py:class}`JeffProgram`, or {py:class}`QIRProgram` owns +its MLIR module and prints as textual MLIR. Dialect-changing methods consume a +program by default, which avoids copying a potentially large module. Pass +`copy=True` when a source program should remain available for another pipeline +branch. + +```{code-cell} ipython3 +from mqt.core.mlir import OutputFormat, QCProgram, compile_program +``` + +## Frontends and QC + +`QCProgram` represents QC immediately after frontend translation. It can be +created from OpenQASM 3, MLIR, an MQT `QuantumComputation`, or a Qiskit +`QuantumCircuit`. + +```{code-cell} ipython3 +qasm = """OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +""" + +qc = QCProgram.from_qasm_str(qasm) +print(qc.ir) +``` + +The high-level function accepts the same frontends, including `.qasm`, `.mlir`, +and `.jeff` paths. Use `QC_IMPORT` to stop immediately after a QC frontend, or +the default `QC` format for the fully optimized QCO round trip. + +```{code-cell} ipython3 +imported = compile_program(qasm, output=OutputFormat.QC_IMPORT) +optimized_qc = compile_program(qasm) +``` + +## Building a Custom Pipeline + +Each cleanup and conversion is available as a small typed operation. This makes +pipeline construction explicit while transferring ownership rather than copying +the module. Here `qc` stays valid because the conversion was requested with +`copy=True`; `qco` is consumed by `to_qc()`. + +```{code-cell} ipython3 +qco = qc.to_qco(copy=True) +qco.cleanup() +qco.optimize(enable_hadamard_lifting=True) +final_qc = qco.to_qc() + +assert qc.is_valid +assert not qco.is_valid +print(final_qc) +``` + +The methods correspond directly to the compiler stages: + +- `QCProgram.cleanup()`, `QCProgram.to_qco()`, and `QCProgram.to_qir()` +- `QCOProgram.cleanup()`, `QCOProgram.optimize()`, `QCOProgram.to_qc()`, and + `QCOProgram.to_jeff()` +- `JeffProgram.cleanup()` and `JeffProgram.to_qco()` +- `QIRProgram.cleanup()` and `QIRProgram.llvm_ir` + +## Jeff Serialization + +Jeff serialization is now a capability of {py:class}`JeffProgram`, rather than a +separate compiler entry point. The generic compiler returns a `JeffProgram`, +which can be stored as bytes or written to a file and later restored. + +```{code-cell} ipython3 +from pathlib import Path +from tempfile import TemporaryDirectory + +with TemporaryDirectory() as directory: + path = Path(directory) / "example.jeff" + jeff = compile_program(qasm, output=OutputFormat.JEFF) + path.write_bytes(jeff.to_bytes()) + restored = compile_program(path, output=OutputFormat.QC) + +print(restored.ir) +``` + +## QIR + +Both QIR profiles are first-class pipeline targets. `QIRProgram` keeps the +lowered MLIR available through `ir` and provides LLVM IR with `llvm_ir`. + +```{code-cell} ipython3 +base_qir = compile_program(qasm, output=OutputFormat.QIR_BASE) +print(base_qir.llvm_ir) +``` diff --git a/mlir/include/mlir/Compiler/CompilerPipeline.h b/mlir/include/mlir/Compiler/CompilerPipeline.h index 54d9f039de..5bf6b71b61 100644 --- a/mlir/include/mlir/Compiler/CompilerPipeline.h +++ b/mlir/include/mlir/Compiler/CompilerPipeline.h @@ -10,13 +10,33 @@ #pragma once -#include #include +#include #include namespace mlir { class ModuleOp; +class PassManager; + +/** + * @brief Dialect checkpoints of the quantum compiler pipeline + * + * @details + * Identifies the intermediate representations at which the pipeline can be + * entered or exited. A run may enter at @ref PipelineDialect::QC or + * @ref PipelineDialect::QCO and exit at any of the four checkpoints. + */ +enum class PipelineDialect : std::uint8_t { + /// QC dialect (reference semantics) + QC, + /// QCO dialect (value semantics) + QCO, + /// QIR (Quantum Intermediate Representation) + QIR, + /// `jeff` dialect (serializable value semantics) + Jeff, +}; /** * @brief Configuration for the quantum compiler pipeline @@ -70,6 +90,8 @@ struct CompilationRecord { std::string afterQCCanon; std::string afterQIRConversion; std::string afterQIRCanon; + std::string afterJeffConversion; + std::string afterJeffCanon; }; /** @@ -77,19 +99,21 @@ struct CompilationRecord { * * @details * Provides a high-level interface for compiling quantum programs through - * the MQT compiler infrastructure. The pipeline stages are: + * the MQT compiler infrastructure. A run enters at a source dialect + * (@ref PipelineDialect::QC or @ref PipelineDialect::QCO) and exits at a + * target dialect (@ref PipelineDialect::QC, @ref PipelineDialect::QCO, + * @ref PipelineDialect::QIR, or @ref PipelineDialect::Jeff), executing only + * the stages in between: * - * 1. QC dialect (reference semantics) - imported from - * qc::QuantumComputation + * 1. QC dialect (reference semantics) - imported from qc::QuantumComputation * 2. QC cleanup pipeline * 3. QCO dialect (value semantics) - enables SSA-based optimizations * 4. QCO cleanup pipeline * 5. Quantum optimization passes * 6. QCO cleanup pipeline - * 7. QC dialect - converted back for backend lowering - * 8. QC cleanup pipeline - * 9. QIR (Quantum Intermediate Representation) - optional final lowering - * 10. QIR cleanup pipeline + * 7a. QC dialect - converted back for backend lowering, then QC cleanup, then + * optional QIR lowering and cleanup, or + * 7b. `jeff` dialect - converted for serialization, then `jeff` cleanup. * * Following MLIR best practices, simplification and dead-value cleanup are * run after each major transformation stage. @@ -103,9 +127,28 @@ class QuantumCompilerPipeline { * @brief Run the complete compilation pipeline on a module * * @details - * Executes all enabled compilation stages on the provided MLIR module. - * If recordIntermediates is enabled in the config, captures IR snapshots - * at every stage (10 snapshots total for full pipeline). + * Convenience wrapper around @ref run that enters at + * @ref PipelineDialect::QC and exits at @ref PipelineDialect::QIR if a QIR + * profile is configured, or @ref PipelineDialect::QC otherwise. + * + * @param module The MLIR module to compile + * @param record Optional pointer to record intermediate states + * @return success() if compilation succeeded, failure() otherwise + */ + LogicalResult runPipeline(ModuleOp module, + CompilationRecord* record = nullptr) const; + + /** + * @brief Run the compilation stages between two dialect checkpoints + * + * @details + * Executes the enabled compilation stages required to lower @p module from + * @p from to @p to. If recordIntermediates is enabled in the config, + * captures an IR snapshot after each stage. + * + * The target is required to be a forward lowering target. In particular, + * QIR requires exactly one QIR profile to be selected in the configuration, + * while all other targets require no QIR profile. * * Automatically configures the PassManager with: * - Timing statistics if enableTiming is true @@ -113,11 +156,13 @@ class QuantumCompilerPipeline { * - IR printing after each stage if printIRAfterAllStages is true * * @param module The MLIR module to compile + * @param from The dialect the module is currently in (QC or QCO) + * @param to The dialect to lower the module to * @param record Optional pointer to record intermediate states * @return success() if compilation succeeded, failure() otherwise */ - LogicalResult runPipeline(ModuleOp module, - CompilationRecord* record = nullptr) const; + LogicalResult run(ModuleOp module, PipelineDialect from, PipelineDialect to, + CompilationRecord* record = nullptr) const; private: /** diff --git a/mlir/include/mlir/Compiler/Programs.h b/mlir/include/mlir/Compiler/Programs.h new file mode 100644 index 0000000000..5110540889 --- /dev/null +++ b/mlir/include/mlir/Compiler/Programs.h @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Compiler/CompilerPipeline.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace qc { +class QuantumComputation; +} // namespace qc + +namespace mlir { + +class QCProgram; +class QCOProgram; +class JeffProgram; +class QIRProgram; + +/** + * @brief The QIR profile represented by a QIR program. + */ +enum class QIRProfile { + /// The QIR Base Profile. + Base, + /// The QIR Adaptive Profile. + Adaptive, +}; + +/** + * @brief Formats accepted and produced by the default compiler pipeline. + */ +enum class ProgramFormat { + /// QC directly after frontend import, without any compiler pass. + QCImport, + /// QC after the QCO optimization round trip. + QC, + /// Optimized QCO. + QCO, + /// Serializable Jeff MLIR. + Jeff, + /// QIR for the Base Profile. + QIRBase, + /// QIR for the Adaptive Profile. + QIRAdaptive, +}; + +/** + * @brief A move-aware MLIR program with a shared dialect context. + * + * @details Programs own their module and keep the context alive for its full + * lifetime. Dialect-changing operations consume an rvalue program, making + * ownership transfer explicit and avoiding expensive implicit cloning. + */ +class Program { +public: + Program(const Program&) = delete; + Program& operator=(const Program&) = delete; + Program(Program&&) noexcept = default; + Program& operator=(Program&&) noexcept = default; + virtual ~Program() = default; + + /** @brief Check whether this program still owns a module. */ + [[nodiscard]] bool isValid() const noexcept; + + /** @brief Return the program as textual MLIR. */ + [[nodiscard]] std::string str() const; + +protected: + struct Storage { + std::shared_ptr context; + OwningOpRef module; + }; + + explicit Program(Storage storage); + + /** @brief Return the owned module, throwing when the program was consumed. */ + [[nodiscard]] ModuleOp module() const; + + /** @brief Clone the owned module while sharing its immutable dialect context. + */ + [[nodiscard]] Storage cloneStorage() const; + + /** @brief Transfer module ownership to a new program. */ + [[nodiscard]] Storage releaseStorage() &&; + +private: + friend std::variant + runDefaultPipeline( + std::variant&& program, + ProgramFormat output, const QuantumCompilerConfig& config); + + Storage storage_; +}; + +/** + * @brief A QC-dialect program with reference semantics. + */ +class QCProgram final : public Program { +public: + explicit QCProgram(Storage storage) : Program(std::move(storage)) {} + + /** @brief Parse QC MLIR assembly. */ + [[nodiscard]] static QCProgram fromMLIRString(const std::string& source); + + /** @brief Parse QC MLIR assembly from a file. */ + [[nodiscard]] static QCProgram + fromMLIRFile(const std::filesystem::path& path); + + /** @brief Translate OpenQASM 3 source to QC. */ + [[nodiscard]] static QCProgram fromQASMString(const std::string& source); + + /** @brief Translate an OpenQASM 3 file to QC. */ + [[nodiscard]] static QCProgram + fromQASMFile(const std::filesystem::path& path); + + /** @brief Translate an MQT quantum computation to QC. */ + [[nodiscard]] static QCProgram + fromQuantumComputation(const ::qc::QuantumComputation& computation); + + /** @brief Create an independent QC program copy. */ + [[nodiscard]] QCProgram copy() const; + + /** @brief Run the standard QC cleanup passes in place. */ + void cleanup(); + + /** @brief Consume this program and convert it to QCO. */ + [[nodiscard]] QCOProgram intoQCO() &&; + + /** @brief Consume this program and lower it to QIR. */ + [[nodiscard]] QIRProgram intoQIR(QIRProfile profile) &&; +}; + +/** + * @brief A QCO-dialect program with value semantics. + */ +class QCOProgram final : public Program { +public: + explicit QCOProgram(Storage storage) : Program(std::move(storage)) {} + + /** @brief Parse QCO MLIR assembly. */ + [[nodiscard]] static QCOProgram fromMLIRString(const std::string& source); + + /** @brief Parse QCO MLIR assembly from a file. */ + [[nodiscard]] static QCOProgram + fromMLIRFile(const std::filesystem::path& path); + + /** @brief Create an independent QCO program copy. */ + [[nodiscard]] QCOProgram copy() const; + + /** @brief Run the standard QCO cleanup passes in place. */ + void cleanup(); + + /** @brief Run the standard QCO optimization passes in place. */ + void optimize(bool mergeSingleQubitRotations = true, + bool enableHadamardLifting = false); + + /** @brief Consume this program and convert it to QC. */ + [[nodiscard]] QCProgram intoQC() &&; + + /** @brief Consume this program and convert it to Jeff MLIR. */ + [[nodiscard]] JeffProgram intoJeff() &&; +}; + +/** + * @brief A serializable Jeff-dialect program. + */ +class JeffProgram final : public Program { +public: + explicit JeffProgram(Storage storage) : Program(std::move(storage)) {} + + /** @brief Deserialize a Jeff binary file. */ + [[nodiscard]] static JeffProgram fromFile(const std::filesystem::path& path); + + /** @brief Deserialize a Jeff binary buffer. */ + [[nodiscard]] static JeffProgram fromBytes(const std::string& bytes); + + /** @brief Create an independent Jeff program copy. */ + [[nodiscard]] JeffProgram copy() const; + + /** @brief Run the standard Jeff cleanup passes in place. */ + void cleanup(); + + /** @brief Serialize this program to a binary Jeff buffer. */ + [[nodiscard]] std::string toBytes() const; + + /** @brief Serialize this program to a binary Jeff file. */ + void write(const std::filesystem::path& path) const; + + /** @brief Consume this program and convert it to QCO. */ + [[nodiscard]] QCOProgram intoQCO() &&; +}; + +/** + * @brief A QIR-dialect program. + */ +class QIRProgram final : public Program { +public: + QIRProgram(Storage storage, QIRProfile profile); + + /** @brief Create an independent QIR program copy. */ + [[nodiscard]] QIRProgram copy() const; + + /** @brief Run QIR cleanup passes in place. */ + void cleanup(); + + /** @brief Return the selected QIR profile. */ + [[nodiscard]] QIRProfile profile() const noexcept; + + /** @brief Translate this QIR MLIR program to LLVM IR text. */ + [[nodiscard]] std::string llvmIR() const; + +private: + QIRProfile profile_; +}; + +/** @brief The program variants returned by the default compiler pipeline. */ +using CompilerProgram = + std::variant; + +/** + * @brief Run the coordinated default compiler pipeline. + * + * @details The supplied program is consumed. Call `copy()` before this function + * when the source program must remain available for another pipeline branch. + */ +[[nodiscard]] CompilerProgram +runDefaultPipeline(CompilerProgram&& program, ProgramFormat output, + const QuantumCompilerConfig& config = {}); + +} // namespace mlir diff --git a/mlir/include/mlir/Support/Passes.h b/mlir/include/mlir/Support/Passes.h index 0f10e92d44..c84b701a39 100644 --- a/mlir/include/mlir/Support/Passes.h +++ b/mlir/include/mlir/Support/Passes.h @@ -44,6 +44,14 @@ void populateQCOCleanupPipeline(mlir::PassManager& pm); */ void populateQIRCleanupPipeline(mlir::PassManager& pm, bool useAdaptive); +/** + * @brief Populate a `jeff`-oriented cleanup pipeline on the given pass manager. + * @details Adds generic cleanup and dead-value removal. This matches the QCO + * cleanup minus the QTensor-specific shrink pass, as QTensor operations no + * longer exist once lowered into the `jeff` dialect. + */ +void populateJeffCleanupPipeline(mlir::PassManager& pm); + /** * @brief Run the QC-oriented cleanup pipeline on a module. */ @@ -59,3 +67,8 @@ void populateQIRCleanupPipeline(mlir::PassManager& pm, bool useAdaptive); */ [[nodiscard]] mlir::LogicalResult runQIRCleanupPipeline(mlir::ModuleOp module, bool useAdaptive); + +/** + * @brief Run the `jeff`-oriented cleanup pipeline on a module. + */ +[[nodiscard]] mlir::LogicalResult runJeffCleanupPipeline(mlir::ModuleOp module); diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 735c12d6fd..7352a6b17d 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -10,6 +10,7 @@ add_mlir_library( MQTCompilerPipeline CompilerPipeline.cpp + Programs.cpp ADDITIONAL_HEADER_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler LINK_LIBS @@ -19,9 +20,17 @@ add_mlir_library( MLIRTransformUtils MLIRQCToQCO MLIRQCOToQC + MLIRQCOToJeff MLIRQCToQIRBase MLIRQCToQIRAdaptive MLIRQCOTransforms + MLIRQCTranslation + MLIRParser + MLIRJeffTranslation + MLIRJeffToQCO + MLIRTargetLLVMIRExport + MLIRBuiltinToLLVMIRTranslation + MLIRLLVMToLLVMIRTranslation MQT::MLIRSupport) mqt_mlir_target_use_project_options(MQTCompilerPipeline) diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index 821ccda2ee..ba373e94cf 100644 --- a/mlir/lib/Compiler/CompilerPipeline.cpp +++ b/mlir/lib/Compiler/CompilerPipeline.cpp @@ -10,6 +10,7 @@ #include "mlir/Compiler/CompilerPipeline.h" +#include "mlir/Conversion/QCOToJeff/QCOToJeff.h" #include "mlir/Conversion/QCOToQC/QCOToQC.h" #include "mlir/Conversion/QCToQCO/QCToQCO.h" #include "mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.h" @@ -18,6 +19,7 @@ #include "mlir/Support/Passes.h" #include "mlir/Support/PrettyPrinting.h" +#include #include #include #include @@ -58,174 +60,161 @@ void QuantumCompilerPipeline::configurePassManager(PassManager& pm) const { } } -LogicalResult -QuantumCompilerPipeline::runPipeline(ModuleOp module, - CompilationRecord* record) const { - if (config_.convertToQIRBase && config_.convertToQIRAdaptive) { - llvm::errs() - << "convertToQIRBase and convertToQIRAdaptive are mutually " - "exclusive; only one QIR profile can be targeted at a time.\n"; - return failure(); - } - const auto convertToQIR = - config_.convertToQIRAdaptive || config_.convertToQIRBase; +namespace { +using PopulatePasses = void (*)(PassManager&, const QuantumCompilerConfig&); + +/// A single compilation stage and its optional IR snapshot. +/// +/// A stage with a null @c populatePasses function only records or prints the +/// initial import checkpoint. Function pointers avoid the heap allocations and +/// type erasure associated with `std::function` in this hot orchestration path. +struct Stage { + StringRef name; + PopulatePasses populatePasses; + std::string CompilationRecord::* field; +}; + +void populateInitialQCCleanup(PassManager& pm, const QuantumCompilerConfig&) { + populateQCCleanupPipeline(pm); +} - // Ensure printIRAfterAllStages implies recordIntermediates - if (config_.printIRAfterAllStages && - (!config_.recordIntermediates || record == nullptr)) { - llvm::errs() << "printIRAfterAllStages requires recordIntermediates to be " - "enabled and the record pointer to be non-null.\n"; - return failure(); - } +void populateQCToQCO(PassManager& pm, const QuantumCompilerConfig&) { + pm.addPass(createQCToQCO()); +} - auto runStage = [&](auto&& populatePasses) -> LogicalResult { - PassManager pm(module.getContext()); - configurePassManager(pm); - populatePasses(pm); - return pm.run(module); - }; +void populateInitialQCOCleanup(PassManager& pm, const QuantumCompilerConfig&) { + populateQCOCleanupPipeline(pm); +} - // Determine total number of stages for progress indication - // 1. QC import - // 2. QC cleanup - // 3. QC-to-QCO conversion - // 4. QCO cleanup - // 5. Optimization passes - // 6. QCO cleanup - // 7. QCO-to-QC conversion - // 8. QC cleanup - // 9. QC-to-QIR conversion (optional) - // 10. QIR cleanup (optional) - auto totalStages = 8; - if (convertToQIR) { - totalStages += 2; +void populateOptimizations(PassManager& pm, + const QuantumCompilerConfig& config) { + if (!config.disableMergeSingleQubitRotationGates) { + pm.addPass(qco::createMergeSingleQubitRotationGates()); } - auto currentStage = 0; - - // Stage 1: QC import - if (record != nullptr && config_.recordIntermediates) { - record->afterQCImport = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "QC Import", ++currentStage, totalStages); - } + if (config.enableHadamardLifting) { + pm.addPass(qco::createHadamardLifting()); } +} - // Stage 2: QC cleanup - if (failed( - runStage([&](PassManager& pm) { populateQCCleanupPipeline(pm); }))) { - return failure(); - } - if (record != nullptr && config_.recordIntermediates) { - record->afterInitialCanon = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "Initial QC Cleanup", ++currentStage, - totalStages); - } - } - // Stage 3: QC-to-QCO conversion - if (failed(runStage([&](PassManager& pm) { pm.addPass(createQCToQCO()); }))) { - return failure(); - } - if (record != nullptr && config_.recordIntermediates) { - record->afterQCOConversion = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "QC → QCO Conversion", ++currentStage, - totalStages); - } +void populateQCOToJeff(PassManager& pm, const QuantumCompilerConfig&) { + pm.addPass(createQCOToJeff()); +} + +void populateJeffCleanup(PassManager& pm, const QuantumCompilerConfig&) { + populateJeffCleanupPipeline(pm); +} + +void populateQCOToQC(PassManager& pm, const QuantumCompilerConfig&) { + pm.addPass(createQCOToQC()); +} + +void populateFinalQCCleanup(PassManager& pm, const QuantumCompilerConfig&) { + populateQCCleanupPipeline(pm); +} + +void populateQCToQIR(PassManager& pm, const QuantumCompilerConfig& config) { + if (config.convertToQIRAdaptive) { + pm.addPass(createQCToQIRAdaptive()); + return; } - // Stage 4: QCO cleanup - if (failed( - runStage([&](PassManager& pm) { populateQCOCleanupPipeline(pm); }))) { + pm.addPass(createQCToQIRBase()); +} + +void populateQIRCleanup(PassManager& pm, const QuantumCompilerConfig& config) { + populateQIRCleanupPipeline(pm, config.convertToQIRAdaptive); +} +} // namespace + +LogicalResult QuantumCompilerPipeline::run(ModuleOp module, + const PipelineDialect from, + const PipelineDialect to, + CompilationRecord* record) const { + if (config_.convertToQIRBase && config_.convertToQIRAdaptive) { + llvm::errs() + << "convertToQIRBase and convertToQIRAdaptive are mutually " + "exclusive; only one QIR profile can be targeted at a time.\n"; return failure(); } - if (record != nullptr && config_.recordIntermediates) { - record->afterQCOCanon = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "Initial QCO Cleanup", ++currentStage, - totalStages); - } - } - // Stage 5: Optimization passes - if (failed(runStage([&](PassManager& pm) { - if (!config_.disableMergeSingleQubitRotationGates) { - pm.addPass(qco::createMergeSingleQubitRotationGates()); - } - if (config_.enableHadamardLifting) { - pm.addPass(qco::createHadamardLifting()); - } - }))) { + + if (from != PipelineDialect::QC && from != PipelineDialect::QCO) { + llvm::errs() << "The compiler pipeline can only be entered at the QC or " + "QCO dialect.\n"; return failure(); } - if (record != nullptr && config_.recordIntermediates) { - record->afterOptimization = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "Optimization Passes", ++currentStage, - totalStages); - } - } - // Stage 6: QCO cleanup - if (failed( - runStage([&](PassManager& pm) { populateQCOCleanupPipeline(pm); }))) { + if (to != PipelineDialect::QC && to != PipelineDialect::QCO && + to != PipelineDialect::QIR && to != PipelineDialect::Jeff) { + llvm::errs() << "The compiler pipeline received an unknown target " + "dialect.\n"; return failure(); } - if (record != nullptr && config_.recordIntermediates) { - record->afterOptimizationCanon = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "Final QCO Cleanup", ++currentStage, - totalStages); - } - } - // Stage 7: QCO-to-QC conversion - if (failed(runStage([&](PassManager& pm) { pm.addPass(createQCOToQC()); }))) { + + const auto hasQIRProfile = + config_.convertToQIRBase || config_.convertToQIRAdaptive; + if (to == PipelineDialect::QIR && !hasQIRProfile) { + llvm::errs() << "Lowering to QIR requires selecting either the Base or " + "Adaptive QIR profile.\n"; return failure(); } - if (record != nullptr && config_.recordIntermediates) { - record->afterQCConversion = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "QCO → QC Conversion", ++currentStage, - totalStages); - } - } - // Stage 8: QC cleanup - if (failed( - runStage([&](PassManager& pm) { populateQCCleanupPipeline(pm); }))) { + if (to != PipelineDialect::QIR && hasQIRProfile) { + llvm::errs() << "A QIR profile can only be selected when lowering to " + "QIR.\n"; return failure(); } - if (record != nullptr && config_.recordIntermediates) { - record->afterQCCanon = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "Final QC Cleanup", ++currentStage, totalStages); + + // Assemble the stages required to lower from `from` to `to`. Every run passes + // through the optimized-QCO checkpoint; the entry and exit legs are appended + // conditionally. + llvm::SmallVector stages; + if (from == PipelineDialect::QC) { + stages.push_back({"QC Import", nullptr, &CompilationRecord::afterQCImport}); + stages.push_back({"Initial QC Cleanup", &populateInitialQCCleanup, + &CompilationRecord::afterInitialCanon}); + stages.push_back({"QC → QCO Conversion", &populateQCToQCO, + &CompilationRecord::afterQCOConversion}); + } + stages.push_back({"Initial QCO Cleanup", &populateInitialQCOCleanup, + &CompilationRecord::afterQCOCanon}); + stages.push_back({"Optimization Passes", &populateOptimizations, + &CompilationRecord::afterOptimization}); + stages.push_back({"Final QCO Cleanup", &populateInitialQCOCleanup, + &CompilationRecord::afterOptimizationCanon}); + + if (to == PipelineDialect::Jeff) { + stages.push_back({"QCO → Jeff Conversion", &populateQCOToJeff, + &CompilationRecord::afterJeffConversion}); + stages.push_back({"Jeff Cleanup", &populateJeffCleanup, + &CompilationRecord::afterJeffCanon}); + } else if (to != PipelineDialect::QCO) { + stages.push_back({"QCO → QC Conversion", &populateQCOToQC, + &CompilationRecord::afterQCConversion}); + stages.push_back({"Final QC Cleanup", &populateFinalQCCleanup, + &CompilationRecord::afterQCCanon}); + if (to == PipelineDialect::QIR) { + stages.push_back({"QC → QIR Conversion", &populateQCToQIR, + &CompilationRecord::afterQIRConversion}); + stages.push_back({"QIR Cleanup", &populateQIRCleanup, + &CompilationRecord::afterQIRCanon}); } } - // Stage 9: QC-to-QIR conversion (optional) - if (convertToQIR) { - if (failed(runStage([&](PassManager& pm) { - if (config_.convertToQIRAdaptive) { - pm.addPass(createQCToQIRAdaptive()); - } else { - pm.addPass(createQCToQIRBase()); - } - }))) { + + auto runStage = [&](const PopulatePasses populatePasses) { + PassManager pm(module.getContext()); + configurePassManager(pm); + populatePasses(pm, config_); + return pm.run(module); + }; + + const auto totalStages = static_cast(stages.size()); + auto currentStage = 0; + for (const auto& stage : stages) { + if (stage.populatePasses && failed(runStage(stage.populatePasses))) { return failure(); } if (record != nullptr && config_.recordIntermediates) { - record->afterQIRConversion = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "QC → QIR Conversion", ++currentStage, - totalStages); - } + record->*(stage.field) = captureIR(module); } - // Stage 10: QIR cleanup (optional) - if (failed(runStage([&](PassManager& pm) { - populateQIRCleanupPipeline(pm, config_.convertToQIRAdaptive); - }))) { - return failure(); - } - if (record != nullptr && config_.recordIntermediates) { - record->afterQIRCanon = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "QIR Cleanup", ++currentStage, totalStages); - } + if (config_.printIRAfterAllStages) { + prettyPrintStage(module, stage.name, ++currentStage, totalStages); } } @@ -247,6 +236,15 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, return success(); } +LogicalResult +QuantumCompilerPipeline::runPipeline(ModuleOp module, + CompilationRecord* record) const { + const auto convertToQIR = + config_.convertToQIRAdaptive || config_.convertToQIRBase; + return run(module, PipelineDialect::QC, + convertToQIR ? PipelineDialect::QIR : PipelineDialect::QC, record); +} + std::string captureIR(ModuleOp module) { std::string result; llvm::raw_string_ostream os(result); diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp new file mode 100644 index 0000000000..a0c71d7a5d --- /dev/null +++ b/mlir/lib/Compiler/Programs.cpp @@ -0,0 +1,415 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Compiler/Programs.h" + +#include "ir/QuantumComputation.hpp" +#include "mlir/Conversion/JeffToQCO/JeffToQCO.h" +#include "mlir/Conversion/QCOToJeff/QCOToJeff.h" +#include "mlir/Conversion/QCOToQC/QCOToQC.h" +#include "mlir/Conversion/QCToQCO/QCToQCO.h" +#include "mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.h" +#include "mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" +#include "mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h" +#include "mlir/Dialect/QC/Translation/TranslateQuantumComputationToQC.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QTensor/IR/QTensorDialect.h" +#include "mlir/Support/Passes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace mlir { + +namespace { + +[[nodiscard]] std::shared_ptr createCompilerContext() { + DialectRegistry registry; + registry.insert(); + registerBuiltinDialectTranslation(registry); + registerLLVMDialectTranslation(registry); + + auto context = std::make_shared(registry); + context->loadAllAvailableDialects(); + return context; +} + +[[nodiscard]] OwningOpRef parseMLIRString(MLIRContext* context, + const std::string& source) { + auto module = parseSourceString(llvm::StringRef(source), context); + if (!module) { + throw std::runtime_error("Failed to parse MLIR source string."); + } + return module; +} + +[[nodiscard]] llvm::SourceMgr openSourceMgr(const std::filesystem::path& path) { + std::string errorMessage; + auto file = openInputFile(path.string(), &errorMessage); + if (!file) { + throw std::runtime_error("Failed to load file '" + path.string() + + "': " + errorMessage); + } + + llvm::SourceMgr sourceMgr; + sourceMgr.AddNewSourceBuffer(std::move(file), llvm::SMLoc()); + return sourceMgr; +} + +[[nodiscard]] OwningOpRef +parseMLIRFile(MLIRContext* context, const std::filesystem::path& path) { + const auto sourceMgr = openSourceMgr(path); + auto module = parseSourceFile(sourceMgr, context); + if (!module) { + throw std::runtime_error("Failed to parse MLIR file '" + path.string() + + "'."); + } + return module; +} + +template +void runPasses(ModuleOp module, PopulatePasses&& populatePasses, + const llvm::StringRef failureMessage) { + PassManager pm(module.getContext()); + populatePasses(pm); + if (failed(pm.run(module))) { + throw std::runtime_error(failureMessage.str()); + } +} + +} // namespace + +Program::Program(Storage storage) : storage_(std::move(storage)) {} + +bool Program::isValid() const noexcept { + return static_cast(storage_.module); +} + +ModuleOp Program::module() const { + if (!storage_.module) { + throw std::runtime_error("This program was consumed by a conversion."); + } + return *storage_.module; +} + +std::string Program::str() const { return captureIR(module()); } + +Program::Storage Program::cloneStorage() const { + const auto cloned = cast(module()->clone()); + return {storage_.context, OwningOpRef(cloned)}; +} + +Program::Storage Program::releaseStorage() && { + if (!storage_.module) { + throw std::runtime_error("This program was already consumed."); + } + return {std::move(storage_.context), std::move(storage_.module)}; +} + +QCProgram QCProgram::fromMLIRString(const std::string& source) { + auto context = createCompilerContext(); + return QCProgram({context, parseMLIRString(context.get(), source)}); +} + +QCProgram QCProgram::fromMLIRFile(const std::filesystem::path& path) { + auto context = createCompilerContext(); + return QCProgram({context, parseMLIRFile(context.get(), path)}); +} + +QCProgram QCProgram::fromQASMString(const std::string& source) { + auto context = createCompilerContext(); + auto module = qc::translateQASM3ToQC(source, context.get()); + if (!module) { + throw std::runtime_error("Failed to translate OpenQASM 3 source to QC."); + } + return QCProgram({std::move(context), std::move(module)}); +} + +QCProgram QCProgram::fromQASMFile(const std::filesystem::path& path) { + auto context = createCompilerContext(); + auto sourceMgr = openSourceMgr(path); + auto module = qc::translateQASM3ToQC(sourceMgr, context.get()); + if (!module) { + throw std::runtime_error("Failed to translate OpenQASM 3 file '" + + path.string() + "' to QC."); + } + return QCProgram({std::move(context), std::move(module)}); +} + +QCProgram +QCProgram::fromQuantumComputation(const ::qc::QuantumComputation& computation) { + auto context = createCompilerContext(); + auto module = translateQuantumComputationToQC(context.get(), computation); + if (!module) { + throw std::runtime_error("Failed to translate QuantumComputation to QC."); + } + return QCProgram({std::move(context), std::move(module)}); +} + +QCProgram QCProgram::copy() const { return QCProgram(cloneStorage()); } + +void QCProgram::cleanup() { + runPasses(module(), populateQCCleanupPipeline, + "Failed to run the QC cleanup pipeline."); +} + +QCOProgram QCProgram::intoQCO() && { + runPasses( + module(), [](PassManager& pm) { pm.addPass(createQCToQCO()); }, + "Failed to convert QC to QCO."); + return QCOProgram(std::move(*this).releaseStorage()); +} + +QIRProgram QCProgram::intoQIR(const QIRProfile profile) && { + runPasses( + module(), + [profile](PassManager& pm) { + if (profile == QIRProfile::Adaptive) { + pm.addPass(createQCToQIRAdaptive()); + } else { + pm.addPass(createQCToQIRBase()); + } + }, + "Failed to convert QC to QIR."); + auto result = QIRProgram(std::move(*this).releaseStorage(), profile); + result.cleanup(); + return result; +} + +QCOProgram QCOProgram::fromMLIRString(const std::string& source) { + auto context = createCompilerContext(); + return QCOProgram({context, parseMLIRString(context.get(), source)}); +} + +QCOProgram QCOProgram::fromMLIRFile(const std::filesystem::path& path) { + auto context = createCompilerContext(); + return QCOProgram({context, parseMLIRFile(context.get(), path)}); +} + +QCOProgram QCOProgram::copy() const { return QCOProgram(cloneStorage()); } + +void QCOProgram::cleanup() { + runPasses(module(), populateQCOCleanupPipeline, + "Failed to run the QCO cleanup pipeline."); +} + +void QCOProgram::optimize(const bool mergeSingleQubitRotations, + const bool enableHadamardLifting) { + runPasses( + module(), + [mergeSingleQubitRotations, enableHadamardLifting](PassManager& pm) { + if (mergeSingleQubitRotations) { + pm.addPass(qco::createMergeSingleQubitRotationGates()); + } + if (enableHadamardLifting) { + pm.addPass(qco::createHadamardLifting()); + } + }, + "Failed to run QCO optimization passes."); +} + +QCProgram QCOProgram::intoQC() && { + runPasses( + module(), [](PassManager& pm) { pm.addPass(createQCOToQC()); }, + "Failed to convert QCO to QC."); + return QCProgram(std::move(*this).releaseStorage()); +} + +JeffProgram QCOProgram::intoJeff() && { + runPasses( + module(), [](PassManager& pm) { pm.addPass(createQCOToJeff()); }, + "Failed to convert QCO to Jeff."); + return JeffProgram(std::move(*this).releaseStorage()); +} + +JeffProgram JeffProgram::fromFile(const std::filesystem::path& path) { + auto context = createCompilerContext(); + auto module = deserializeFromFile(context.get(), path.string()); + if (!module) { + throw std::runtime_error("Failed to deserialize Jeff file '" + + path.string() + "'."); + } + return JeffProgram({std::move(context), std::move(module)}); +} + +JeffProgram JeffProgram::fromBytes(const std::string& bytes) { + if (bytes.size() % sizeof(capnp::word) != 0U) { + throw std::invalid_argument( + "Jeff data size must be a multiple of the Cap'n Proto word size."); + } + + auto words = kj::heapArray(bytes.size() / sizeof(capnp::word)); + std::memcpy(words.begin(), bytes.data(), bytes.size()); + + auto context = createCompilerContext(); + auto module = deserialize(context.get(), words.asPtr()); + if (!module) { + throw std::runtime_error("Failed to deserialize Jeff bytes."); + } + return JeffProgram({std::move(context), std::move(module)}); +} + +JeffProgram JeffProgram::copy() const { return JeffProgram(cloneStorage()); } + +void JeffProgram::cleanup() { + runPasses(module(), populateJeffCleanupPipeline, + "Failed to run the Jeff cleanup pipeline."); +} + +std::string JeffProgram::toBytes() const { + const auto serialized = serialize(module()); + const auto bytes = serialized.asBytes(); + return {reinterpret_cast(bytes.begin()), bytes.size()}; +} + +void JeffProgram::write(const std::filesystem::path& path) const { + const auto bytes = toBytes(); + std::ofstream output(path, std::ios::binary); + if (!output) { + throw std::runtime_error("Failed to open output file '" + path.string() + + "'."); + } + output.write(bytes.data(), static_cast(bytes.size())); + if (!output) { + throw std::runtime_error("Failed to write output file '" + path.string() + + "'."); + } +} + +QCOProgram JeffProgram::intoQCO() && { + runPasses( + module(), [](PassManager& pm) { pm.addPass(createJeffToQCO()); }, + "Failed to convert Jeff to QCO."); + return QCOProgram(std::move(*this).releaseStorage()); +} + +QIRProgram::QIRProgram(Storage storage, const QIRProfile profile) + : Program(std::move(storage)), profile_(profile) {} + +QIRProgram QIRProgram::copy() const { return {cloneStorage(), profile_}; } + +void QIRProgram::cleanup() { + runPasses( + module(), + [this](PassManager& pm) { + populateQIRCleanupPipeline(pm, profile_ == QIRProfile::Adaptive); + }, + "Failed to run the QIR cleanup pipeline."); +} + +QIRProfile QIRProgram::profile() const noexcept { return profile_; } + +std::string QIRProgram::llvmIR() const { + llvm::LLVMContext context; + auto llvmModule = translateModuleToLLVMIR(module(), context); + if (!llvmModule) { + throw std::runtime_error("Failed to translate QIR MLIR to LLVM IR."); + } + std::string result; + llvm::raw_string_ostream stream(result); + llvmModule->print(stream, nullptr); + return result; +} + +CompilerProgram runDefaultPipeline(CompilerProgram&& program, + const ProgramFormat output, + const QuantumCompilerConfig& config) { + if (output == ProgramFormat::QCImport) { + if (!std::holds_alternative(program)) { + throw std::invalid_argument( + "QCImport output is only available for QC frontend input."); + } + return std::move(std::get(program)); + } + + // Jeff is deserialized into QCO before entering the shared pipeline, so it + // must skip the QC-to-QCO conversion just like native QCO input. + const auto inputDialect = std::holds_alternative(program) || + std::holds_alternative(program) + ? PipelineDialect::QCO + : PipelineDialect::QC; + Program::Storage storage = std::visit( + [](auto&& value) -> Program::Storage { + using ProgramType = std::remove_cvref_t; + if constexpr (std::is_same_v) { + return std::move(std::move(value).intoQCO()).releaseStorage(); + } else if constexpr (std::is_same_v) { + throw std::invalid_argument( + "QIR programs cannot be used as compiler pipeline input."); + } else { + return std::move(value).releaseStorage(); + } + }, + std::move(program)); + + auto pipelineConfig = config; + pipelineConfig.convertToQIRBase = output == ProgramFormat::QIRBase; + pipelineConfig.convertToQIRAdaptive = output == ProgramFormat::QIRAdaptive; + + const auto target = + output == ProgramFormat::QCO ? PipelineDialect::QCO + : output == ProgramFormat::Jeff ? PipelineDialect::Jeff + : output == ProgramFormat::QIRBase || output == ProgramFormat::QIRAdaptive + ? PipelineDialect::QIR + : PipelineDialect::QC; + if (const QuantumCompilerPipeline pipeline(pipelineConfig); + failed(pipeline.run(*storage.module, inputDialect, target))) { + throw std::runtime_error("Failed to run the default compiler pipeline."); + } + + if (target == PipelineDialect::QCO) { + return QCOProgram(std::move(storage)); + } + if (target == PipelineDialect::Jeff) { + return JeffProgram(std::move(storage)); + } + if (target == PipelineDialect::QIR) { + const auto profile = output == ProgramFormat::QIRAdaptive + ? QIRProfile::Adaptive + : QIRProfile::Base; + return QIRProgram(std::move(storage), profile); + } + return QCProgram(std::move(storage)); +} + +} // namespace mlir diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index bbd56a68c7..78b760d9ac 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -60,6 +60,11 @@ void populateQIRCleanupPipeline(PassManager& pm, bool useAdaptive) { pm.addPass(qir::createQIRSetAttributesAndMetadata({useAdaptive})); } +void populateJeffCleanupPipeline(PassManager& pm) { + addSimplificationPasses(pm); + pm.addPass(createRemoveDeadValuesPass()); +} + [[nodiscard]] LogicalResult runQCCleanupPipeline(ModuleOp module) { return runWithPassManager(module, populateQCCleanupPipeline, "Failed to run QC cleanup pipeline."); @@ -77,3 +82,8 @@ void populateQIRCleanupPipeline(PassManager& pm, bool useAdaptive) { [&](PassManager& pm) { populateQIRCleanupPipeline(pm, useAdaptive); }, "Failed to run QIR cleanup pipeline."); } + +[[nodiscard]] LogicalResult runJeffCleanupPipeline(ModuleOp module) { + return runWithPassManager(module, populateJeffCleanupPipeline, + "Failed to run Jeff cleanup pipeline."); +} diff --git a/mlir/tools/mqt-cc/CMakeLists.txt b/mlir/tools/mqt-cc/CMakeLists.txt index 66b79f1a71..0a630338ab 100644 --- a/mlir/tools/mqt-cc/CMakeLists.txt +++ b/mlir/tools/mqt-cc/CMakeLists.txt @@ -15,6 +15,8 @@ target_link_libraries( MLIRParser MLIRSupport MLIRQCTranslation + MLIRJeffTranslation + MLIRJeffToQCO MLIRBytecodeWriter MLIRTargetLLVMIRExport MLIRBuiltinToLLVMIRTranslation diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index fb52a87f25..d73f4034ef 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -9,11 +9,15 @@ */ #include "mlir/Compiler/CompilerPipeline.h" +#include "mlir/Conversion/JeffToQCO/JeffToQCO.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" +#include +#include +#include #include #include #include @@ -35,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -43,7 +48,9 @@ #include #include +#include #include +#include #include using namespace mlir; @@ -51,13 +58,26 @@ using namespace mlir; // Command-line options static llvm::cl::opt inputFilename(llvm::cl::Positional, - llvm::cl::desc(""), + llvm::cl::desc(""), llvm::cl::init("-")); +static llvm::cl::opt inputFormat( + "input-format", + llvm::cl::desc( + "Input format: auto, jeff, mlir, qco, or qasm (default: auto)"), + llvm::cl::value_desc("format"), llvm::cl::init("auto")); + static llvm::cl::opt outputFilename("o", llvm::cl::desc("Output filename"), llvm::cl::value_desc("filename"), llvm::cl::init("-")); +static llvm::cl::opt outputFormat( + "emit", + llvm::cl::desc( + "Output format: qc-import, mlir, qco, qir-base, qir-adaptive, or " + "jeff"), + llvm::cl::value_desc("format"), llvm::cl::init("mlir")); + static llvm::cl::opt convertToQIRBase("emit-qir-base", llvm::cl::desc("Convert to QIR Base Profile at the end"), @@ -98,11 +118,66 @@ static llvm::cl::opt enableHadamardLifting( llvm::cl::desc("Apply Hadamard lifting during optimization"), llvm::cl::init(false)); +namespace { + +enum class InputFormat { MLIR, QCO, QASM, Jeff }; +enum class OutputFormat { QCImport, QC, QCO, QIRBase, QIRAdaptive, Jeff }; + +struct ParsedProgram { + OwningOpRef module; + PipelineDialect dialect = PipelineDialect::QC; +}; + +[[nodiscard]] std::optional +parseInputFormat(const llvm::StringRef format, const llvm::StringRef filename) { + if (format == "mlir" || (format == "auto" && filename.ends_with(".mlir"))) { + return InputFormat::MLIR; + } + if (format == "qco") { + return InputFormat::QCO; + } + if (format == "qasm" || (format == "auto" && filename.ends_with(".qasm"))) { + return InputFormat::QASM; + } + if (format == "jeff" || (format == "auto" && filename.ends_with(".jeff"))) { + return InputFormat::Jeff; + } + if (format == "auto" && filename == "-") { + return InputFormat::MLIR; + } + return std::nullopt; +} + +[[nodiscard]] std::optional +parseOutputFormat(const llvm::StringRef format) { + if (format == "qc-import") { + return OutputFormat::QCImport; + } + if (format == "mlir" || format == "qc") { + return OutputFormat::QC; + } + if (format == "qco") { + return OutputFormat::QCO; + } + if (format == "qir-base") { + return OutputFormat::QIRBase; + } + if (format == "qir-adaptive") { + return OutputFormat::QIRAdaptive; + } + if (format == "jeff") { + return OutputFormat::Jeff; + } + return std::nullopt; +} + +} // namespace + /** * @brief Load and parse a `.qasm` file */ -static OwningOpRef loadQASMFile(StringRef filename, - MLIRContext* context) { +static OwningOpRef loadQASMFile(const StringRef filename, + MLIRContext* const context) { std::string errorMessage; auto file = openInputFile(filename, &errorMessage); if (!file) { @@ -119,8 +194,8 @@ static OwningOpRef loadQASMFile(StringRef filename, /** * @brief Load and parse an `.mlir` file */ -static OwningOpRef loadMLIRFile(StringRef filename, - MLIRContext* context) { +static OwningOpRef loadMLIRFile(const StringRef filename, + MLIRContext* const context) { std::string errorMessage; auto file = openInputFile(filename, &errorMessage); if (!file) { @@ -134,6 +209,73 @@ static OwningOpRef loadMLIRFile(StringRef filename, return parseSourceFile(sourceMgr, context); } +/** + * @brief Load and lower a `.jeff` file to QCO. + */ +static ParsedProgram loadJeffFile(const StringRef filename, + MLIRContext* const context) { + if (filename == "-") { + llvm::errs() << "Reading `jeff` from standard input is not supported.\n"; + return {}; + } + + std::string errorMessage; + if (!openInputFile(filename, &errorMessage)) { + llvm::errs() << "Failed to load file '" << filename << "': '" + << errorMessage << "'\n"; + return {}; + } + + auto module = deserializeFromFile(context, filename); + if (!module) { + llvm::errs() << "Failed to deserialize jeff file '" << filename << "'.\n"; + return {}; + } + + PassManager pm(context); + pm.addPass(createJeffToQCO()); + if (pm.run(*module).failed()) { + llvm::errs() << "Failed to convert jeff input to QCO.\n"; + return {}; + } + return {std::move(module), PipelineDialect::QCO}; +} + +/** + * @brief Write serialized `jeff` bytes to an output file or standard output. + */ +static LogicalResult writeJeffOutput(const ModuleOp module, + const StringRef filename) { + std::string errorMessage; + const auto output = openOutputFile(filename, &errorMessage); + if (!output) { + llvm::errs() << errorMessage << "\n"; + return failure(); + } + + const auto serialized = serialize(module); + const auto bytes = serialized.asBytes(); + output->os().write(reinterpret_cast(bytes.begin()), + bytes.size()); + output->os().flush(); + if (output->os().has_error()) { + llvm::errs() << "I/O error while writing output file: " << filename << "\n"; + return failure(); + } + + output->keep(); + return success(); +} + +/** + * @brief Print all compiler checkpoints that were recorded for this run. + */ +static void printRecordedStage(const StringRef title, const std::string& ir) { + if (!ir.empty()) { + llvm::outs() << "After " << title << ":\n" << ir << "\n"; + } +} + /** * @brief Write a module to an output file */ @@ -179,33 +321,71 @@ int main(int argc, char** argv) { llvm::cl::ParseCommandLineOptions(argc, argv, "MQT Compiler Collection Driver\n"); - // Set up MLIR context with all required dialects + const auto parsedInputFormat = parseInputFormat(inputFormat, inputFilename); + if (!parsedInputFormat) { + llvm::errs() << "Could not determine the input format for '" + << inputFilename << "'. Use --input-format.\n"; + return 1; + } + auto parsedOutputFormat = parseOutputFormat(outputFormat); + if (!parsedOutputFormat) { + llvm::errs() << "Unknown output format '" << outputFormat << "'.\n"; + return 1; + } + if (convertToQIRBase && convertToQIRAdaptive) { + llvm::errs() << "--emit-qir-base and --emit-qir-adaptive are mutually " + "exclusive.\n"; + return 1; + } + if ((convertToQIRBase || convertToQIRAdaptive) && + outputFormat.getNumOccurrences() != 0U) { + llvm::errs() << "--emit cannot be combined with --emit-qir-base or " + "--emit-qir-adaptive.\n"; + return 1; + } + if (convertToQIRBase) { + parsedOutputFormat = OutputFormat::QIRBase; + } else if (convertToQIRAdaptive) { + parsedOutputFormat = OutputFormat::QIRAdaptive; + } + + // Set up MLIR context with all required dialects. DialectRegistry registry; - registry - .insert(); + registry.insert(); registerBuiltinDialectTranslation(registry); registerLLVMDialectTranslation(registry); MLIRContext context(registry); context.loadAllAvailableDialects(); - // Load the input .mlir file - OwningOpRef mod; - if (inputFilename.getValue().ends_with(".qasm")) { - mod = loadQASMFile(inputFilename, &context); - } else { - mod = loadMLIRFile(inputFilename, &context); + ParsedProgram program; + switch (*parsedInputFormat) { + case InputFormat::MLIR: + program.module = loadMLIRFile(inputFilename, &context); + break; + case InputFormat::QCO: + program.module = loadMLIRFile(inputFilename, &context); + program.dialect = PipelineDialect::QCO; + break; + case InputFormat::QASM: + program.module = loadQASMFile(inputFilename, &context); + break; + case InputFormat::Jeff: + program = loadJeffFile(inputFilename, &context); + break; } - if (!mod) { + if (!program.module) { return 1; } // Configure the compiler pipeline QuantumCompilerConfig config; - config.convertToQIRBase = convertToQIRBase; - config.convertToQIRAdaptive = convertToQIRAdaptive; + config.convertToQIRBase = *parsedOutputFormat == OutputFormat::QIRBase; + config.convertToQIRAdaptive = + *parsedOutputFormat == OutputFormat::QIRAdaptive; config.recordIntermediates = recordIntermediates; config.enableTiming = enableTiming; config.enableStatistics = enableStatistics; @@ -214,42 +394,59 @@ int main(int argc, char** argv) { disableMergeSingleQubitRotationGates; config.enableHadamardLifting = enableHadamardLifting; - // Run the compilation pipeline - CompilationRecord record; - if (const QuantumCompilerPipeline pipeline(config); - pipeline.runPipeline(mod.get(), recordIntermediates ? &record : nullptr) - .failed()) { - llvm::errs() << "Compilation pipeline failed\n"; + if (*parsedOutputFormat == OutputFormat::QCImport && + program.dialect != PipelineDialect::QC) { + llvm::errs() << "--emit=qc-import requires QC frontend input.\n"; return 1; } + const auto target = + *parsedOutputFormat == OutputFormat::QCO ? PipelineDialect::QCO + : *parsedOutputFormat == OutputFormat::Jeff ? PipelineDialect::Jeff + : config.convertToQIRBase || config.convertToQIRAdaptive + ? PipelineDialect::QIR + : PipelineDialect::QC; + + // Run the compilation pipeline unless the requested QC import checkpoint is + // already represented by the frontend result. + CompilationRecord record; + if (*parsedOutputFormat != OutputFormat::QCImport) { + if (const QuantumCompilerPipeline pipeline(config); + pipeline + .run(program.module.get(), program.dialect, target, + recordIntermediates ? &record : nullptr) + .failed()) { + llvm::errs() << "Compilation pipeline failed\n"; + return 1; + } + } + if (recordIntermediates) { llvm::outs() << "=== Compilation Record ===\n"; - llvm::outs() << "After QC Import:\n" << record.afterQCImport << "\n"; - llvm::outs() << "After Initial QC Canonicalization:\n" - << record.afterInitialCanon << "\n"; - llvm::outs() << "After QC-to-QCO Conversion:\n" - << record.afterQCOConversion << "\n"; - llvm::outs() << "After Initial QCO Canonicalization:\n" - << record.afterQCOCanon << "\n"; - llvm::outs() << "After Optimization:\n" << record.afterOptimization << "\n"; - llvm::outs() << "After Final QCO Canonicalization:\n" - << record.afterOptimizationCanon << "\n"; - llvm::outs() << "After QCO-to-QC Conversion:\n" - << record.afterQCConversion << "\n"; - llvm::outs() << "After Final QC Canonicalization:\n" - << record.afterQCCanon << "\n"; - llvm::outs() << "After QC-to-QIR Conversion:\n" - << record.afterQIRConversion << "\n"; - llvm::outs() << "After QIR Canonicalization:\n" - << record.afterQIRCanon << "\n"; - } - - // Write the output - if (convertToQIRBase || convertToQIRAdaptive) { + printRecordedStage("QC Import", record.afterQCImport); + printRecordedStage("Initial QC Canonicalization", record.afterInitialCanon); + printRecordedStage("QC-to-QCO Conversion", record.afterQCOConversion); + printRecordedStage("Initial QCO Canonicalization", record.afterQCOCanon); + printRecordedStage("Optimization", record.afterOptimization); + printRecordedStage("Final QCO Canonicalization", + record.afterOptimizationCanon); + printRecordedStage("QCO-to-QC Conversion", record.afterQCConversion); + printRecordedStage("Final QC Canonicalization", record.afterQCCanon); + printRecordedStage("QC-to-QIR Conversion", record.afterQIRConversion); + printRecordedStage("QIR Canonicalization", record.afterQIRCanon); + printRecordedStage("QCO-to-Jeff Conversion", record.afterJeffConversion); + printRecordedStage("Jeff Cleanup", record.afterJeffCanon); + } + + // Write the output. + if (*parsedOutputFormat == OutputFormat::Jeff) { + if (writeJeffOutput(*program.module, outputFilename).failed()) { + return 1; + } + } else if (config.convertToQIRBase || config.convertToQIRAdaptive) { llvm::LLVMContext llvmContext; std::unique_ptr llvmMod = - translateModuleToLLVMIR(*mod, llvmContext); + translateModuleToLLVMIR(*program.module, llvmContext); if (!llvmMod) { llvm::errs() << "Failed to translate MLIR module to LLVM IR\n"; return 1; @@ -257,7 +454,8 @@ int main(int argc, char** argv) { if (writeOutput(llvmMod.get(), outputFilename).failed()) { return 1; } - } else if (writeOutput(mod.get(), outputFilename).failed()) { + } else if (writeOutput(program.module.get(), outputFilename) + .failed()) { return 1; } diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index ddc3e4ce4d..bd28856e8d 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -11,6 +11,8 @@ #include "TestCaseUtils.h" #include "ir/QuantumComputation.hpp" #include "mlir/Compiler/CompilerPipeline.h" +#include "mlir/Compiler/Programs.h" +#include "mlir/Conversion/QCToQCO/QCToQCO.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/Translation/TranslateQuantumComputationToQC.h" @@ -24,6 +26,7 @@ #include "quantum_computation_programs.h" #include +#include #include #include #include @@ -36,6 +39,7 @@ #include #include #include +#include #include #include @@ -90,7 +94,7 @@ class CompilerPipelineTest mlir::qtensor::QTensorDialect, mlir::arith::ArithDialect, mlir::cf::ControlFlowDialect, mlir::func::FuncDialect, mlir::memref::MemRefDialect, mlir::scf::SCFDialect, - mlir::LLVM::LLVMDialect>(); + mlir::LLVM::LLVMDialect, mlir::jeff::JeffDialect>(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -245,6 +249,122 @@ TEST_F(CompilerPipelineTest, HadamardLiftingPass) { EXPECT_NE(record.afterQCOCanon, record.afterOptimization); } +/** + * @brief Test: entering the pipeline at QCO yields the same result as QC + * + * @details + * The `jeff` import path hands the pipeline an already-QCO module. This test + * proves that entering at QCO (skipping the QC → QCO leg) converges to the same + * final QC IR as the ordinary QC entry point for the same program. + */ +TEST_F(CompilerPipelineTest, RunFromQCOMatchesRunFromQC) { + const auto buildQC = [this] { + return mlir::qc::QCProgramBuilder::build(context.get(), + [](mlir::qc::QCProgramBuilder& b) { + const auto q = b.allocQubit(); + b.h(q); + b.rz(1.0, q); + }); + }; + + const mlir::QuantumCompilerPipeline pipeline; + + // Reference: enter at QC. + auto fromQC = buildQC(); + ASSERT_TRUE(fromQC); + ASSERT_TRUE(pipeline + .run(fromQC.get(), mlir::PipelineDialect::QC, + mlir::PipelineDialect::QC) + .succeeded()); + + // Under test: convert to QCO first, then enter at QCO. + auto fromQCO = buildQC(); + ASSERT_TRUE(fromQCO); + { + mlir::PassManager pm(context.get()); + pm.addPass(mlir::createQCToQCO()); + ASSERT_TRUE(pm.run(fromQCO.get()).succeeded()); + } + ASSERT_TRUE(pipeline + .run(fromQCO.get(), mlir::PipelineDialect::QCO, + mlir::PipelineDialect::QC) + .succeeded()); + + EXPECT_TRUE(mlir::verify(*fromQC).succeeded()); + EXPECT_TRUE(mlir::verify(*fromQCO).succeeded()); + EXPECT_TRUE( + areModulesEquivalentWithPermutations(fromQC.get(), fromQCO.get())); +} + +/** + * @brief Test: lowering to the `jeff` dialect populates the record and verifies + */ +TEST_F(CompilerPipelineTest, RunToJeffProducesValidModule) { + auto module = mlir::qc::QCProgramBuilder::build( + context.get(), [](mlir::qc::QCProgramBuilder& b) { + const auto q = b.allocQubit(); + b.h(q); + }); + ASSERT_TRUE(module); + + mlir::QuantumCompilerConfig config; + config.recordIntermediates = true; + const mlir::QuantumCompilerPipeline pipeline(config); + + mlir::CompilationRecord record; + ASSERT_TRUE(pipeline + .run(module.get(), mlir::PipelineDialect::QC, + mlir::PipelineDialect::Jeff, &record) + .succeeded()); + + EXPECT_FALSE(record.afterJeffConversion.empty()); + EXPECT_FALSE(record.afterJeffCanon.empty()); + + auto reparsed = parseRecordedModule(record.afterJeffCanon); + ASSERT_TRUE(reparsed); + EXPECT_TRUE(mlir::verify(*reparsed).succeeded()); +} + +/** + * @brief Test: QIR lowering requires an explicit target profile + */ +TEST_F(CompilerPipelineTest, RunToQIRRequiresProfile) { + auto module = mlir::qc::QCProgramBuilder::build( + context.get(), [](mlir::qc::QCProgramBuilder& b) { b.allocQubit(); }); + ASSERT_TRUE(module); + + const mlir::QuantumCompilerPipeline pipeline; + EXPECT_TRUE(pipeline + .run(module.get(), mlir::PipelineDialect::QC, + mlir::PipelineDialect::QIR) + .failed()); +} + +/** + * @brief Test: typed programs transfer ownership between compiler dialects + */ +TEST_F(CompilerPipelineTest, TypedProgramsComposeWithoutImplicitCopies) { + const std::string qasm = R"(OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +h q; +)"; + + auto qc = mlir::QCProgram::fromQASMString(qasm); + auto qco = std::move(qc).intoQCO(); + EXPECT_FALSE(qc.isValid()); + EXPECT_TRUE(qco.isValid()); + + qco.cleanup(); + qco.optimize(); + auto roundTrip = std::move(qco).intoQC(); + EXPECT_FALSE(qco.isValid()); + roundTrip.cleanup(); + auto reparsed = parseRecordedModule(roundTrip.str()); + ASSERT_TRUE(reparsed); + EXPECT_TRUE(mlir::verify(*reparsed).succeeded()); +} + INSTANTIATE_TEST_SUITE_P( QuantumComputationPipelineProgramsTest, CompilerPipelineTest, testing::Values( diff --git a/noxfile.py b/noxfile.py index a80ca6ba80..2b4e0168c2 100755 --- a/noxfile.py +++ b/noxfile.py @@ -241,6 +241,8 @@ def stubs(session: nox.Session) -> None: "--module", "mqt.core.fomac", "--module", + "mqt.core.mlir", + "--module", "mqt.core.na", ) diff --git a/pyproject.toml b/pyproject.toml index 5524b3a2a9..2011a088ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,7 @@ build.targets = [ "mqt-core-dd-bindings", "mqt-core-fomac-bindings", "mqt-core-na-bindings", + "mqt-core-mlir-bindings", "mqt-core-qdmi-ddsim-device", "mqt-core-qdmi-na-device", "mqt-core-qdmi-sc-device", @@ -133,7 +134,7 @@ git-only = [ BUILD_MQT_CORE_BINDINGS = "ON" BUILD_MQT_CORE_TESTS = "OFF" BUILD_MQT_CORE_SHARED_LIBS = "ON" -BUILD_MQT_CORE_MLIR = "OFF" +BUILD_MQT_CORE_MLIR = "ON" [[tool.scikit-build.overrides]] if.python-version = ">=3.13" @@ -285,9 +286,12 @@ test-skip = [ ] [tool.cibuildwheel.linux] -before-all = "uv tool install sccache>=0.10.0" +before-all = """ +uv tool install sccache>=0.10.0 +curl -LsSf https://github.com/munich-quantum-software/setup-mlir/releases/download/v1.4.1/setup-mlir.sh | bash -s -- -v 22.1.7 -p /opt/llvm-22.1.7 +""" before-test = "uvx sccache --show-stats" -environment = { DEPLOY = "ON", PATH="$PATH:/root/.local/bin" } +environment = { DEPLOY = "ON", PATH="$PATH:/root/.local/bin", MLIR_DIR="/opt/llvm-22.1.7/lib/cmake/mlir" } [tool.cibuildwheel.macos] environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" } diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi new file mode 100644 index 0000000000..498948baa2 --- /dev/null +++ b/python/mqt/core/mlir.pyi @@ -0,0 +1,175 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""MQT Core MLIR compiler bindings.""" + +import enum +import os +from typing import Literal, TypeAlias, overload + +import qiskit + +import mqt.core.ir + +InputProgram: TypeAlias = ( + str + | os.PathLike[str] + | mqt.core.ir.QuantumComputation + | qiskit.circuit.QuantumCircuit + | QCProgram + | QCOProgram + | JeffProgram +) + +class QIRProfile(enum.Enum): + """QIR target profiles.""" + + BASE = 0 + """The QIR Base Profile.""" + + ADAPTIVE = 1 + """The QIR Adaptive Profile.""" + +class OutputFormat(enum.Enum): + """Default compiler output formats.""" + + QC_IMPORT = 0 + """QC directly after frontend import.""" + + QC = 1 + """QC after the optimized QCO round trip.""" + + QCO = 2 + """Optimized QCO.""" + + JEFF = 3 + """Serializable Jeff MLIR.""" + + QIR_BASE = 4 + """QIR for the Base Profile.""" + + QIR_ADAPTIVE = 5 + """QIR for the Adaptive Profile.""" + +class Program: + @property + def is_valid(self) -> bool: ... + @property + def ir(self) -> str: ... + +class QCProgram(Program): + @staticmethod + def from_mlir_str(source: str) -> QCProgram: ... + @staticmethod + def from_mlir_file(path: str | os.PathLike[str]) -> QCProgram: ... + @staticmethod + def from_qasm_str(source: str) -> QCProgram: ... + @staticmethod + def from_qasm_file(path: str | os.PathLike[str]) -> QCProgram: ... + @staticmethod + def from_quantum_computation(computation: mqt.core.ir.QuantumComputation) -> QCProgram: ... + @staticmethod + def from_qiskit(circuit: qiskit.circuit.QuantumCircuit) -> QCProgram: ... + def copy(self) -> QCProgram: ... + def cleanup(self) -> None: ... + def to_qco(self, *, copy: bool = False) -> QCOProgram: ... + def to_qir(self, profile: QIRProfile, *, copy: bool = False) -> QIRProgram: ... + +class QCOProgram(Program): + @staticmethod + def from_mlir_str(source: str) -> QCOProgram: ... + @staticmethod + def from_mlir_file(path: str | os.PathLike[str]) -> QCOProgram: ... + def copy(self) -> QCOProgram: ... + def cleanup(self) -> None: ... + def optimize(self, *, merge_single_qubit_rotations: bool = True, enable_hadamard_lifting: bool = False) -> None: ... + def to_qc(self, *, copy: bool = False) -> QCProgram: ... + def to_jeff(self, *, copy: bool = False) -> JeffProgram: ... + +class JeffProgram(Program): + @staticmethod + def from_file(path: str | os.PathLike[str]) -> JeffProgram: ... + @staticmethod + def from_bytes(data: bytes) -> JeffProgram: ... + def copy(self) -> JeffProgram: ... + def cleanup(self) -> None: ... + def to_bytes(self) -> bytes: ... + def write(self, path: str | os.PathLike[str]) -> None: ... + def to_qco(self, *, copy: bool = False) -> QCOProgram: ... + +class QIRProgram(Program): + def copy(self) -> QIRProgram: ... + def cleanup(self) -> None: ... + @property + def profile(self) -> QIRProfile: ... + @property + def llvm_ir(self) -> str: ... + +@overload +def compile_program( + program: InputProgram, + *, + output: Literal[OutputFormat.QC_IMPORT, OutputFormat.QC] = ..., + inplace: bool = False, + disable_merge_single_qubit_rotation_gates: bool = False, + enable_hadamard_lifting: bool = False, + enable_timing: bool = False, + enable_statistics: bool = False, +) -> QCProgram: + """Run the coordinated default MQT compiler pipeline. + + Input source strings, files, MQT `QuantumComputation` objects, Qiskit circuits, + and typed compiler programs can be combined with any supported output format. + Typed program inputs are copied by default; set `inplace=True` to consume them. + Use the typed programs directly to construct a custom pipeline stage by stage. + """ + +@overload +def compile_program( + program: InputProgram, + *, + output: Literal[OutputFormat.QCO], + inplace: bool = False, + disable_merge_single_qubit_rotation_gates: bool = False, + enable_hadamard_lifting: bool = False, + enable_timing: bool = False, + enable_statistics: bool = False, +) -> QCOProgram: ... +@overload +def compile_program( + program: InputProgram, + *, + output: Literal[OutputFormat.JEFF], + inplace: bool = False, + disable_merge_single_qubit_rotation_gates: bool = False, + enable_hadamard_lifting: bool = False, + enable_timing: bool = False, + enable_statistics: bool = False, +) -> JeffProgram: ... +@overload +def compile_program( + program: InputProgram, + *, + output: Literal[OutputFormat.QIR_BASE, OutputFormat.QIR_ADAPTIVE], + inplace: bool = False, + disable_merge_single_qubit_rotation_gates: bool = False, + enable_hadamard_lifting: bool = False, + enable_timing: bool = False, + enable_statistics: bool = False, +) -> QIRProgram: ... +@overload +def compile_program( + program: InputProgram, + *, + output: OutputFormat, + inplace: bool = False, + disable_merge_single_qubit_rotation_gates: bool = False, + enable_hadamard_lifting: bool = False, + enable_timing: bool = False, + enable_statistics: bool = False, +) -> QCProgram | QCOProgram | JeffProgram | QIRProgram: ... diff --git a/test/circuits/bell.jeff b/test/circuits/bell.jeff new file mode 100644 index 0000000000..3f4f45c230 Binary files /dev/null and b/test/circuits/bell.jeff differ diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py new file mode 100644 index 0000000000..072a01ab34 --- /dev/null +++ b/test/python/test_mlir.py @@ -0,0 +1,209 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Tests for the MLIR compiler Python bindings.""" + +from __future__ import annotations + +import re +from itertools import groupby +from pathlib import Path + +import pytest +from qiskit import QuantumCircuit + +from mqt.core.ir import QuantumComputation +from mqt.core.mlir import ( + JeffProgram, + OutputFormat, + QCOProgram, + QCProgram, + QIRProfile, + QIRProgram, + compile_program, +) + +MLIR_STRING = r"""module { + func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { + %c0_i64 = arith.constant 0 : i64 + %c1 = arith.constant 1 : index + %c0 = arith.constant 0 : index + %alloc = memref.alloc() : memref<2x!qc.qubit> + %0 = memref.load %alloc[%c0] : memref<2x!qc.qubit> + %1 = memref.load %alloc[%c1] : memref<2x!qc.qubit> + qc.h %0 : !qc.qubit + qc.ctrl(%0) targets (%arg0 = %1) { + qc.x %arg0 : !qc.qubit + qc.yield + } : {!qc.qubit}, {!qc.qubit} + memref.dealloc %alloc : memref<2x!qc.qubit> + return %c0_i64 : i64 + } +} +""" + +QASM_STRING = """OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +""" + + +def _sort_constants(text: str) -> str: + constant_re = re.compile(r"%.* = arith\.constant") + input_lines = text.splitlines() + output_lines = [] + for is_constant, group in groupby(input_lines, key=lambda line: bool(constant_re.match(line.strip()))): + group_lines = list(group) + output_lines.extend(sorted(group_lines) if is_constant else group_lines) + return "\n".join(output_lines) + + +def test_compile_program_jeff_file() -> None: + """Compile a `.jeff` file.""" + path = Path(__file__).parent.parent / "circuits" / "bell.jeff" + + result = compile_program(path) + assert isinstance(result, QCProgram) + assert _sort_constants(result.ir) == _sort_constants(MLIR_STRING) + + +def test_compile_program_mlir_string() -> None: + """Compile an MLIR string.""" + result = compile_program(MLIR_STRING) + assert isinstance(result, QCProgram) + assert result.ir == MLIR_STRING + + +def test_compile_program_mlir_file(tmp_path: Path) -> None: + """Compile a `.mlir` file.""" + path = tmp_path / "program.mlir" + path.write_text(MLIR_STRING, encoding="utf-8") + + result = compile_program(path) + assert isinstance(result, QCProgram) + assert result.ir == MLIR_STRING + + +def test_compile_program_qasm_string() -> None: + """Compile an OpenQASM string.""" + result = compile_program(QASM_STRING) + assert isinstance(result, QCProgram) + assert result.ir == MLIR_STRING + + +def test_compile_program_single_line_qasm_string() -> None: + """Compile a single-line OpenQASM source string.""" + result = compile_program(QASM_STRING.replace("\n", " ")) + + assert isinstance(result, QCProgram) + assert "qc.h" in result.ir + + +def test_compile_program_qasm_file(tmp_path: Path) -> None: + """Compile a `.qasm` file.""" + path = tmp_path / "program.qasm" + path.write_text(QASM_STRING, encoding="utf-8") + + result = compile_program(path) + assert isinstance(result, QCProgram) + assert result.ir == MLIR_STRING + + +def test_compile_program_quantum_computation() -> None: + """Compile a `QuantumComputation`.""" + qc = QuantumComputation(2, 2) + qc.h(0) + qc.cx(0, 1) + + result = compile_program(qc) + assert isinstance(result, QCProgram) + assert result.ir == MLIR_STRING + + +def test_compile_program_qiskit_quantum_circuit() -> None: + """Compile a `QuantumCircuit`.""" + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + + result = compile_program(qc) + assert isinstance(result, QCProgram) + assert result.ir == MLIR_STRING + + +def test_jeff_program_round_trip(tmp_path: Path) -> None: + """Store and load a `JeffProgram` through bytes and a file.""" + path = tmp_path / "program.jeff" + result = compile_program(QASM_STRING, output=OutputFormat.JEFF) + assert isinstance(result, JeffProgram) + + path.write_bytes(result.to_bytes()) + loaded = JeffProgram.from_file(path) + restored = compile_program(loaded, output=OutputFormat.QC) + assert isinstance(restored, QCProgram) + assert _sort_constants(restored.ir) == _sort_constants(MLIR_STRING) + + +def test_compile_program_jeff_input_runs_from_qco(tmp_path: Path) -> None: + """Compile a serialized Jeff program through the QCO pipeline entry point.""" + path = tmp_path / "program.jeff" + compile_program(QASM_STRING, output=OutputFormat.JEFF).write(path) + + result = compile_program(path, output=OutputFormat.QCO) + + assert isinstance(result, QCOProgram) + assert "qco." in result.ir + + +def test_program_conversions_are_composable() -> None: + """Compose frontend, cleanup, conversion, and optimization stages.""" + source = QCProgram.from_qasm_str(QASM_STRING) + qco = source.to_qco(copy=True) + assert source.is_valid + assert isinstance(qco, QCOProgram) + + qco.cleanup() + qco.optimize() + result = qco.to_qc() + assert not qco.is_valid + result.cleanup() + assert _sort_constants(result.ir) == _sort_constants(MLIR_STRING) + + +def test_compile_program_convert_to_qir() -> None: + """Compile with the QIR Base Profile output format.""" + result = compile_program(QASM_STRING, output=OutputFormat.QIR_BASE) + + assert isinstance(result, QIRProgram) + assert "; ModuleID" in result.llvm_ir + assert "@__quantum__qis__h__body" in result.llvm_ir + + +def test_compile_program_output_format_convert_to_qir() -> None: + """Lower a QC program directly to the QIR Adaptive Profile.""" + result = QCProgram.from_qasm_str(QASM_STRING).to_qir(QIRProfile.ADAPTIVE) + + assert isinstance(result, QIRProgram) + assert result.profile == QIRProfile.ADAPTIVE + assert "@__quantum__qis__h__body" in result.llvm_ir + + +def test_compile_program_qc_import_output() -> None: + """Expose QC directly after the frontend translation.""" + result = compile_program(QASM_STRING, output=OutputFormat.QC_IMPORT) + + assert isinstance(result, QCProgram) + assert "qc.h" in result.ir + + +def test_compile_program_fails_for_missing_file() -> None: + """A missing known input file extension raises an error.""" + with pytest.raises(RuntimeError, match="does not exist"): + compile_program("missing_program.qasm")