Skip to content

✨ Add Python bindings for mqt-cc#1815

Open
denialhaag wants to merge 23 commits into
mainfrom
first-mlir-python-bindings
Open

✨ Add Python bindings for mqt-cc#1815
denialhaag wants to merge 23 commits into
mainfrom
first-mlir-python-bindings

Conversation

@denialhaag

@denialhaag denialhaag commented Jun 26, 2026

Copy link
Copy Markdown
Member

Description

This PR adds a first version of Python bindings for mqt-cc. The implementation does not use the official MLIR Python bindings; instead, it relies on basic nanobind bindings. The official MLIR Python bindings are being explored in #1693.

Checklist

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

@denialhaag denialhaag added this to the MLIR Support milestone Jun 26, 2026
@denialhaag denialhaag added feature New feature or request python Anything related to Python code MLIR Anything related to MLIR labels Jun 26, 2026
@denialhaag denialhaag force-pushed the first-mlir-python-bindings branch from e457bf9 to f3ca462 Compare June 26, 2026 22:53
@denialhaag denialhaag changed the title ✨ Python bindings for mqt-qcc ✨ Python bindings for mqt-cc Jun 26, 2026
@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@denialhaag denialhaag force-pushed the first-mlir-python-bindings branch from da9a9be to 51d88a9 Compare June 26, 2026 23:54
@denialhaag denialhaag marked this pull request as ready for review June 29, 2026 10:02
@denialhaag

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added Python MLIR compiler bindings, including a new compile_program API for compiling from strings, files, IR objects, and Qiskit circuits.
    • Enabled MLIR Python package builds and added published documentation plus type stubs for better IDE support.
  • Bug Fixes

    • Improved build configuration so MLIR-related components are enabled consistently across CI, packaging, and local development.
    • Updated MLIR toolchain versions used in documentation and build environments.

Walkthrough

Adds the compile_program MLIR Python binding, enables MLIR in Python builds, and updates CI, docs, stubs, tests, and toolchain setup around the new entry point and LLVM/MLIR 22.1.7.

Changes

MLIR Python Bindings and Build Enablement

Layer / File(s) Summary
Build wiring and toolchain
CMakeLists.txt, bindings/CMakeLists.txt, bindings/mlir/CMakeLists.txt, cmake/SetupMLIR.cmake, pyproject.toml, .github/workflows/ci.yml, .readthedocs.yaml
QIR runner option conditions now exclude bindings builds; the mlir bindings subdirectory is added conditionally; a new MLIR bindings CMake target and editable stub install are defined; MLIR_DIR can be read from .env; Python package builds enable MLIR and add the MLIR bindings target; CI and docs builds pass MLIR setup inputs and version 22.1.7.
compile_program binding
bindings/mlir/register_mlir.cpp
Adds MLIR context setup, input dispatch for .jeff, .mlir, .qasm, source strings, paths, QuantumComputation, and Qiskit QuantumCircuit, runs QuantumCompilerPipeline, and registers the public compile_program function.
Python API surface and tests
python/mqt/core/mlir.pyi, noxfile.py, test/python/test_mlir.py
Adds the compile_program stub with keyword-only flags, extends stub generation to include mqt.core.mlir, and adds pytest coverage for the supported input forms, QIR lowering, and missing-file errors.
Documentation and upgrade notes
docs/mlir/index.md, docs/mlir/python_compiler.md, UPGRADING.md
Adds MLIR compiler documentation and toctree navigation, plus upgrade notes for Python-package MLIR enablement and local .env configuration.

Sequence Diagram(s)

sequenceDiagram
  participant PythonCaller
  participant mqt.core.compile_program
  participant moduleFromInputProgram
  participant mqt.core.load
  participant QuantumCompilerPipeline
  participant MLIRText
  PythonCaller->>mqt.core.compile_program: program + keyword flags
  mqt.core.compile_program->>moduleFromInputProgram: resolve input to MLIR module
  moduleFromInputProgram->>mqt.core.load: load QuantumCircuit input
  mqt.core.compile_program->>QuantumCompilerPipeline: run configured lowering pipeline
  QuantumCompilerPipeline->>MLIRText: capture final IR
  MLIRText-->>PythonCaller: return compiled module text
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

