diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4d9370843..6692b16d14 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 57f56a5587..4e10ee2a23 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,7 +6,7 @@ please refer to the [changelog](CHANGELOG.md). ## [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. @@ -24,10 +24,15 @@ You can then point CMake to the installation directory using the 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. +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. Known limitations: 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..a900f61381 --- /dev/null +++ b/bindings/mlir/CMakeLists.txt @@ -0,0 +1,41 @@ +# 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 + MLIRQCTranslation + MLIRParser + MLIRJeffTranslation + MLIRJeffToQCO + MLIRQCOToQC + MQT::MLIRSupport + 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..63109eb503 --- /dev/null +++ b/bindings/mlir/register_mlir.cpp @@ -0,0 +1,328 @@ +/* + * 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/CompilerPipeline.h" +#include "mlir/Conversion/JeffToQCO/JeffToQCO.h" +#include "mlir/Conversion/QCOToQC/QCOToQC.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/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 // NOLINT(misc-include-cleaner) + +#include +#include +#include +#include +#include +#include +#include + +namespace mqt { + +namespace nb = nanobind; +using namespace nb::literals; + +namespace { + +/** + * @brief Construct and initialize the MLIR context used by the compiler. + */ +[[nodiscard]] std::unique_ptr createCompilerContext() { + mlir::DialectRegistry registry; + registry.insert(); + + auto context = std::make_unique(registry); + context->loadAllAvailableDialects(); + return context; +} + +/** + * @brief Deserialize a `.jeff` file and convert the program to QC. + */ +[[nodiscard]] mlir::OwningOpRef +moduleFromJeffFile(mlir::MLIRContext* context, const std::string& path) { + auto module = deserializeFromFile(context, path); + if (!module) { + throw std::runtime_error(std::string("Failed to deserialize jeff file '") + + path + "'."); + } + + mlir::PassManager pm(module->getContext()); + pm.addPass(mlir::createJeffToQCO()); + populateQCOCleanupPipeline(pm); + pm.addPass(mlir::createQCOToQC()); + populateQCCleanupPipeline(pm); + + if (mlir::failed(pm.run(*module))) { + throw std::runtime_error("Failed to convert from jeff to QC."); + } + + return module; +} + +/** + * @brief Parse an MLIR source string into a module. + */ +[[nodiscard]] mlir::OwningOpRef +moduleFromMlirString(mlir::MLIRContext* context, const std::string& text) { + return mlir::parseSourceString(llvm::StringRef(text), + context); +} + +/** + * @brief Parse a `.mlir` file into a module. + */ +[[nodiscard]] mlir::OwningOpRef +moduleFromMlirFile(mlir::MLIRContext* context, const std::string& path) { + std::string errorMessage; + auto file = mlir::openInputFile(path, &errorMessage); + if (!file) { + throw std::runtime_error(std::string("Failed to load file '") + path + + "': '" + errorMessage + "'"); + } + + llvm::SourceMgr sourceMgr; + sourceMgr.AddNewSourceBuffer(std::move(file), llvm::SMLoc()); + return parseSourceFile(sourceMgr, context); +} + +/** + * @brief Parse an OpenQASM source string and translate it to a QC program. + */ +[[nodiscard]] mlir::OwningOpRef +moduleFromQasmString(mlir::MLIRContext* context, + const std::string& qasmSource) { + return mlir::qc::translateQASM3ToQC(qasmSource, context); +} + +/** + * @brief Parse a `.qasm` file and translate it to a QC program. + */ +[[nodiscard]] mlir::OwningOpRef +moduleFromQasmFile(mlir::MLIRContext* context, const std::string& path) { + std::string errorMessage; + auto file = mlir::openInputFile(path, &errorMessage); + if (!file) { + throw std::runtime_error(std::string("Failed to load file '") + path + + "': '" + errorMessage + "'"); + } + + llvm::SourceMgr sourceMgr; + sourceMgr.AddNewSourceBuffer(std::move(file), llvm::SMLoc()); + return mlir::qc::translateQASM3ToQC(sourceMgr, context); +} + +/** + * @brief Parse a source string into an MLR module. + */ +[[nodiscard]] mlir::OwningOpRef +moduleFromSourceString(mlir::MLIRContext* context, const std::string& input) { + if (input.find("OPENQASM") != std::string::npos) { + return moduleFromQasmString(context, input); + } + + if (auto module = moduleFromMlirString(context, input)) { + return module; + } + + throw std::runtime_error("Failed to parse source string."); +} + +/** + * @brief Resolve a string to an MLIR module. + * + * @details The string can be a source string or a path to a file. + */ +[[nodiscard]] mlir::OwningOpRef +moduleFromString(mlir::MLIRContext* context, const std::string& input) { + if (input.find('\n') != std::string::npos) { + return moduleFromSourceString(context, input); + } + + const auto path = std::filesystem::path(input); + if (path.empty()) { + return moduleFromSourceString(context, input); + } + + std::error_code ec; + const auto pathExists = std::filesystem::exists(path, ec); + if (ec) { + throw std::runtime_error(std::string("Failed to inspect path '") + input + + "': " + ec.message()); + } + if (!pathExists) { + throw std::runtime_error(std::string("Input file '") + input + + "' does not exist."); + } + + const auto isFile = std::filesystem::is_regular_file(path, ec); + if (ec) { + throw std::runtime_error(std::string("Failed to inspect path '") + input + + "': " + ec.message()); + } + if (!isFile) { + throw std::runtime_error(std::string("Input path '") + input + + "' is not a file."); + } + + const auto extension = path.extension().string(); + if (extension != ".jeff" && extension != ".mlir" && extension != ".qasm") { + throw std::runtime_error(std::string("Input file '") + input + + "' has unsupported extension '" + extension + + "'."); + } + + if (extension == ".jeff") { + return moduleFromJeffFile(context, input); + } + if (extension == ".mlir") { + return moduleFromMlirFile(context, input); + } + return moduleFromQasmFile(context, input); +} + +/** + * @brief Translate a `QuantumComputation` to a QC program. + */ +[[nodiscard]] mlir::OwningOpRef +moduleFromQuantumComputation(mlir::MLIRContext* context, + const qc::QuantumComputation& computation) { + auto module = mlir::translateQuantumComputationToQC(context, computation); + if (!module) { + throw std::runtime_error("Failed to translate QuantumComputation to MLIR."); + } + return module; +} + +/** + * @brief Convert a generic Python object to an MLIR module. + */ +[[nodiscard]] mlir::OwningOpRef +moduleFromInputProgram(mlir::MLIRContext* context, const nb::object& program) { + if (nb::isinstance(program)) { + return moduleFromString(context, nb::cast(program)); + } + + if (nb::hasattr(program, "__fspath__")) { + const auto path = nb::cast( + nb::module_::import_("os").attr("fspath")(program)); + return moduleFromString(context, path); + } + + if (nb::isinstance(program)) { + const auto& qc = nb::cast(program); + return moduleFromQuantumComputation(context, qc); + } + + const auto programType = + nb::cast(program.type().attr("__name__")); + if (programType == "QuantumCircuit") { + const auto& qc = nb::cast( + nb::module_::import_("mqt.core.load").attr("load")(program)); + return moduleFromQuantumComputation(context, qc); + } + + throw std::runtime_error(std::string("Program type ") + programType + + " is not supported."); +} + +/** + * @brief Compile a program and return the final MLIR module as a string. + */ +[[nodiscard]] std::string +compileProgram(const nb::object& program, const bool convertToQIRBase, + const bool convertToQIRAdaptive, + const bool disableMergeSingleQubitRotationGates, + const bool enableHadamardLifting, const bool enableTiming, + const bool enableStatistics) { + auto context = createCompilerContext(); + auto module = moduleFromInputProgram(context.get(), program); + + mlir::QuantumCompilerConfig config; + config.convertToQIRBase = convertToQIRBase; + config.convertToQIRAdaptive = convertToQIRAdaptive; + config.disableMergeSingleQubitRotationGates = + disableMergeSingleQubitRotationGates; + config.enableHadamardLifting = enableHadamardLifting; + config.enableTiming = enableTiming; + config.enableStatistics = enableStatistics; + + const mlir::QuantumCompilerPipeline pipeline(config); + if (mlir::failed(pipeline.runPipeline(module.get()))) { + throw std::runtime_error("Compilation pipeline failed."); + } + + return mlir::captureIR(module.get()); +} + +} // namespace + +NB_MODULE(MQT_CORE_MODULE_NAME, m) { + m.doc() = R"pb( +MQT Core MLIR compiler bindings. +)pb"; + + nb::module_::import_("mqt.core.ir"); + + m.def("compile_program", &compileProgram, "program"_a, nb::kw_only(), + "convert_to_qir_base"_a = false, "convert_to_qir_adaptive"_a = false, + "disable_merge_single_qubit_rotation_gates"_a = false, + "enable_hadamard_lifting"_a = false, "enable_timing"_a = false, + "enable_statistics"_a = false, + R"pb( +Compile an input quantum program with the MQT MLIR compiler pipeline. + +Args: + program: Input program in one of the supported forms: + - Path to a `.jeff`, `.mlir`, or `.qasm` file + - MLIR or OpenQASM source string + - :class:`mqt.core.ir.QuantumComputation` + - :class:`~qiskit.circuit.QuantumCircuit` + convert_to_qir_base: Whether to lower the result to a QIR program compliant with the Base Profile. + convert_to_qir_adaptive: Whether to lower the result to QIR program compliant with the Adaptive Profile. + disable_merge_single_qubit_rotation_gates: Disable quaternion-based rotation merging. + enable_hadamard_lifting: Enable Hadamard lifting optimization. + enable_timing: Enable MLIR pass timing. + enable_statistics: Enable MLIR pass statistics. + +Returns: + The final MLIR module as text. +)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 1c4ddd12b3..5e478c1e7b 100644 --- a/docs/mlir/index.md +++ b/docs/mlir/index.md @@ -28,6 +28,7 @@ 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..4f29d4627b --- /dev/null +++ b/docs/mlir/python_compiler.md @@ -0,0 +1,78 @@ +--- +file_format: mystnb +kernelspec: + name: python3 +mystnb: + number_source_lines: true +--- + +# Python Compiler Entry Point + +The {py:mod}`mqt.core.mlir` exposes a compact compiler entry point, +{py:func}`~mqt.core.mlir.compile_program`, +that routes multiple frontend formats into the MLIR-based compiler pipeline. + +```{code-cell} ipython3 +from mqt.core.mlir import compile_program +``` + +## OpenQASM Input + +```{code-cell} ipython3 +qasm = """OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +""" + +result = compile_program(qasm) +print(result) +``` + +## File-Based Input (`.jeff` / `.mlir` / `.qasm`) + +```{code-cell} ipython3 +from pathlib import Path +from tempfile import TemporaryDirectory + +with TemporaryDirectory() as directory: + qasm_path = Path(directory) / "example.qasm" + qasm_path.write_text(qasm, encoding="utf-8") + result = compile_program(qasm_path) + +print(result) +``` + +## `QuantumComputation` Input + +```{code-cell} ipython3 +from mqt.core.ir import QuantumComputation + +qc = QuantumComputation(2, 2) +qc.h(0) +qc.cx(0, 1) + +result = compile_program(qc) +print(result) +``` + +## Qiskit `QuantumCircuit` Input + +```{code-cell} ipython3 +from qiskit import QuantumCircuit + +circuit = QuantumCircuit(2) +circuit.h(0) +circuit.cx(0, 1) + +result = compile_program(circuit) +print(result) +``` + +## Lowering to QIR + +```{code-cell} ipython3 +result = compile_program(result, convert_to_qir_base=True) +print(result) +``` 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 f1ea00d081..ab479f540d 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..fea9adc0e8 --- /dev/null +++ b/python/mqt/core/mlir.pyi @@ -0,0 +1,38 @@ +# 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.""" + +def compile_program( + program: object, + *, + convert_to_qir_base: bool = False, + convert_to_qir_adaptive: bool = False, + disable_merge_single_qubit_rotation_gates: bool = False, + enable_hadamard_lifting: bool = False, + enable_timing: bool = False, + enable_statistics: bool = False, +) -> str: + """Compile an input quantum program with the MQT MLIR compiler pipeline. + + Args: + program: Input program in one of the supported forms: + - Path to a `.jeff`, `.mlir`, or `.qasm` file + - MLIR or OpenQASM source string + - :class:`mqt.core.ir.QuantumComputation` + - :class:`~qiskit.circuit.QuantumCircuit` + convert_to_qir_base: Whether to lower the result to a QIR program compliant with the Base Profile. + convert_to_qir_adaptive: Whether to lower the result to QIR program compliant with the Adaptive Profile. + disable_merge_single_qubit_rotation_gates: Disable quaternion-based rotation merging. + enable_hadamard_lifting: Enable Hadamard lifting optimization. + enable_timing: Enable MLIR pass timing. + enable_statistics: Enable MLIR pass statistics. + + Returns: + The final MLIR module as text. + """ 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..97071b5051 --- /dev/null +++ b/test/python/test_mlir.py @@ -0,0 +1,129 @@ +# 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 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 _sort_constants(result) == _sort_constants(MLIR_STRING) + + +def test_compile_program_mlir_string() -> None: + """Compile an MLIR string.""" + result = compile_program(MLIR_STRING) + assert result == 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 result == MLIR_STRING + + +def test_compile_program_qasm_string() -> None: + """Compile an OpenQASM string.""" + result = compile_program(QASM_STRING) + assert result == MLIR_STRING + + +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 result == 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 result == 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 result == MLIR_STRING + + +def test_compile_program_convert_to_qir() -> None: + """Compile with `convert_to_qir_base` enabled.""" + result = compile_program(QASM_STRING, convert_to_qir_base=True) + + assert "module" in result + assert "llvm." in result + + +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")