diff --git a/.github/codecov.yml b/.github/codecov.yml index 2f6d5612c9..3f63a6606a 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -2,6 +2,8 @@ ignore: - "**/python" - "test/**/*" - "mlir/unittests/**/*" + # The MLIR Python bindings require an MLIR toolchain built with Python + - "python/mqt/core/mlir/**" coverage: range: 60..90 diff --git a/.gitignore b/.gitignore index d6a4939889..3657671220 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ cmake-build-* # Distribution / packaging .Python /build/ +/build_*/ /test/*/build develop-eggs/ dist/ diff --git a/bindings/CMakeLists.txt b/bindings/CMakeLists.txt index 2ba92c4400..ea15836b16 100644 --- a/bindings/CMakeLists.txt +++ b/bindings/CMakeLists.txt @@ -10,3 +10,8 @@ add_subdirectory(ir) add_subdirectory(dd) add_subdirectory(fomac) add_subdirectory(na) + +# The MLIR bindings require the MLIR submodule to be built. +if(BUILD_MQT_CORE_MLIR) + add_subdirectory(mlir) +endif() diff --git a/bindings/mlir/.clang-tidy b/bindings/mlir/.clang-tidy new file mode 100644 index 0000000000..bd71412f6a --- /dev/null +++ b/bindings/mlir/.clang-tidy @@ -0,0 +1,13 @@ +# This binding module uses the NB_MODULE macro and the MLIR C-API type-casters +# from NanobindAdaptors.h. The include-cleaner, internal-linkage, +# identifier-naming, named-parameter, value-parameter, and unused-alias checks +# cannot reason about the macro expansion and the templated casters, so they +# report false positives here. Disable them for this binding glue. +InheritParentConfig: true +Checks: | + -misc-include-cleaner, + -misc-unused-alias-decls, + -misc-use-internal-linkage, + -performance-unnecessary-value-param, + -readability-identifier-naming, + -readability-named-parameter diff --git a/bindings/mlir/CMakeLists.txt b/bindings/mlir/CMakeLists.txt new file mode 100644 index 0000000000..312921fd17 --- /dev/null +++ b/bindings/mlir/CMakeLists.txt @@ -0,0 +1,34 @@ +# 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 + +if(NOT TARGET ${MQT_CORE_TARGET_NAME}-mlir-bindings) + # collect source files + file(GLOB_RECURSE MLIR_BINDINGS_SOURCES **.cpp) + + # declare the Python module + add_mqt_python_binding_nanobind( + CORE + ${MQT_CORE_TARGET_NAME}-mlir-bindings + ${MLIR_BINDINGS_SOURCES} + MODULE_NAME + _mlir + INSTALL_DIR + ./mlir + LINK_LIBS + MQTCoreCAPI + MLIRIR) + + # install the Python stub files in editable mode for better IDE support + if(SKBUILD_STATE STREQUAL "editable") + file(GLOB_RECURSE MLIR_PYI_FILES ${PROJECT_SOURCE_DIR}/python/mqt/core/mlir/*.pyi) + install( + FILES ${MLIR_PYI_FILES} + DESTINATION ./mlir + 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..95ef78e1a0 --- /dev/null +++ b/bindings/mlir/register_mlir.cpp @@ -0,0 +1,59 @@ +/* + * 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 "mqt-core-c/Registration.h" + +#include +#include +#include +#include + +#include +#include + +namespace mqt { + +namespace nb = nanobind; + +NB_MODULE(MQT_CORE_MODULE_NAME, m) { + m.doc() = + R"pb(MQT Core MLIR - Python bindings for the MQT Compiler Collection. + +This module exposes the dialects and passes of the MQT Compiler Collection so +that quantum programs can be compiled from Python using MLIR's Python bindings.)pb"; + + m.def( + "register_dialects", + [](MlirContext context) { mqtRegisterAllDialects(context); }, + nb::arg("context"), + "Register and load all MQT Compiler Collection dialects with the given " + "context."); + + m.def( + "register_passes", []() { mqtRegisterAllPasses(); }, + "Register all MQT Compiler Collection passes with MLIR's global pass " + "registry."); + + m.def( + "import_qasm3_to_qc", + [](MlirContext context, const std::string& qasm) { + const MlirModule module = mqtImportQASM3ToQC( + context, mlirStringRefCreate(qasm.data(), qasm.size())); + if (mlirModuleIsNull(module)) { + throw std::runtime_error( + "Failed to import OpenQASM 3 program into the QC dialect"); + } + return module; + }, + nb::arg("context"), nb::arg("qasm"), + "Import an OpenQASM 3 program into a QC-dialect module."); +} + +} // namespace mqt diff --git a/mlir/include/mqt-core-c/Registration.h b/mlir/include/mqt-core-c/Registration.h new file mode 100644 index 0000000000..54667c22de --- /dev/null +++ b/mlir/include/mqt-core-c/Registration.h @@ -0,0 +1,60 @@ +/* + * 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 + */ + +#ifndef MQT_CORE_C_REGISTRATION_H +#define MQT_CORE_C_REGISTRATION_H + +#include "mlir-c/IR.h" +#include "mlir-c/Support.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Register and load all dialects of the MQT Compiler Collection with the + * given context. + * + * @details Registers the QC, QCO, QTensor, and Jeff dialects together with the + * upstream dialects (arith, func, scf, cf, memref, llvm) that the dialects, + * conversions, and transformations depend on, and loads them so that modules + * and passes can use them. + * + * @param ctx The MLIR context to populate. + */ +MLIR_CAPI_EXPORTED void mqtRegisterAllDialects(MlirContext ctx); + +/** + * @brief Register all conversion and transformation passes of the MQT Compiler + * Collection with MLIR's global pass registry. + * + * @details After this call, every MQT pass can be instantiated by name (e.g. + * via a textual pass pipeline) from Python through the standard MLIR + * PassManager API. + */ +MLIR_CAPI_EXPORTED void mqtRegisterAllPasses(void); + +/** + * @brief Import an OpenQASM 3 program into a module of the QC dialect. + * + * @param ctx The MLIR context that owns the resulting module. The QC dialect + * must be registered with this context. + * @param qasm The OpenQASM 3 source program. + * @return The resulting QC-dialect module, or a null module on error. The + * caller owns the returned module. + */ +MLIR_CAPI_EXPORTED MlirModule mqtImportQASM3ToQC(MlirContext ctx, + MlirStringRef qasm); + +#ifdef __cplusplus +} +#endif + +#endif // MQT_CORE_C_REGISTRATION_H diff --git a/mlir/lib/CAPI/.clang-tidy b/mlir/lib/CAPI/.clang-tidy new file mode 100644 index 0000000000..af6b95016c --- /dev/null +++ b/mlir/lib/CAPI/.clang-tidy @@ -0,0 +1,7 @@ +# The C-API registration translation unit includes the dialect, conversion, and +# transformation headers solely to invoke their registration entry points. The +# include-cleaner check cannot trace these uses through the registration +# functions and reports false positives, so it is disabled here. +InheritParentConfig: true +Checks: | + -misc-include-cleaner diff --git a/mlir/lib/CAPI/CMakeLists.txt b/mlir/lib/CAPI/CMakeLists.txt new file mode 100644 index 0000000000..6e0466f9b4 --- /dev/null +++ b/mlir/lib/CAPI/CMakeLists.txt @@ -0,0 +1,46 @@ +# 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 + +add_mlir_public_c_api_library( + MQTCoreCAPI + Registration.cpp + LINK_LIBS + PUBLIC + # Dialects + MLIRQCDialect + MLIRQCODialect + MLIRQTensorDialect + MLIRJeff + # Conversions + MLIRQCToQCO + MLIRQCOToQC + MLIRQCToQIRBase + MLIRQCToQIRAdaptive + MLIRJeffToQCO + MLIRQCOToJeff + # Transformations + MLIRQCTransforms + MLIRQCOTransforms + MLIRQIRTransforms + MLIRQTensorTransforms + # OpenQASM 3 import + MLIRQCTranslation + MQT::CoreQASM + MQT::CoreIR + # Upstream + MLIRIR + MLIRPass + MLIRTransforms + MLIRArithDialect + MLIRControlFlowDialect + MLIRFuncDialect + MLIRLLVMDialect + MLIRMemRefDialect + MLIRSCFDialect) + +mqt_mlir_target_use_project_options(MQTCoreCAPI) diff --git a/mlir/lib/CAPI/Registration.cpp b/mlir/lib/CAPI/Registration.cpp new file mode 100644 index 0000000000..1d6764f609 --- /dev/null +++ b/mlir/lib/CAPI/Registration.cpp @@ -0,0 +1,93 @@ +/* + * 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 "mqt-core-c/Registration.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/Transforms/Passes.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/QIR/Transforms/Passes.h" +#include "mlir/Dialect/QTensor/IR/QTensorDialect.h" +#include "mlir/Dialect/QTensor/Transforms/Passes.h" +#include "qasm3/Importer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +void mqtRegisterAllDialects(MlirContext ctx) { + mlir::MLIRContext* context = unwrap(ctx); + mlir::DialectRegistry registry; + registry.insert(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); +} + +void mqtRegisterAllPasses() { + // Common upstream transformations (canonicalization, CSE, ...) used by the + // MQT cleanup pipelines. + mlir::registerTransformsPasses(); + + // Conversions between the MQT dialects. + mlir::registerQCToQCOPasses(); + mlir::registerQCOToQCPasses(); + mlir::registerQCToQIRBasePasses(); + mlir::registerQCToQIRAdaptivePasses(); + mlir::registerJeffToQCOPasses(); + mlir::registerQCOToJeffPasses(); + + // Dialect-specific transformations. + mlir::qc::registerQCPasses(); + mlir::qco::registerQCOPasses(); + mlir::qir::registerQIRPasses(); + mlir::qtensor::registerQTensorPasses(); +} + +MlirModule mqtImportQASM3ToQC(MlirContext ctx, MlirStringRef qasm) { + mlir::MLIRContext* context = unwrap(ctx); + try { + const ::qc::QuantumComputation qc = + qasm3::Importer::imports(std::string(qasm.data, qasm.length)); + mlir::OwningOpRef module = + mlir::translateQuantumComputationToQC(context, qc); + if (!module) { + return MlirModule{nullptr}; + } + return wrap(module.release()); + } catch (const std::exception&) { + return MlirModule{nullptr}; + } +} diff --git a/mlir/lib/CMakeLists.txt b/mlir/lib/CMakeLists.txt index 959d80e2f1..6681556327 100644 --- a/mlir/lib/CMakeLists.txt +++ b/mlir/lib/CMakeLists.txt @@ -10,3 +10,8 @@ add_subdirectory(Conversion) add_subdirectory(Compiler) add_subdirectory(Dialect) add_subdirectory(Support) + +# The C-API library backs the Python bindings. +if(BUILD_MQT_CORE_BINDINGS) + add_subdirectory(CAPI) +endif() diff --git a/pyproject.toml b/pyproject.toml index 34e5c12220..6952285369 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,6 +180,8 @@ testpaths = ["test/python"] run.source = ["mqt.core"] run.omit = [ '*/_compat/*', + # The MLIR bindings require an MLIR toolchain built with Python bindings. + '*/mqt/core/mlir/*', ] run.disable_warnings = [ "no-sysmon", diff --git a/python/mqt/core/mlir/__init__.py b/python/mqt/core/mlir/__init__.py new file mode 100644 index 0000000000..1acfc72afb --- /dev/null +++ b/python/mqt/core/mlir/__init__.py @@ -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 + +"""Python bindings for the MQT Compiler Collection. + +Exposes the dialects and passes of the MQT Compiler Collection to Python via +MLIR's Python bindings. OpenQASM 3 programs are imported into the QC dialect and +returned as native :class:`mlir.ir.Module` objects, which can then be run +through any MQT pass pipeline. +""" + +from __future__ import annotations + +from .pipeline import ( + QASMProgram, + create_context, + read_qasm, + run_pipeline, + transform_to_qco, + translate_to_qc, +) + +__all__ = [ + "QASMProgram", + "create_context", + "read_qasm", + "run_pipeline", + "transform_to_qco", + "translate_to_qc", +] diff --git a/python/mqt/core/mlir/pipeline.py b/python/mqt/core/mlir/pipeline.py new file mode 100644 index 0000000000..44d19db97f --- /dev/null +++ b/python/mqt/core/mlir/pipeline.py @@ -0,0 +1,139 @@ +# 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 + +"""The MQT Compiler Collection pipeline exposed through MLIR's Python bindings. + +OpenQASM 3 programs are imported into the QC dialect, and any registered MQT +pass or pass pipeline can be run on the resulting :class:`mlir.ir.Module` via +:func:`run_pipeline`. The heavy lifting (parsing, conversions, optimizations) +runs in C++. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING + +from mlir.ir import Context # ty: ignore[unresolved-import] +from mlir.passmanager import PassManager # ty: ignore[unresolved-import] + +from ._mlir import ( # ty: ignore[unresolved-import] + import_qasm3_to_qc, + register_dialects, + register_passes, +) + +if TYPE_CHECKING: + from mlir.ir import Module # ty: ignore[unresolved-import] + +__all__ = [ + "QASMProgram", + "create_context", + "read_qasm", + "run_pipeline", + "transform_to_qco", + "translate_to_qc", +] + +# Passes are registered with MLIR's global registry exactly once per process. +register_passes() + + +@dataclass(frozen=True) +class QASMProgram: + """An in-memory OpenQASM 3 program (the ``py:qasm`` stage of the pipeline). + + Attributes: + source: The OpenQASM 3 source code. + """ + + source: str + + +def create_context() -> Context: + """Create an MLIR context with all MQT Compiler Collection dialects registered. + + Returns: + A fresh :class:`mlir.ir.Context` with the QC, QCO, QTensor, Jeff, and + supporting dialects registered and loaded. + """ + context = Context() + register_dialects(context) + return context + + +def read_qasm(source: str | Path) -> QASMProgram: + """Read an OpenQASM 3 program (the ``py:qasm`` stage of the pipeline). + + Args: + source: Either an OpenQASM 3 source string or a path to a ``.qasm`` + file. + + Returns: + The loaded program as a :class:`QASMProgram`. + """ + if isinstance(source, Path): + text = source.read_text(encoding="utf-8") + elif "\n" not in source and source.endswith(".qasm") and Path(source).is_file(): + text = Path(source).read_text(encoding="utf-8") + else: + text = source + return QASMProgram(source=text) + + +def translate_to_qc(program: QASMProgram | str, context: Context | None = None) -> Module: + """Translate an OpenQASM 3 program to the QC dialect (``mlir:qc`` stage). + + Args: + program: The program to translate, as returned by :func:`read_qasm`, or + a raw OpenQASM 3 source string. + context: The MLIR context to own the resulting module. If ``None``, a + new context with all dialects registered is created. + + Returns: + The quantum program as an :class:`mlir.ir.Module` in the QC dialect. + """ + qasm = program.source if isinstance(program, QASMProgram) else program + if context is None: + context = create_context() + return import_qasm3_to_qc(context, qasm) + + +def run_pipeline(module: Module, pipeline: str) -> Module: + """Run a textual MLIR pass pipeline on a module in place. + + Any pass of the MQT Compiler Collection (e.g. ``qc-to-qco``, + ``hadamard-lifting``, ``qc-to-qir``) can be used, as well as their + combinations. + + Args: + module: The :class:`mlir.ir.Module` to transform. + pipeline: A textual pass pipeline, e.g. ``"builtin.module(qc-to-qco)"``. + + Returns: + The same module, transformed by the pipeline. + """ + pass_manager = PassManager.parse(pipeline, context=module.context) + pass_manager.run(module.operation) + return module + + +def transform_to_qco(module: Module) -> Module: + """Transform a QC-dialect module to the QCO dialect (``mlir:qco`` stage). + + Convenience wrapper around :func:`run_pipeline` with the ``qc-to-qco`` pass. + + Args: + module: A QC-dialect :class:`mlir.ir.Module`, as returned by + :func:`translate_to_qc`. + + Returns: + The same module, transformed to the QCO dialect. + """ + return run_pipeline(module, "builtin.module(qc-to-qco)") diff --git a/test/python/mlir/test_pipeline.py b/test/python/mlir/test_pipeline.py new file mode 100644 index 0000000000..342f1768d6 --- /dev/null +++ b/test/python/mlir/test_pipeline.py @@ -0,0 +1,69 @@ +# 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 MQT Compiler Collection Python bindings.""" + +from __future__ import annotations + +import pytest + +# The MLIR bindings require MQT Core to be built with the bindings and MLIR +# enabled, and the MLIR Python runtime to be importable. Skip otherwise. +pytest.importorskip("mlir.ir") +mqt_mlir = pytest.importorskip("mqt.core.mlir") + +BELL_QASM = """OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c[0] = measure q[0]; +c[1] = measure q[1]; +""" + + +def test_translate_to_qc_produces_qc_module() -> None: + """``translate_to_qc`` translates an OpenQASM 3 program to a QC-dialect module.""" + program = mqt_mlir.read_qasm(BELL_QASM) + module = mqt_mlir.translate_to_qc(program) + ir = str(module) + assert "qc." in ir + assert "qc.h" in ir + + +def test_transform_to_qco_produces_qco_module() -> None: + """``transform_to_qco`` lowers a QC-dialect module to the QCO dialect.""" + qc_module = mqt_mlir.translate_to_qc(BELL_QASM) + qco_module = mqt_mlir.transform_to_qco(qc_module) + assert "qco." in str(qco_module) + + +def test_run_pipeline_round_trips_qc_to_qco_to_qc() -> None: + """``run_pipeline`` runs arbitrary MQT passes; QC -> QCO -> QC round-trips.""" + module = mqt_mlir.translate_to_qc(BELL_QASM) + mqt_mlir.run_pipeline(module, "builtin.module(qc-to-qco)") + assert "qco." in str(module) + mqt_mlir.run_pipeline(module, "builtin.module(qco-to-qc)") + assert "qc." in str(module) + + +def test_run_pipeline_rejects_unknown_pass() -> None: + """An unknown pass name in a pipeline raises an error.""" + module = mqt_mlir.translate_to_qc(BELL_QASM) + with pytest.raises((ValueError, RuntimeError)): + mqt_mlir.run_pipeline(module, "builtin.module(this-pass-does-not-exist)") + + +def test_pipeline_shares_context() -> None: + """The QASM -> QC -> QCO pipeline runs end to end in one context.""" + context = mqt_mlir.create_context() + program = mqt_mlir.read_qasm(BELL_QASM) + qc_module = mqt_mlir.translate_to_qc(program, context=context) + qco_module = mqt_mlir.transform_to_qco(qc_module) + assert "qco." in str(qco_module)