c++, packaging, usability

Suggested reviewers

  • burgholzer

Poem

A bunny hops through MLIR light,
With compile_program compiling right.
From QASM strings to QIR’s glow,
The warren’s code now leaps and knows 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding Python bindings for mqt-cc.
Description check ✅ Passed The description includes a summary, context, and checklist; only optional issue/dependency details are omitted.
Docstring Coverage ✅ Passed Docstring coverage is 95.83% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch first-mlir-python-bindings

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.23.0)
bindings/mlir/register_mlir.cpp

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

[00.16][ERROR]: unable to find a config; path .coderabbit-opengrep-fallback.yml does not exist


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pyproject.toml (1)

87-99: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep build.targets consistent with the BUILD_MQT_CORE_MLIR option.

bindings/CMakeLists.txt only defines mqt-core-mlir-bindings when BUILD_MQT_CORE_MLIR is enabled, but this target is now requested unconditionally here. Any source build that flips BUILD_MQT_CORE_MLIR=OFF will still ask scikit-build to build a non-existent target and fail before packaging starts. Either make the target list conditional on the same flag or stop enumerating the MLIR bindings target explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` around lines 87 - 99, The build target list is out of sync
with the BUILD_MQT_CORE_MLIR option, because build.targets currently always
includes mqt-core-mlir-bindings even though bindings/CMakeLists.txt only defines
it when that flag is enabled. Update the build.targets configuration to match
the same condition used for BUILD_MQT_CORE_MLIR, either by making the target
list conditional or by removing the explicit mqt-core-mlir-bindings entry so
source builds with the flag disabled do not request a missing target.
.github/workflows/ci.yml (1)

171-190: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Set explicit permissions: on these reusable-workflow jobs.

Right now each caller inherits the repository default GITHUB_TOKEN scope. On private/org repos that can be broader than needed, which is exactly the excessive-permissions warning zizmor is surfacing here. Please declare the minimum permissions on each job before uses: and only widen them if the reusable workflow actually needs more than read access.

Minimal pattern
   python-tests:
+    permissions:
+      contents: read
     uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@e7f84f39ce2d3b6c5d1d04526b8f94f98e455143

Also applies to: 202-214, 216-226, 234-254

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 171 - 190, Add explicit minimal
permissions to the reusable-workflow jobs in the CI workflow instead of relying
on inherited repository defaults, since the current `python-tests` job (and the
other cited `uses:` jobs) may be granting excessive `GITHUB_TOKEN` scope. Update
each reusable-workflow caller before its `uses:` line to declare only the
permissions required by the called workflow, widening them only if a specific
job needs more than read access.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bindings/mlir/register_mlir.cpp`:
- Around line 238-243: The dispatch in the binding code currently relies on
program.type().__name__, which can misidentify unrelated classes and miss valid
subclasses or proxies. In the register_mlir.cpp path around
moduleFromQuantumComputation handling, import qiskit.circuit.QuantumCircuit
lazily and switch the QuantumCircuit check to nb::isinstance against that real
type instead of comparing the name string.
- Around line 171-202: In moduleFromString, the early fallback to
moduleFromSourceString is swallowing real file paths with unsupported
extensions, making the unsupported-extension branch unreachable. Update the path
handling so existing file inputs are validated before deciding between
source-string parsing and file parsing, and only treat non-path-like input as
source text. Use the moduleFromString extension checks and the
moduleFromJeffFile/moduleFromMlirFile/moduleFromQasmFile dispatch to ensure
unsupported files reach the runtime_error path instead of being parsed as source
strings.

In `@pyproject.toml`:
- Around line 289-294: The build setup currently fetches setup-mlir from
releases/latest, which makes the installer mutable and non-reproducible. Update
the setup logic in pyproject.toml and the matching Read the Docs config to use a
fixed setup-mlir release URL or pinned version, and add checksum or signature
verification if the project supports it. Keep the existing installation flow
intact while replacing the latest download with a stable, explicitly referenced
release.

