Skip to content

Add python bindings for the mlir QASM→QC→QCO pipeline #1783

Open
InderpalSuthar wants to merge 10 commits into
munich-quantum-toolkit:mainfrom
InderpalSuthar:unitaryhack26-fix-issue#1693
Open

Add python bindings for the mlir QASM→QC→QCO pipeline #1783
InderpalSuthar wants to merge 10 commits into
munich-quantum-toolkit:mainfrom
InderpalSuthar:unitaryhack26-fix-issue#1693

Conversation

@InderpalSuthar

Copy link
Copy Markdown

Description

This PR adds Python bindings for the MQT Compiler Collection's MLIR pipeline, exposing the (py:qasm) → (mlir:qc) → (mlir:qco) flow to Python through MLIR's own Python bindings.

A new mqt.core.mlir package provides three thin functions:

  • read_qasm(source) — load an OpenQASM 3 program (string or .qasm path)
  • translate_to_qc(program) — translate it to a QC-dialect mlir.ir.Module
  • transform_to_qco(module) — run the qc-to-qco pass, yielding a QCO-dialect module

The QC and QCO stages are returned as native mlir.ir.Module objects, so the
in-memory representation can be printed, walked, and further processed with the
standard MLIR Python API. All transformation logic stays in C++; the bindings
only trigger the existing code.

Implementation: a small C-API (mqt-core-c) that registers the QC/QCO/QTensor
dialects, imports OpenQASM 3 to the QC dialect, and runs the qc-to-qco
conversion; a nanobind module built on MLIR's NanobindAdaptors; and a Python
wrapper. Everything is gated behind a new BUILD_MQT_CORE_MLIR_PYTHON CMake
option. No existing dialects or passes were modified (~300 lines of binding code).

Dependencies / how to build: requires an LLVM/MLIR 22.1.x toolchain built with
-DMLIR_ENABLE_BINDINGS_PYTHON=ON -DLLVM_ENABLE_RTTI=ON -DLLVM_ENABLE_EH=ON. The
prebuilt MLIR toolchain currently does not ship Python bindings, so this is not yet
exercised in CI — enabling bindings in the packaged toolchain is a separate upstream
task. Built and verified locally against LLVM 22.1.7; the new pytest suite passes.

Fixes #1693

@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@burgholzer

Copy link
Copy Markdown
Member

Thanks for opening this PR @InderpalSuthar 🙏🏼

As it stands, this is not ready for review.
Both the C++ and Python linter checks fail in CI. Pre-commit fails as well.
(You can ignore the two Windows failures for now; these are unrelated).

As pointed out in other reviews to similar PRs already, we would also like to expose all of the dialects and passes from the compiler collection.

The current implementation also does not quite fit into the existing infrastructure of MQT Core. Its existing bindings live in the top-level bindings folder. The Python package lives under python/mqt/core.

@InderpalSuthar

InderpalSuthar commented Jun 14, 2026

Copy link
Copy Markdown
Author

Thanks for the feedback @burgholzer, I made significant progress :

I have exposed all dialects and passes. The capi now registers every dialect (qc, qco, QTensor, Jeff) and all conversion + transform passes, and python runs any of them via the standard PassManager with run_pipeline so it's no longer limited to qc-to-qco.

And fitting the existing infrastructure nanobind module moved to bindings/mlir/ (built with add_mqt_python_binding_nanobind, like the other bindings) and is gated on BUILD_MQT_CORE_BINDINGS + BUILD_MQT_CORE_MLIR; the python package stays under python/mqt/core/mlir/.I also merged latest main, which needed adapting the C-API to the QCToQIR → QIRBase/QIRAdaptive refactor.

(this is edited message this test failed 7 hours ago, Now all checks have passed) :
The only remaining test is cpp-tests-macos, failing on DeviceStatus.TransitionsBusyThenIdleAfterJob and DeviceStatus.MultipleConcurrentJobsKeepBusyUntilLastFinishes. These are timing-based device-status tests that poll for BUSY mid-run, which races on the macos-26 runner — they pass on Ubuntu/Windows and this PR touches no QDMI code. I think check aggregator fail only because of this, re-running jobs on a fork PR could pass the macOS job.

@denialhaag denialhaag left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also from my side, thanks for your interest in contributing to mqt-cc, @InderpalSuthar! 🙂

I briefly reviewed your PR and left some suggestions; you can find them below.

There is still one major issue: none of your code is currently being tested in our CI. As you might have noticed, the MLIR installation we use in our CI currently does not support Python bindings. We are trying to make this work as soon as possible. In the meantime, I'd be curious to know if you can run the tests locally. If so, please ensure your implementation is properly tested. This is necessary for us to consider your contribution. If you are looking for inspiration, feel free to have a look at mlir/unittests (first OpenQASM tests are being added in #1780.

text = Path(source).read_text(encoding="utf-8")
else:
text = source
return QASMProgram(source=text)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function and the QASMProgram class seem unnecessary to me. In the current state, they barely add any value. 🤔

return QASMProgram(source=text)


def translate_to_qc(program: QASMProgram | str, context: Context | None = None) -> Module:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nice to be a bit more verbose here and call this translate_qasm3_to_qc.

return module


def transform_to_qco(module: Module) -> Module:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we decide to keep this helper function, it should be called convert_qc_to_qco because a transformation between two MLIR dialects is called a conversion.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests should be expanded significantly. The tests should cover at least one example of each pass. Furthermore, the tests could be a bit more assertive and compare the output programs with the expected programs (e.g., by using regex matching).

Comment thread .gitignore
# Distribution / packaging
.Python
/build/
/build_*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change shouldn't be necessary.

Suggested change
/build_*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, the path should probably be mlir/include/CAPI/Registration.h. 🤔

Comment thread pyproject.toml
Comment on lines +183 to +184
# The MLIR bindings require an MLIR toolchain built with Python bindings.
'*/mqt/core/mlir/*',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my general point above.

Comment thread .github/codecov.yml
Comment on lines +5 to +6
# The MLIR Python bindings require an MLIR toolchain built with Python
- "python/mqt/core/mlir/**"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my general point above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Enable Python Bindings for the MQT Compiler Collection

3 participants