---

Outside diff comments:
In @.github/workflows/ci.yml:
- Around line 171-190: Add explicit minimal permissions to the reusable-workflow
jobs in the CI workflow instead of relying on inherited repository defaults,
since the current `python-tests` job (and the other cited `uses:` jobs) may be
granting excessive `GITHUB_TOKEN` scope. Update each reusable-workflow caller
before its `uses:` line to declare only the permissions required by the called
workflow, widening them only if a specific job needs more than read access.

In `@pyproject.toml`:
- Around line 87-99: The build target list is out of sync with the
BUILD_MQT_CORE_MLIR option, because build.targets currently always includes
mqt-core-mlir-bindings even though bindings/CMakeLists.txt only defines it when
that flag is enabled. Update the build.targets configuration to match the same
condition used for BUILD_MQT_CORE_MLIR, either by making the target list
conditional or by removing the explicit mqt-core-mlir-bindings entry so source
builds with the flag disabled do not request a missing target.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 28bf8e10-f083-4423-b51b-2511abc34b2c

📥 Commits

Reviewing files that changed from the base of the PR and between 95e7e05 and e7f6f48.

📒 Files selected for processing (15)
  • .github/workflows/ci.yml
  • .readthedocs.yaml
  • CMakeLists.txt
  • UPGRADING.md
  • bindings/CMakeLists.txt
  • bindings/mlir/CMakeLists.txt
  • bindings/mlir/register_mlir.cpp
  • cmake/SetupMLIR.cmake
  • docs/mlir/index.md
  • docs/mlir/python_compiler.md
  • noxfile.py
  • pyproject.toml
  • python/mqt/core/mlir.pyi
  • test/circuits/bell.jeff
  • test/python/test_mlir.py

Comment thread bindings/mlir/register_mlir.cpp Outdated
Comment thread bindings/mlir/register_mlir.cpp
Comment thread pyproject.toml
@denialhaag denialhaag changed the title ✨ Python bindings for mqt-cc ✨ Add Python bindings for mqt-cc Jun 29, 2026
@denialhaag

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
bindings/mlir/register_mlir.cpp (1)

100-120: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Reject failed parses before returning a null module.

moduleFromMlirFile(), moduleFromQasmString(), and moduleFromQasmFile() can return a null OwningOpRef; compileProgram() later dereferences module.get() in the pipeline path, so invalid input can crash the Python process instead of raising RuntimeError.

Proposed fix
 [[nodiscard]] mlir::OwningOpRef<mlir::ModuleOp>
 moduleFromMlirFile(mlir::MLIRContext* context, const std::string& path) {
@@
   llvm::SourceMgr sourceMgr;
   sourceMgr.AddNewSourceBuffer(std::move(file), llvm::SMLoc());
-  return parseSourceFile<mlir::ModuleOp>(sourceMgr, context);
+  auto module = parseSourceFile<mlir::ModuleOp>(sourceMgr, context);
+  if (!module) {
+    throw std::runtime_error(std::string("Failed to parse MLIR file '") +
+                             path + "'.");
+  }
+  return module;
 }
@@
 moduleFromQasmString(mlir::MLIRContext* context,
                      const std::string& qasmSource) {
-  return mlir::qc::translateQASM3ToQC(qasmSource, context);
+  auto module = mlir::qc::translateQASM3ToQC(qasmSource, context);
+  if (!module) {
+    throw std::runtime_error("Failed to translate OpenQASM source to MLIR.");
+  }
+  return module;
 }
@@
   llvm::SourceMgr sourceMgr;
   sourceMgr.AddNewSourceBuffer(std::move(file), llvm::SMLoc());
-  return mlir::qc::translateQASM3ToQC(sourceMgr, context);
+  auto module = mlir::qc::translateQASM3ToQC(sourceMgr, context);
+  if (!module) {
+    throw std::runtime_error(std::string("Failed to translate OpenQASM file '") +
+                             path + "'.");
+  }
+  return module;
 }

Also applies to: 126-147

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/mlir/register_mlir.cpp` around lines 100 - 120,
`moduleFromMlirFile()` and the other parse helpers can return a null
`mlir::OwningOpRef`, which later lets `compileProgram()` dereference
`module.get()` and crash. Add an explicit null check immediately after parsing
in `moduleFromMlirString()`, `moduleFromMlirFile()`, `moduleFromQasmString()`,
and `moduleFromQasmFile()`, and throw a `std::runtime_error` with a clear
parse-failure message instead of returning the null module. Make sure the
pipeline path in `compileProgram()` only proceeds with a valid module.
♻️ Duplicate comments (1)
bindings/mlir/register_mlir.cpp (1)

172-190: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t classify all one-line strings as filesystem paths.

A valid one-line source string such as OPENQASM 3.0; qubit[1] q; now reaches std::filesystem::exists() and fails as “file does not exist” instead of compiling as source. This is the same unresolved path-vs-source dispatch concern discussed earlier, but the current newline-only heuristic still misses valid inline inputs.

Safer shape
-  if (input.find('\n') != std::string::npos) {
+  if (input.find('\n') != std::string::npos ||
+      input.find("OPENQASM") != std::string::npos ||
+      input.find("module") != std::string::npos) {
     return moduleFromSourceString(context, input);
   }

For a fuller fix, dispatch known file extensions as files first, then try moduleFromSourceString() before probing ambiguous unsupported extensions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/mlir/register_mlir.cpp` around lines 172 - 190, The input dispatch
in register_mlir.cpp is treating any one-line string as a filesystem path, so
valid inline source like OPENQASM text falls through to
std::filesystem::exists() and errors out. Update the path-vs-source logic around
moduleFromSourceString and the std::filesystem::path check to prefer known file
extensions as files first, then try parsing as source before probing ambiguous
inputs with exists(). Keep the current newline and empty-input handling, but
make sure one-line source strings are not rejected as missing files.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bindings/mlir/register_mlir.cpp`:
- Line 181: The use of std::error_code in the MLIR registration code lacks a
direct standard-library include; add the missing <system_error> include
alongside the other headers in register_mlir.cpp so the type is declared
explicitly. Keep the change localized near the existing includes and ensure the
registration code that uses std::error_code continues to compile cleanly.

---

Outside diff comments:
In `@bindings/mlir/register_mlir.cpp`:
- Around line 100-120: `moduleFromMlirFile()` and the other parse helpers can
return a null `mlir::OwningOpRef`, which later lets `compileProgram()`
dereference `module.get()` and crash. Add an explicit null check immediately
after parsing in `moduleFromMlirString()`, `moduleFromMlirFile()`,
`moduleFromQasmString()`, and `moduleFromQasmFile()`, and throw a
`std::runtime_error` with a clear parse-failure message instead of returning the
null module. Make sure the pipeline path in `compileProgram()` only proceeds
with a valid module.

---

Duplicate comments:
In `@bindings/mlir/register_mlir.cpp`:
- Around line 172-190: The input dispatch in register_mlir.cpp is treating any
one-line string as a filesystem path, so valid inline source like OPENQASM text
falls through to std::filesystem::exists() and errors out. Update the
path-vs-source logic around moduleFromSourceString and the std::filesystem::path
check to prefer known file extensions as files first, then try parsing as source
before probing ambiguous inputs with exists(). Keep the current newline and
empty-input handling, but make sure one-line source strings are not rejected as
missing files.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ffc1d8e0-9617-4f32-a6e1-b7f776e754bc

📥 Commits

Reviewing files that changed from the base of the PR and between e7f6f48 and 9f7a0c3.

📒 Files selected for processing (3)
  • .readthedocs.yaml
  • bindings/mlir/register_mlir.cpp
  • pyproject.toml

Comment thread bindings/mlir/register_mlir.cpp
@denialhaag denialhaag requested a review from burgholzer June 29, 2026 12:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request MLIR Anything related to MLIR python Anything related to Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants