Skip to content

Catalyst Quantum -> QC Conversion#87

Draft
NiklasKostrzewa wants to merge 9 commits intomunich-quantum-toolkit:mainfrom
NiklasKostrzewa:qc-dialect
Draft

Catalyst Quantum -> QC Conversion#87
NiklasKostrzewa wants to merge 9 commits intomunich-quantum-toolkit:mainfrom
NiklasKostrzewa:qc-dialect

Conversation

@NiklasKostrzewa
Copy link
Copy Markdown

@NiklasKostrzewa NiklasKostrzewa commented Mar 18, 2026

Description

This PR updates the plugin to convert to MQT qc dialect instead of MQTOpt dialect.
Back conversion is started, but needs to be finished later. This draft PR is mainly for feedback and discussion.

Tests via pytest are updated for the qc dialect, but will fail, since the back conversion is not finished.
If code for testing the back conversion is commented out, the test are successfull.

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.

Kostrzewa, Niklas and others added 9 commits February 9, 2026 11:26
cmake/ExternalDependencies.cmake
  MQT_CORE_REV -> "main"

include/mlir/Conversion
  Filename CatalystQuantumToQC -> Changed in CMakeLists.txt

include/mlir/Conversion/CatalystQuantumToQC
  Filenames to QC

include/mlir/Conversion/CatalystQuantumToQC/CMakeLists.txt
  MQTOpt replaced by QC

include/mlir/Conversion/CatalystQuantumToQC/.h
  MQTOpt replaced by QC

include/mlir/Conversion/CatalystQuantumToQC/.td
  MQTOpt replaced by QC
  namespace dependent dialects: ::mlir::qc

lib/CMakelists.txt
  MQTOpt replaced by QC

lib/mqt-plugin.cpp
  #includes exclude QCToCatalystQuantum, MQTOpt replaced by QC
  change register namespaces to ::mlir::

lib/Conversion
  Filename CatalystQuantumToQC -> changed also in CMakeLists.txt

lib/Conversion/CatalystQuantumToQC/CMakeLists.txt
  MLIRQCDialect in add_mlir_library
  MQTOpt replaced by QC

lib/Conversion/CatalystQuantumToQC/.cpp
  MQTOpt replaced by QC
…tes (based of catalyst -> mqtopt conversions)
…ugin to test quantum -> qc conversion (commented out qc -> quantum conversion
@NiklasKostrzewa
Copy link
Copy Markdown
Author

For discussion:
Some newer commits in the MQT core repository fails compiling, so this is based for now on a specific commit.
One Attribute for the qc qubit type was necessary, see fork: https://github.com/NiklasKostrzewa/core/tree/test_Memref_explicit, which is the MQT core version, which is used. But maybe this is fix in later commits in the original MQT core repository

@NiklasKostrzewa
Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 18, 2026

✅ Actions performed

Full review triggered.

1 similar comment
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 18, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 18, 2026

📝 Walkthrough

Summary by CodeRabbit

  • Refactor
    • Switched conversion pipeline from MQTOpt to QC dialect; passes renamed to catalystquantum-to-qc and qc-to-catalystquantum.
    • Plugin and pass registrations updated to expose QC dialect and QC pass group instead of MQTOpt.
  • Behavior
    • New QC→Catalyst and Catalyst→QC conversion implementations added; legacy MQTOpt conversions removed.
  • Tests
    • Test suite updated to use QC-based roundtrip pipelines and expectations.

Walkthrough

This pull request replaces MQTOpt-based conversion passes and build wiring with QC-based equivalents: deletes CatalystQuantum↔MQTOpt and MQTOpt↔CatalystQuantum passes, adds CatalystQuantum↔QC and QC↔CatalystQuantum passes and TableGen/CMake entries, updates plugin and tests, and tweaks an external dependency cache variable.

Changes

Cohort / File(s) Summary
Top-level CMake updates
include/mlir/Conversion/CMakeLists.txt, lib/Conversion/CMakeLists.txt
Swapped subdirectory entries to remove MQTOpt conversions and add QC conversions.
Removed MQTOpt CMake targets
include/mlir/Conversion/CatalystQuantumToMQTOpt/CMakeLists.txt, include/mlir/Conversion/MQTOptToCatalystQuantum/CMakeLists.txt
Deleted TableGen, LLVM target definitions, public tablegen targets, and mlir_doc wiring for MQTOpt passes.
Added QC CMake targets
include/mlir/Conversion/CatalystQuantumToQC/CMakeLists.txt, include/mlir/Conversion/QCToCatalystQuantum/CMakeLists.txt
Added LLVM target definitions, mlir_tablegen invocations, public tablegen targets, and mlir_doc wiring for QC passes.
TableGen / Pass specs
include/mlir/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.td, include/mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.td
Renamed pass definitions and metadata from MQTOpt→QC, updated pass names, summaries, descriptions, and dependent dialects.
Pass headers
include/mlir/Conversion/CatalystQuantumToQC/*.h, include/mlir/Conversion/QCToCatalystQuantum/*.h
Updated include paths to use generated QC pass headers instead of MQTOpt variants.
Removed conversion implementations
lib/Conversion/CatalystQuantumToMQTOpt/CatalystQuantumToMQTOpt.cpp, lib/Conversion/MQTOptToCatalystQuantum/MQTOptToCatalystQuantum.cpp
Deleted full MLIR conversion pass implementations, patterns, and type converters for MQTOpt conversions.
Added conversion implementations
lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp, lib/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.cpp
Added full MLIR conversion passes, type converters, and OpConversionPattern implementations for Catalyst↔QC conversions.
Plugin & build linkage
lib/mqt-plugin.cpp, lib/CMakeLists.txt, lib/Conversion/.../CMakeLists.txt
Switched plugin registration and add_llvm_library LINK_LIBS from MQTOpt targets/dialect to QC equivalents; updated include/dependency references and coverage flags.
Tests
test/test_plugin.py
Updated tests and pipelines to use catalystquantum→qc and qc→catalystquantum flow, file names, expectations, and helper signatures.
External dependencies
cmake/ExternalDependencies.cmake
Changed MQT_CORE_REV to "test_Memref_explicit" (inline comment added) and MQT_CORE_REPO_OWNER to "NiklasKostrzewa".

Sequence Diagram(s)

sequenceDiagram
    participant Source as Catalyst Quantum IR
    participant Conv1 as CatalystQuantumToQC Pass
    participant QC as QC Dialect
    participant Conv2 as QCToCatalystQuantum Pass
    participant Target as Catalyst Quantum IR

    Source->>Conv1: submit IR for conversion
    Conv1->>Conv1: apply type converter (Qubit/Qureg)
    Conv1->>Conv1: apply OpConversionPatterns (Alloc/Measure/Custom/etc.)
    Conv1->>QC: emit QC operations
    QC->>Conv2: submit QC IR for conversion
    Conv2->>Conv2: apply type converter (QC Qubit/MemRef)
    Conv2->>Conv2: apply OpConversionPatterns (Load/Store/Measure/etc.)
    Conv2->>Target: emit Catalyst Quantum operations
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

feature

Suggested reviewers

  • burgholzer

Poem

🐰 I hopped from MQTOpt to QC so spry,
I mapped each qubit and memref high,
Passes dance, types convert with cheer,
Catalyst and QC now draw near,
A rabbit's code-hop, crisp and sly.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Catalyst Quantum -> QC Conversion' directly and concisely describes the main change: replacing MQTOpt dialect with QC dialect in the conversion pipeline.
Description check ✅ Passed The description explains the main objective (converting to QC dialect instead of MQTOpt), acknowledges incomplete back-conversion, notes test failures due to unfinished work, and identifies which checklist items remain unchecked.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Tip

You can get early access to new features in CodeRabbit.

Enable the early_access setting to enable early access features such as new models, tools, and more.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

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

⚠️ Outside diff range comments (1)
test/test_plugin.py (1)

65-78: ⚠️ Potential issue | 🟠 Major

Don't delete every top-level *.mlir file.

This autouse fixture runs from Path.cwd() and unlinks all top-level .mlir files before and after each test. When pytest runs in the repo root, that will remove checked-in MLIR fixtures in addition to the generated intermediates.

🧹 Limit cleanup to this test module's generated files
 def _cleanup_mlir_artifacts() -> None:
     """Clean up MLIR intermediate files and directories created by Catalyst.
@@
     # Remove all module_N directories
     for module_dir in mlir_dir.glob("module_*"):
         if module_dir.is_dir():
             shutil.rmtree(module_dir)
-    # Remove any loose .mlir files
-    for mlir_file in mlir_dir.glob("*.mlir"):
-        mlir_file.unlink()
+    # Remove only the loose files this test module creates.
+    for name in (MLIR_FILE_INIT, MLIR_FILE_TO_QC, MLIR_FILE_TO_CATALYST):
+        mlir_file = mlir_dir / name
+        if mlir_file.exists():
+            mlir_file.unlink()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test_plugin.py` around lines 65 - 78, The cleanup function
_cleanup_mlir_artifacts currently uses Path.cwd() and unlinks all top-level
"*.mlir" files which can delete checked-in fixtures; change it to only remove
MLIR files that are part of generated module_N directories (e.g., iterate
module_dir.glob("*.mlir") and unlink those, and remove module_dir via
shutil.rmtree only when module_dir.is_dir()), and stop unlinking mlir files
directly from Path.cwd() so files at the repo root are not touched.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmake/ExternalDependencies.cmake`:
- Around line 18-21: Replace the temporary fork/branch references in the
external dependency variables: set MQT_CORE_REPO_OWNER back to the official
owner ("munich-quantum-toolkit") and change MQT_CORE_REV from the branch name
("test_Memref_explicit") to a stable pinned commit hash (replace the branch with
the specific commit hash you intend to use), and update/remove the inconsistent
inline comment referencing 7d1d7fb so the REV and comment match; these changes
are located where MQT_CORE_REV and MQT_CORE_REPO_OWNER are defined.

In `@include/mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.td`:
- Around line 11-22: The QCToCatalystQuantum pass (QCToCatalystQuantum) is
defined but not registered because registerQCToCatalystQuantumPasses() is
commented out in lib/mqt-plugin.cpp, so runtime lookups (e.g.
`@apply_pass`("mqt.qc-to-catalystquantum") and DEFAULT_PIPELINE referencing
"qc-to-catalystquantum") fail; to fix, uncomment and enable the call to
registerQCToCatalystQuantumPasses() in lib/mqt-plugin.cpp (or add conditional
registration gated by a readiness flag) so that the pass is registered with MLIR
when the implementation/back-conversion is complete and tests can find
mqt.qc-to-catalystquantum.

In `@lib/CMakeLists.txt`:
- Around line 25-26: The plugin module currently links only CatalystQuantumToQC
via the add_llvm_library(${TARGET_NAME} MODULE mqt-plugin.cpp LINK_LIBS
CatalystQuantumToQC) invocation; update that add_llvm_library call to also link
QCToCatalystQuantum so the back-conversion pass is exported by the plugin.
Locate the add_llvm_library line (with TARGET_NAME and mqt-plugin.cpp) and add
QCToCatalystQuantum to the LINK_LIBS list alongside CatalystQuantumToQC.

In `@lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp`:
- Line 319: The local variable qcOp is unused and should be removed: delete the
declaration "Operation* qcOp = nullptr;" and replace any assignment like "qcOp =
rewriter.create(...);" with just "rewriter.create(...);", relying on the side
effects of rewriter.create; ensure no other code references qcOp (search for
qcOp) and compile to verify the -Wunused-but-set-variable warning is resolved.
- Around line 446-515: The adjoint flag (op.getAdjoint()) is ignored for
parameterized gates (RX/RY/RZ, PhaseShift/P, CRX/CRY/CRZ,
ControlledPhaseShift/POp, IsingXY/IsingXX/IsingYY/IsingZZ and their lowering
macros CREATE_* like CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP and
CREATE_TWO_TARGET_ONE_PARAMETER_GATE_OP), so update the lowering to check
op.getAdjoint() and emit the adjointed operation: for single-parameter rotations
and phase gates negate the parameter (use -paramsValues[0]) when adjoint is
true; for controlled variants negate the same parameter passed into the inner
rewriter.create<qc::RXOp/RYOp/RZOp/POp>; for IsingXY negate the first parameter
(keep the added pi as-is) when adjoint is true; and for
CREATE_TWO_TARGET_ONE_PARAMETER_GATE_OP usages (RXX/RYY/RZZ) negate the
parameter when adjoint is set so the lowered qcOp preserves semantics. Ensure
you reference and modify the branches that inspect gateName (e.g., the RX/RY/RZ,
PhaseShift, CRX/CRY/CRZ, ControlledPhaseShift, Ising* branches) and use
op.getAdjoint() to decide whether to pass paramsValues[0] or -paramsValues[0].

In `@lib/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.cpp`:
- Around line 39-329: The file defines conversions
(QCToCatalystQuantumTypeConverter, ConvertQCAlloc, ConvertQCDealloc,
ConvertQCMeasure, ConvertQCLoad, ConvertQCStore, and helpers like
extractControlInfo/extractOperands) but never implements or registers the pass;
add a pass implementation using GEN_PASS_DEF_QCTOCATALYSTQUANTUM that defines
QCToCatalystQuantum::runOnOperation(), builds a ConversionTarget, instantiates
QCToCatalystQuantumTypeConverter, populates a RewritePatternSet with the
provided OpConversionPattern classes (ConvertQCAlloc, ConvertQCDealloc,
ConvertQCMeasure, ConvertQCLoad, ConvertQCStore, etc.), marks legal/illegal ops
appropriately, and call applyFullConversion/applyPartialConversion; finally
register the pass so the declared pass actually runs in tests.
- Around line 319-324: The InsertOp result is being dropped which loses the
write; instead capture the value returned by
rewriter.create<catalyst::quantum::InsertOp> (the new !quantum.reg) and thread
it to later users: replace subsequent uses of the original memref/register SSA
value (the value from adaptor.getMemref()) with the InsertOp result (or update
your value-mapping used during conversion) before calling rewriter.eraseOp(op),
so the write becomes the SSA value consumed by later ops (refer to
catalyst::quantum::InsertOp, adaptor.getMemref(), adaptor.getValue(), and the
current rewriter.eraseOp(op) call).

In `@lib/mqt-plugin.cpp`:
- Around line 46-47: Back-conversion registration
(mqt::ir::conversions::registerQCToCatalystQuantumPasses) is commented out which
breaks tests that expect the full roundtrip (DEFAULT_PIPELINE includes
"qc-to-catalystquantum"); add a clear TODO/FIXME above the commented line that
names registerQCToCatalystQuantumPasses and references DEFAULT_PIPELINE, and
either (a) briefly describe what remains to be implemented for back-conversion
or (b) add a short conditional/feature-flag note explaining why it is disabled
so future readers/tests can find and re-enable it.
- Around line 11-13: The build is failing because QCToCatalystQuantum.h depends
on a generated QCToCatalystQuantum.h.inc that isn't being produced or made
available; fix by making the TableGen-generated header available and/or
including the generated .inc explicitly: ensure your build (CMake) invokes the
tablegen step that produces QCToCatalystQuantum.h.inc and add the generated
include directory as an include path for the target that builds
lib/mqt-plugin.cpp (or update the target that defines the QCToCatalystQuantum
tablegen to export the generated header), and if the public header expects the
.inc, confirm or add the line to include
"mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.h.inc" from
QCToCatalystQuantum.h so the generated declarations are pulled in at compile
time.

In `@test/test_plugin.py`:
- Around line 552-565: The CHECK expectations are written for nested qc.ctrl
blocks but ConvertQuantumCustomOp emits a single qc.ctrl with a flattened
control list; update the check strings (e.g., the check_after_qc block and the
similar blocks at the other locations) to expect one qc.ctrl containing the full
control list rather than nested qc.ctrls, and remove the stray trailing comma in
the Toffoli pattern; locate occurrences by looking for the
check_after_qc/related CHECK string literals and adjust the qc.ctrl patterns to
a flattened form (single qc.ctrl with multiple %[[Q?_#:.*]] captures) for
RX/RY/RZ and Toffoli expectations.

---

Outside diff comments:
In `@test/test_plugin.py`:
- Around line 65-78: The cleanup function _cleanup_mlir_artifacts currently uses
Path.cwd() and unlinks all top-level "*.mlir" files which can delete checked-in
fixtures; change it to only remove MLIR files that are part of generated
module_N directories (e.g., iterate module_dir.glob("*.mlir") and unlink those,
and remove module_dir via shutil.rmtree only when module_dir.is_dir()), and stop
unlinking mlir files directly from Path.cwd() so files at the repo root are not
touched.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 645359af-01be-4279-bbde-dc43b03e5cf0

📥 Commits

Reviewing files that changed from the base of the PR and between 4b76fa6 and 0b1a52a.

📒 Files selected for processing (20)
  • cmake/ExternalDependencies.cmake
  • include/mlir/Conversion/CMakeLists.txt
  • include/mlir/Conversion/CatalystQuantumToMQTOpt/CMakeLists.txt
  • include/mlir/Conversion/CatalystQuantumToQC/CMakeLists.txt
  • include/mlir/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.h
  • include/mlir/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.td
  • include/mlir/Conversion/MQTOptToCatalystQuantum/CMakeLists.txt
  • include/mlir/Conversion/QCToCatalystQuantum/CMakeLists.txt
  • include/mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.h
  • include/mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.td
  • lib/CMakeLists.txt
  • lib/Conversion/CMakeLists.txt
  • lib/Conversion/CatalystQuantumToMQTOpt/CatalystQuantumToMQTOpt.cpp
  • lib/Conversion/CatalystQuantumToQC/CMakeLists.txt
  • lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp
  • lib/Conversion/MQTOptToCatalystQuantum/MQTOptToCatalystQuantum.cpp
  • lib/Conversion/QCToCatalystQuantum/CMakeLists.txt
  • lib/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.cpp
  • lib/mqt-plugin.cpp
  • test/test_plugin.py
💤 Files with no reviewable changes (4)
  • include/mlir/Conversion/MQTOptToCatalystQuantum/CMakeLists.txt
  • include/mlir/Conversion/CatalystQuantumToMQTOpt/CMakeLists.txt
  • lib/Conversion/CatalystQuantumToMQTOpt/CatalystQuantumToMQTOpt.cpp
  • lib/Conversion/MQTOptToCatalystQuantum/MQTOptToCatalystQuantum.cpp

Comment on lines +18 to 21
set(MQT_CORE_REV "test_Memref_explicit" # fix clang-tidy warnings 7d1d7fb
CACHE STRING "MQT Core identifier (tag, branch or commit hash)")
set(MQT_CORE_REPO_OWNER "munich-quantum-toolkit"
set(MQT_CORE_REPO_OWNER "NiklasKostrzewa"
CACHE STRING "MQT Core repository owner (change when using a fork)")
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.

⚠️ Potential issue | 🟠 Major

Personal fork and branch reference must be reverted before merging.

The external dependency now points to a personal fork (NiklasKostrzewa/core) with a branch name (test_Memref_explicit) instead of the official munich-quantum-toolkit/core repository with a pinned commit hash.

Issues:

  1. Non-reproducible builds: Using a branch name instead of a commit hash means the build target can change over time.
  2. Personal fork dependency: Production code should not depend on personal forks.
  3. Inconsistent comment: The inline comment references commit 7d1d7fb but the REV is set to a branch name.

This is acceptable for a draft PR during development, but ensure these values are reverted to the official repository with a stable commit hash before merging.

Suggested values for merge readiness
-set(MQT_CORE_REV "test_Memref_explicit" # fix clang-tidy warnings 7d1d7fb
+set(MQT_CORE_REV "<pinned-commit-hash-from-main>"
     CACHE STRING "MQT Core identifier (tag, branch or commit hash)")
-set(MQT_CORE_REPO_OWNER "NiklasKostrzewa"
+set(MQT_CORE_REPO_OWNER "munich-quantum-toolkit"
     CACHE STRING "MQT Core repository owner (change when using a fork)")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmake/ExternalDependencies.cmake` around lines 18 - 21, Replace the temporary
fork/branch references in the external dependency variables: set
MQT_CORE_REPO_OWNER back to the official owner ("munich-quantum-toolkit") and
change MQT_CORE_REV from the branch name ("test_Memref_explicit") to a stable
pinned commit hash (replace the branch with the specific commit hash you intend
to use), and update/remove the inconsistent inline comment referencing 7d1d7fb
so the REV and comment match; these changes are located where MQT_CORE_REV and
MQT_CORE_REPO_OWNER are defined.

Comment on lines +11 to +22
def QCToCatalystQuantum : Pass<"qc-to-catalystquantum"> {
let summary = "Convert MQT's `QC` to Catalyst's `Quantum` dialect.";

let description = [{
This pass converts MQT's `MQTOpt` to Catalyst's `Quantum` dialect.
This pass converts MQT's `QC` to Catalyst's `Quantum` dialect.
}];
let dependentDialects = [
"::catalyst::quantum::QuantumDialect",
"::mlir::arith::ArithDialect",
"::mlir::func::FuncDialect",
"::mlir::memref::MemRefDialect",
"::mqt::ir::opt::MQTOptDialect"
"::mlir::qc::QCDialect"
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.

⚠️ Potential issue | 🟡 Minor

Pass defined but registration is commented out.

The qc-to-catalystquantum pass is correctly defined here, but registerQCToCatalystQuantumPasses() is commented out in lib/mqt-plugin.cpp (line 47). This means:

  1. The pass will not be available at runtime.
  2. Tests using @apply_pass("mqt.qc-to-catalystquantum") or the DEFAULT_PIPELINE (which includes "qc-to-catalystquantum") will fail.

Per the PR description, this is intentional for the draft state since back-conversion is incomplete. Ensure the registration is uncommented when the implementation is ready.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.td` around
lines 11 - 22, The QCToCatalystQuantum pass (QCToCatalystQuantum) is defined but
not registered because registerQCToCatalystQuantumPasses() is commented out in
lib/mqt-plugin.cpp, so runtime lookups (e.g.
`@apply_pass`("mqt.qc-to-catalystquantum") and DEFAULT_PIPELINE referencing
"qc-to-catalystquantum") fail; to fix, uncomment and enable the call to
registerQCToCatalystQuantumPasses() in lib/mqt-plugin.cpp (or add conditional
registration gated by a readiness flag) so that the pass is registered with MLIR
when the implementation/back-conversion is complete and tests can find
mqt.qc-to-catalystquantum.

Comment on lines +25 to +26
add_llvm_library(${TARGET_NAME} MODULE mqt-plugin.cpp LINK_LIBS CatalystQuantumToQC)
# QCToCatalystQuantum)
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.

⚠️ Potential issue | 🔴 Critical

Link QCToCatalystQuantum into the plugin module.

The plugin now expects both QC passes, but this target only links CatalystQuantumToQC. QCToCatalystQuantum is a separate library, so the back-conversion pass will not be exported from the plugin unless it is linked here.

🔧 Suggested linkage fix
-add_llvm_library(${TARGET_NAME} MODULE mqt-plugin.cpp LINK_LIBS CatalystQuantumToQC)
-# QCToCatalystQuantum)
+add_llvm_library(
+  ${TARGET_NAME}
+  MODULE
+  mqt-plugin.cpp
+  LINK_LIBS
+  CatalystQuantumToQC
+  QCToCatalystQuantum)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/CMakeLists.txt` around lines 25 - 26, The plugin module currently links
only CatalystQuantumToQC via the add_llvm_library(${TARGET_NAME} MODULE
mqt-plugin.cpp LINK_LIBS CatalystQuantumToQC) invocation; update that
add_llvm_library call to also link QCToCatalystQuantum so the back-conversion
pass is exported by the plugin. Locate the add_llvm_library line (with
TARGET_NAME and mqt-plugin.cpp) and add QCToCatalystQuantum to the LINK_LIBS
list alongside CatalystQuantumToQC.

// "\n";

// Create the new operation
Operation* qcOp = nullptr;
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.

⚠️ Potential issue | 🟡 Minor

Remove the dead qcOp temporary.

The rewrite only relies on the side effects of rewriter.create; the stored pointer is never read, and CI is already warning about -Wunused-but-set-variable here.

🧰 Tools
🪛 GitHub Actions: CI

[warning] 319-319: qcOp: variable defined but not used. (Wunused-but-set-variable)

🪛 GitHub Check: 🇨‌ Lint / 🚨 Lint

[warning] 319-319: lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp:319:16 [clang-diagnostic-unused-but-set-variable]
variable 'qcOp' set but not used


[warning] 319-319: lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp:319:5 [misc-const-correctness]
pointee of variable 'qcOp' of type 'Operation *' can be declared 'const'

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp` at line 319, The
local variable qcOp is unused and should be removed: delete the declaration
"Operation* qcOp = nullptr;" and replace any assignment like "qcOp =
rewriter.create(...);" with just "rewriter.create(...);", relying on the side
effects of rewriter.create; ensure no other code references qcOp (search for
qcOp) and compile to verify the -Wunused-but-set-variable warning is resolved.

Comment on lines +446 to +515
} else if (gateName == "RX") {
if (inCtrlQubits.empty()) {
qcOp = CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP(RX);
} else {
qcOp = rewriter.create<qc::CtrlOp>(op.getLoc(), inCtrlQubits, [&]() {
CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP(RX);
});
}
} else if (gateName == "RY") {
if (inCtrlQubits.empty()) {
qcOp = CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP(RY);
} else {
qcOp = rewriter.create<qc::CtrlOp>(op.getLoc(), inCtrlQubits, [&]() {
CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP(RY);
});
}
} else if (gateName == "RZ") {
if (inCtrlQubits.empty()) {
qcOp = CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP(RZ);
} else {
qcOp = rewriter.create<qc::CtrlOp>(op.getLoc(), inCtrlQubits, [&]() {
CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP(RZ);
});
}
} else if (gateName == "PhaseShift") {
if (inCtrlQubits.empty()) {
qcOp = CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP(P);
} else {
qcOp = rewriter.create<qc::CtrlOp>(op.getLoc(), inCtrlQubits, [&]() {
CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP(P);
});
}
} else if (gateName == "CRX") {
qcOp = rewriter.create<qc::CtrlOp>(
op.getLoc(), inQubits.take_front(1), [&]() {
rewriter.create<qc::RXOp>(op.getLoc(), inQubits[1],
paramsValues[0]);
});
} else if (gateName == "CRY") {
qcOp = rewriter.create<qc::CtrlOp>(
op.getLoc(), inQubits.take_front(1), [&]() {
rewriter.create<qc::RYOp>(op.getLoc(), inQubits[1],
paramsValues[0]);
});
} else if (gateName == "CRZ") {
qcOp = rewriter.create<qc::CtrlOp>(
op.getLoc(), inQubits.take_front(1), [&]() {
rewriter.create<qc::RZOp>(op.getLoc(), inQubits[1],
paramsValues[0]);
});
} else if (gateName == "ControlledPhaseShift") {
qcOp = rewriter.create<qc::CtrlOp>(
op.getLoc(), inQubits.take_front(1), [&]() {
rewriter.create<qc::POp>(op.getLoc(), inQubits[1], paramsValues[0]);
});
} else if (gateName == "IsingXY") {
// PennyLane IsingXY has 1 parameter (phi), OpenQASM XXPlusYY needs 2
// (theta, beta) Relationship: IsingXY(phi) = XXPlusYY(phi, pi)
// Add pi as second parameter (since we add it during compilation)

qcOp =
rewriter.create<qc::XXPlusYYOp>(op.getLoc(), inQubits[0], inQubits[1],
paramsValues[0], std::numbers::pi);
} else if (gateName == "IsingXX") {
qcOp = CREATE_TWO_TARGET_ONE_PARAMETER_GATE_OP(RXX);
} else if (gateName == "IsingYY") {
qcOp = CREATE_TWO_TARGET_ONE_PARAMETER_GATE_OP(RYY);
} else if (gateName == "IsingZZ") {
qcOp = CREATE_TWO_TARGET_ONE_PARAMETER_GATE_OP(RZZ);
} else if (gateName == "CNOT") {
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.

⚠️ Potential issue | 🔴 Critical

adj is ignored for non-self-inverse parameterized gates.

Only the S/T/SX branches consult op.getAdjoint(). RX/RY/RZ, PhaseShift, CRX/CRY/CRZ, ControlledPhaseShift, and the Ising* branches all lower an adjointed quantum.custom to the forward QC op, which changes circuit semantics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp` around lines 446
- 515, The adjoint flag (op.getAdjoint()) is ignored for parameterized gates
(RX/RY/RZ, PhaseShift/P, CRX/CRY/CRZ, ControlledPhaseShift/POp,
IsingXY/IsingXX/IsingYY/IsingZZ and their lowering macros CREATE_* like
CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP and
CREATE_TWO_TARGET_ONE_PARAMETER_GATE_OP), so update the lowering to check
op.getAdjoint() and emit the adjointed operation: for single-parameter rotations
and phase gates negate the parameter (use -paramsValues[0]) when adjoint is
true; for controlled variants negate the same parameter passed into the inner
rewriter.create<qc::RXOp/RYOp/RZOp/POp>; for IsingXY negate the first parameter
(keep the added pi as-is) when adjoint is true; and for
CREATE_TWO_TARGET_ONE_PARAMETER_GATE_OP usages (RXX/RYY/RZZ) negate the
parameter when adjoint is set so the lowered qcOp preserves semantics. Ensure
you reference and modify the branches that inspect gateName (e.g., the RX/RY/RZ,
PhaseShift, CRX/CRY/CRZ, ControlledPhaseShift, Ising* branches) and use
op.getAdjoint() to decide whether to pass paramsValues[0] or -paramsValues[0].

Comment on lines +39 to +329
#define GEN_PASS_DEF_QCTOCATALYSTQUANTUM
#include "mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.h.inc"

using namespace mlir;
using namespace mlir::arith;

// Helper functions to reduce code duplication
namespace {

/// Helper struct to hold control qubit information
struct ControlInfo {
SmallVector<Value> ctrlQubits;
SmallVector<Value> ctrlValues;

ControlInfo() noexcept = default;
};

/// Extract and concatenate control qubits and create corresponding control
/// values
ControlInfo extractControlInfo(ValueRange posCtrlQubits,
ValueRange negCtrlQubits,
ConversionPatternRewriter& rewriter,
Location loc) {
ControlInfo info;

// Concatenate controls: [pos..., neg...] (preserve this order consistently)
info.ctrlQubits.reserve(posCtrlQubits.size() + negCtrlQubits.size());
info.ctrlQubits.append(posCtrlQubits.begin(), posCtrlQubits.end());
info.ctrlQubits.append(negCtrlQubits.begin(), negCtrlQubits.end());

if (info.ctrlQubits.empty()) {
return info;
}

// Create control values: 1 for positive controls, 0 for negative controls
const Value one =
rewriter.create<mlir::arith::ConstantIntOp>(loc, /*value=*/1,
/*width=*/1);
const Value zero =
rewriter.create<mlir::arith::ConstantIntOp>(loc, /*value=*/0,
/*width=*/1);

info.ctrlValues.reserve(info.ctrlQubits.size());
info.ctrlValues.append(posCtrlQubits.size(), one); // +controls => 1
info.ctrlValues.append(negCtrlQubits.size(), zero); // -controls => 0

return info;
}

/// Helper function to extract operands and control info - for more complex
/// cases
template <typename OpAdaptor> struct ExtractedOperands {
ValueRange inQubits;
ControlInfo ctrlInfo;
};

template <typename OpAdaptor>
ExtractedOperands<OpAdaptor>
extractOperands(OpAdaptor adaptor, ConversionPatternRewriter& rewriter,
Location loc) {
const ValueRange inQubits = adaptor.getInQubits();
const ValueRange posCtrlQubits = adaptor.getPosCtrlInQubits();
const ValueRange negCtrlQubits = adaptor.getNegCtrlInQubits();

const ControlInfo ctrlInfo =
extractControlInfo(posCtrlQubits, negCtrlQubits, rewriter, loc);

return {inQubits, ctrlInfo};
}

} // anonymous namespace

class QCToCatalystQuantumTypeConverter final : public TypeConverter {
public:
explicit QCToCatalystQuantumTypeConverter(MLIRContext* ctx) {
// Identity conversion for types that don't need transformation
addConversion([](const Type type) { return type; });

// Convert MemRef of QC QubitType to Catalyst QuregType
// Also handles memrefs where the element type was already converted
addConversion([ctx](MemRefType memrefType) -> Type {
auto elemType = memrefType.getElementType();
if (isa<qc::QubitType>(elemType) ||
isa<catalyst::quantum::QubitType>(elemType)) {
return catalyst::quantum::QuregType::get(ctx);
}
return memrefType;
});

// Convert QC QubitType to Catalyst QubitType
addConversion([ctx](qc::QubitType /*type*/) -> Type {
return catalyst::quantum::QubitType::get(ctx);
});
}
};

struct ConvertQCAlloc final : OpConversionPattern<memref::AllocOp> {
using OpConversionPattern::OpConversionPattern;

LogicalResult
matchAndRewrite(memref::AllocOp op, OpAdaptor /*adaptor*/,
ConversionPatternRewriter& rewriter) const override {
// Only convert memrefs of qubit type
auto memrefType = dyn_cast<BaseMemRefType>(op.getType());
auto elemType = memrefType ? memrefType.getElementType() : Type();
if (!memrefType || !(isa<qc::QubitType>(elemType) ||
isa<catalyst::quantum::QubitType>(elemType))) {
return failure();
}

// Only handle ranked memrefs
auto rankedMemrefType = dyn_cast<MemRefType>(memrefType);
if (!rankedMemrefType) {
return failure();
}

// Prepare the result type(s)
const auto resultType =
catalyst::quantum::QuregType::get(rewriter.getContext());

// Get the size from memref type or dynamic operands
Value size = nullptr;
mlir::IntegerAttr nqubitsAttr = nullptr;

// Check if this is a statically shaped memref
if (rankedMemrefType.hasStaticShape() &&
rankedMemrefType.getNumElements() >= 0) {
// For static memref: use attribute (no operand)
nqubitsAttr =
rewriter.getI64IntegerAttr(rankedMemrefType.getNumElements());
} else {
// For dynamic memref: check if the size is actually a constant
auto dynamicOperands = op.getDynamicSizes();
const Value dynamicSize =
dynamicOperands.empty() ? nullptr : dynamicOperands[0];

if (dynamicSize) {
// Try to recover static size from constant operand
if (auto constOp =
dynamicSize.getDefiningOp<arith::ConstantIndexOp>()) {
// The size is a constant index, use it as an attribute instead
nqubitsAttr = rewriter.getI64IntegerAttr(constOp.value());
} else if (auto constOp =
dynamicSize.getDefiningOp<arith::ConstantIntOp>()) {
// The size is a constant int, use it as an attribute instead
nqubitsAttr = rewriter.getI64IntegerAttr(constOp.value());
} else {
// Truly dynamic size - use operand
size = dynamicSize;
// quantum.alloc expects i64, but memref size is index type
if (mlir::isa<IndexType>(size.getType())) {
size = rewriter.create<arith::IndexCastOp>(
op.getLoc(), rewriter.getI64Type(), size);
}
}
}
}

// Replace with quantum alloc operation
rewriter.replaceOpWithNewOp<catalyst::quantum::AllocOp>(op, resultType,
size, nqubitsAttr);

return success();
}
};

struct ConvertQCDealloc final : OpConversionPattern<memref::DeallocOp> {
using OpConversionPattern::OpConversionPattern;

LogicalResult
matchAndRewrite(memref::DeallocOp op, OpAdaptor adaptor,
ConversionPatternRewriter& rewriter) const override {
// Only convert memrefs of qubit type
auto memrefType = dyn_cast<BaseMemRefType>(op.getMemref().getType());
auto elemType = memrefType ? memrefType.getElementType() : Type();
if (!memrefType || !(isa<qc::QubitType>(elemType) ||
isa<catalyst::quantum::QubitType>(elemType))) {
return failure();
}

// Create the new operation
const auto catalystOp = rewriter.create<catalyst::quantum::DeallocOp>(
op.getLoc(), TypeRange({}), adaptor.getMemref());

// Replace the original with the new operation
rewriter.replaceOp(op, catalystOp);
return success();
}
};

struct ConvertQCMeasure final : OpConversionPattern<qc::MeasureOp> {
using OpConversionPattern::OpConversionPattern;

LogicalResult
matchAndRewrite(qc::MeasureOp op, OpAdaptor adaptor,
ConversionPatternRewriter& rewriter) const override {

// Extract operand(s)
auto inQubit = adaptor.getQubit();

// Prepare the result type(s)
auto qubitType = catalyst::quantum::QubitType::get(rewriter.getContext());
auto bitType = rewriter.getI1Type();

// Create the new operation
const auto catalystOp = rewriter.create<catalyst::quantum::MeasureOp>(
op.getLoc(), bitType, qubitType, inQubit,
/*optional::mlir::IntegerAttr postselect=*/nullptr);

// Replace all uses of both results and then erase the operation
const auto catalystMeasure = catalystOp->getResult(0);
const auto catalystQubit = catalystOp->getResult(1);
rewriter.replaceOp(op, ValueRange{catalystQubit, catalystMeasure});
return success();
}
};

struct ConvertQCLoad final : OpConversionPattern<memref::LoadOp> {
using OpConversionPattern::OpConversionPattern;

LogicalResult
matchAndRewrite(memref::LoadOp op, OpAdaptor adaptor,
ConversionPatternRewriter& rewriter) const override {
// Only convert loads of qubit type
if (!(isa<qc::QubitType>(op.getType()) ||
isa<catalyst::quantum::QubitType>(op.getType()))) {
return failure();
}

// Prepare the result type(s)
auto resultType = catalyst::quantum::QubitType::get(rewriter.getContext());

// Get index (assuming single index for 1D memref)
auto indices = adaptor.getIndices();
Value index = indices.empty() ? nullptr : indices[0];

// Convert index type to i64 if needed
if (index && mlir::isa<IndexType>(index.getType())) {
index = rewriter.create<arith::IndexCastOp>(op.getLoc(),
rewriter.getI64Type(), index);
}

// Create the new operation
auto catalystOp = rewriter.create<catalyst::quantum::ExtractOp>(
op.getLoc(), resultType, adaptor.getMemref(), index, nullptr);

// Replace the load operation with the extracted qubit
rewriter.replaceOp(op, catalystOp.getResult());
return success();
}
};

struct ConvertQCStore final : OpConversionPattern<memref::StoreOp> {
using OpConversionPattern::OpConversionPattern;

LogicalResult
matchAndRewrite(memref::StoreOp op, OpAdaptor adaptor,
ConversionPatternRewriter& rewriter) const override {
// Only convert stores to memrefs with qubit element type
auto memrefType = dyn_cast<BaseMemRefType>(op.getMemRef().getType());
auto elemType = memrefType ? memrefType.getElementType() : Type();
if (!memrefType || !(isa<qc::QubitType>(elemType) ||
isa<catalyst::quantum::QubitType>(elemType))) {
return failure();
}

// Get indices (assuming single index for 1D memref)
auto indices = adaptor.getIndices();
Value index = indices.empty() ? nullptr : indices[0];

// Convert index type to i64 if needed
if (index && mlir::isa<IndexType>(index.getType())) {
index = rewriter.create<arith::IndexCastOp>(op.getLoc(),
rewriter.getI64Type(), index);
}

// Prepare the result type(s)
auto resultType = catalyst::quantum::QuregType::get(rewriter.getContext());

// Create the new operation
rewriter.create<catalyst::quantum::InsertOp>(op.getLoc(), resultType,
adaptor.getMemref(), index,
nullptr, adaptor.getValue());

// Erase the original store operation (store has no results to replace)
rewriter.eraseOp(op);
return success();
}
};

} // namespace mqt::ir::conversions
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.

⚠️ Potential issue | 🔴 Critical

qc-to-catalystquantum is declared, but no pass is implemented.

This file stops after helper/type-converter/pattern declarations. There is no QCToCatalystQuantum::runOnOperation(), no legality setup, and no registration of the rewrite patterns below, so the pass advertised by the CMake/test changes cannot execute. The actual QC gate/control ops exercised in test/test_plugin.py are also not converted anywhere in this file yet.

🧰 Tools
🪛 Cppcheck (2.20.0)

[information] 40-40: Include file

(missingInclude)


[style] 97-97: The function 'extractOperands' is never used.

(unusedFunction)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.cpp` around lines 39 -
329, The file defines conversions (QCToCatalystQuantumTypeConverter,
ConvertQCAlloc, ConvertQCDealloc, ConvertQCMeasure, ConvertQCLoad,
ConvertQCStore, and helpers like extractControlInfo/extractOperands) but never
implements or registers the pass; add a pass implementation using
GEN_PASS_DEF_QCTOCATALYSTQUANTUM that defines
QCToCatalystQuantum::runOnOperation(), builds a ConversionTarget, instantiates
QCToCatalystQuantumTypeConverter, populates a RewritePatternSet with the
provided OpConversionPattern classes (ConvertQCAlloc, ConvertQCDealloc,
ConvertQCMeasure, ConvertQCLoad, ConvertQCStore, etc.), marks legal/illegal ops
appropriately, and call applyFullConversion/applyPartialConversion; finally
register the pass so the declared pass actually runs in tests.

Comment on lines +46 to +47
mqt::ir::conversions::registerCatalystQuantumToQCPasses();
// mqt::ir::conversions::registerQCToCatalystQuantumPasses();
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.

🧹 Nitpick | 🔵 Trivial

Back-conversion pass registration intentionally disabled.

The registerQCToCatalystQuantumPasses() call is commented out, which is consistent with the PR description stating that back-conversion is incomplete. The test suite's DEFAULT_PIPELINE includes "qc-to-catalystquantum", so tests exercising the full roundtrip will fail until this is uncommented.

Consider adding a TODO or FIXME comment to make this more discoverable:

             mqt::ir::conversions::registerCatalystQuantumToQCPasses();
-            // mqt::ir::conversions::registerQCToCatalystQuantumPasses();
+            // TODO(NiklasKostrzewa): Uncomment when back-conversion is complete
+            // mqt::ir::conversions::registerQCToCatalystQuantumPasses();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mqt::ir::conversions::registerCatalystQuantumToQCPasses();
// mqt::ir::conversions::registerQCToCatalystQuantumPasses();
mqt::ir::conversions::registerCatalystQuantumToQCPasses();
// TODO(NiklasKostrzewa): Uncomment when back-conversion is complete
// mqt::ir::conversions::registerQCToCatalystQuantumPasses();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mqt-plugin.cpp` around lines 46 - 47, Back-conversion registration
(mqt::ir::conversions::registerQCToCatalystQuantumPasses) is commented out which
breaks tests that expect the full roundtrip (DEFAULT_PIPELINE includes
"qc-to-catalystquantum"); add a clear TODO/FIXME above the commented line that
names registerQCToCatalystQuantumPasses and references DEFAULT_PIPELINE, and
either (a) briefly describe what remains to be implemented for back-conversion
or (b) add a short conditional/feature-flag note explaining why it is disabled
so future readers/tests can find and re-enable it.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (10)
cmake/ExternalDependencies.cmake (1)

18-21: ⚠️ Potential issue | 🟠 Major

Revert default mqt-core source to official owner + pinned commit SHA before merge.

Line 18 and Line 20 still default to a personal fork and mutable branch. Since this file is executed at configure time, these defaults make builds non-reproducible and reduce supply-chain traceability. Keep fork/branch only as an opt-in local override, not as default values.

Suggested fix
-set(MQT_CORE_REV "test_Memref_explicit" # fix clang-tidy warnings 7d1d7fb
+set(MQT_CORE_REV "<pinned-40-char-commit-sha>"
     CACHE STRING "MQT Core identifier (tag, branch or commit hash)")
-set(MQT_CORE_REPO_OWNER "NiklasKostrzewa"
+set(MQT_CORE_REPO_OWNER "munich-quantum-toolkit"
     CACHE STRING "MQT Core repository owner (change when using a fork)")
#!/bin/bash
set -euo pipefail

FILE="$(fd -p ExternalDependencies.cmake | head -n1)"
echo "Checking ${FILE}"
nl -ba "${FILE}" | sed -n '14,24p'

python - <<'PY'
import re
from pathlib import Path

p = next(Path('.').rglob('ExternalDependencies.cmake'))
text = p.read_text()

owner_m = re.search(r'set\(MQT_CORE_REPO_OWNER\s+"([^"]+)"', text)
rev_m = re.search(r'set\(MQT_CORE_REV\s+"([^"]+)"', text)

owner = owner_m.group(1) if owner_m else None
rev = rev_m.group(1) if rev_m else None

print(f"MQT_CORE_REPO_OWNER={owner}")
print(f"MQT_CORE_REV={rev}")
print("owner_is_official:", owner == "munich-quantum-toolkit")
print("rev_is_pinned_sha:", bool(re.fullmatch(r"[0-9a-f]{40}", rev or "")))
PY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmake/ExternalDependencies.cmake` around lines 18 - 21, The defaults for
MQT_CORE_REPO_OWNER and MQT_CORE_REV must be reverted to the official upstream
and a pinned commit SHA: update the variable MQT_CORE_REPO_OWNER to
"munich-quantum-toolkit" and replace the mutable branch value in MQT_CORE_REV
with a 40-character commit hash (hex) for a reproducible pinned revision; leave
any fork/branch values only as optional overrides or documented examples in
comments, not as the CACHE default, and ensure MQT_CORE_REV matches the full SHA
pattern [0-9a-f]{40}.
lib/CMakeLists.txt (1)

25-26: ⚠️ Potential issue | 🟠 Major

Link QCToCatalystQuantum into the plugin target before re-enabling the pass.

QCToCatalystQuantum is a separate MLIR library, but the plugin currently links only CatalystQuantumToQC. Once registerQCToCatalystQuantumPasses() is turned back on, the reverse conversion pass still will not be available from the plugin unless this target is linked here.

🔧 Suggested linkage fix
-add_llvm_library(${TARGET_NAME} MODULE mqt-plugin.cpp LINK_LIBS CatalystQuantumToQC)
-# QCToCatalystQuantum)
+add_llvm_library(
+  ${TARGET_NAME}
+  MODULE
+  mqt-plugin.cpp
+  LINK_LIBS
+  CatalystQuantumToQC
+  QCToCatalystQuantum)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/CMakeLists.txt` around lines 25 - 26, The plugin target created by
add_llvm_library(${TARGET_NAME} MODULE mqt-plugin.cpp LINK_LIBS
CatalystQuantumToQC currently omits QCToCatalystQuantum, so when
registerQCToCatalystQuantumPasses() is re-enabled the reverse conversion won't
be available; fix this by adding QCToCatalystQuantum to the LINK_LIBS for the
target (i.e., ensure LINK_LIBS includes both CatalystQuantumToQC and
QCToCatalystQuantum for the ${TARGET_NAME} plugin built from mqt-plugin.cpp).
lib/mqt-plugin.cpp (2)

11-13: ⚠️ Potential issue | 🔴 Critical

Build failure due to missing generated header.

The CI reports: fatal error: mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.h.inc: No such file or directory. The QCToCatalystQuantum.h include depends on a TableGen-generated .h.inc file that isn't being produced correctly.

Verify that include/mlir/Conversion/QCToCatalystQuantum/CMakeLists.txt properly invokes mlir_tablegen and that the generated include directory is added to the target's include paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mqt-plugin.cpp` around lines 11 - 13, The build fails because the
TableGen-generated header QCToCatalystQuantum.h.inc isn't being
produced/visible; update
include/mlir/Conversion/QCToCatalystQuantum/CMakeLists.txt to run mlir_tablegen
to generate QCToCatalystQuantum.h.inc (invoking the correct TABLEGEN or
MLIR_TABLEGEN rule for the QCToCatalystQuantum tablegen file) and add the
generated include directory to the consuming target via
target_include_directories or target_include_directories(... SYSTEM BEFORE) so
that the target building lib/mqt-plugin.cpp can find
"mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.h" which expects the
.h.inc; ensure the generated file name matches QCToCatalystQuantum.h.inc and
that the target that builds mqt-plugin depends on the tablegen output target so
generation happens before compilation.

46-47: 🧹 Nitpick | 🔵 Trivial

Add a TODO comment for the disabled back-conversion pass.

The registerQCToCatalystQuantumPasses() call is commented out, which is consistent with the PR stating back-conversion is incomplete. Adding a TODO helps discoverability:

             mqt::ir::conversions::registerCatalystQuantumToQCPasses();
-            // mqt::ir::conversions::registerQCToCatalystQuantumPasses();
+            // TODO(NiklasKostrzewa): Uncomment when back-conversion is complete
+            // mqt::ir::conversions::registerQCToCatalystQuantumPasses();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mqt-plugin.cpp` around lines 46 - 47, Add a short TODO comment next to
the commented-out registerQCToCatalystQuantumPasses() call in lib/mqt-plugin.cpp
to indicate the back-conversion is incomplete and needs re-enabling or
completion later; reference the symbol registerQCToCatalystQuantumPasses() and
mention “back-conversion incomplete” and optionally link to the PR or issue
number if available so future maintainers can discover why it is disabled.
lib/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.cpp (2)

315-326: ⚠️ Potential issue | 🔴 Critical

InsertOp result is dropped — writes are lost in SSA semantics.

catalyst::quantum::InsertOp returns a new !quantum.reg value representing the updated register. Here, the result is discarded, so the write is semantically lost in Catalyst's SSA model. The result should be threaded to subsequent users or the value mapping should be updated.

🐛 Proposed fix
-    // Create the new operation
-    rewriter.create<catalyst::quantum::InsertOp>(op.getLoc(), resultType,
-                                                 adaptor.getMemref(), index,
-                                                 nullptr, adaptor.getValue());
-
-    // Erase the original store operation (store has no results to replace)
-    rewriter.eraseOp(op);
+    // Create the new operation and capture result
+    auto insertOp = rewriter.create<catalyst::quantum::InsertOp>(
+        op.getLoc(), resultType, adaptor.getMemref(), index, nullptr,
+        adaptor.getValue());
+
+    // Replace all uses of the original memref with the new register
+    rewriter.replaceAllUsesWith(adaptor.getMemref(), insertOp.getResult());
+    rewriter.eraseOp(op);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.cpp` around lines 315
- 326, InsertOp's result is being dropped so the updated !quantum.reg is lost;
change the creation to capture the returned value from
rewriter.create<catalyst::quantum::InsertOp> (e.g., auto newInsert =
rewriter.create<...>(...)) and then thread that result into the SSA flow by
replacing the original op's result or updating the conversion mapping (use
rewriter.replaceOp(op, newInsert.getResult(0)) or update your
ValueMapping/rewriter mapping so later uses see newInsert.getResult(0)); ensure
you remove the old op after replacing so no users remain referencing the
discarded value.

37-41: ⚠️ Potential issue | 🔴 Critical

Pass implementation is incomplete — no runOnOperation() defined.

This file declares GEN_PASS_DEF_QCTOCATALYSTQUANTUM but never implements the pass's runOnOperation() method. The patterns (ConvertQCAlloc, ConvertQCDealloc, etc.) are defined but never registered with a ConversionTarget or applied via applyPartialConversion. The pass cannot execute in its current state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.cpp` around lines 37 -
41, Implement the pass's runOnOperation for the pass declared by
GEN_PASS_DEF_QCTOCATALYSTQUANTUM: create a new instance of ConversionTarget,
register legality for target dialects, populate a RewritePatternSet with the
existing patterns (ConvertQCAlloc, ConvertQCDealloc, ConvertQCExtract,
ConvertQCInject, etc.), and then call applyPartialConversion(getOperation(),
target, patterns). Ensure the pass class (the one generated by
GEN_PASS_DEF_QCTOCATALYSTQUANTUM) overrides runOnOperation(), constructs the
target and patterns, marks ops as legal/illegal as appropriate, and handles
failure by signalling signalPassFailure() when applyPartialConversion fails.
lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp (2)

446-515: ⚠️ Potential issue | 🔴 Critical

Adjoint flag is ignored for parameterized rotation gates.

Only S, T, and SX gates check op.getAdjoint(). For RX, RY, RZ, PhaseShift, CRX, CRY, CRZ, ControlledPhaseShift, and Ising* gates, the adjoint flag is not consulted. For parameterized rotations, adjoint typically requires negating the rotation angle.

For example, RX†(θ) = RX(-θ). Without this, adjointed gates convert incorrectly.

🐛 Example fix for RX
   } else if (gateName == "RX") {
+    Value angle = paramsValues[0];
+    if (op.getAdjoint()) {
+      angle = rewriter.create<arith::NegFOp>(op.getLoc(), angle);
+    }
     if (inCtrlQubits.empty()) {
-      qcOp = CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP(RX);
+      rewriter.create<qc::RXOp>(op.getLoc(), inQubits[0], angle);
     } else {
       qcOp = rewriter.create<qc::CtrlOp>(op.getLoc(), inCtrlQubits, [&]() {
-        CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP(RX);
+        rewriter.create<qc::RXOp>(op.getLoc(), inQubits[0], angle);
       });
     }
   }

Apply similar changes to RY, RZ, PhaseShift, CRX, CRY, CRZ, ControlledPhaseShift, and the Ising* gates.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp` around lines 446
- 515, Adjoint handling is missing for parameterized rotation and Ising gates:
check op.getAdjoint() for RX/RY/RZ/PhaseShift (P) and their controlled variants
(CRX/CRY/CRZ/ControlledPhaseShift) and for IsingXY/IsingXX/IsingYY/IsingZZ, and
if adjoint is true, negate the rotation parameter(s) before creating the qc ops;
e.g., use a local angle = op.getAdjoint() ? -paramsValues[0] : paramsValues[0]
(and for IsingXY preserve the added pi for the second argument while negating
the phi), then pass that angle into
CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP(RX)/rewriter.create<qc::RXOp>/CREATE_TWO_TARGET_ONE_PARAMETER_GATE_OP(RXX)/rewriter.create<qc::XXPlusYYOp>
etc., and for controlled forms negate the parameter supplied inside the lambda
similarly.

319-319: 🧹 Nitpick | 🔵 Trivial

Remove unused qcOp variable.

The variable qcOp is assigned but never read. CI warns: "variable 'qcOp' set but not used". Remove it and use the rewriter.create<>() calls directly for their side effects.

🧹 Proposed fix
-    // Create the new operation
-    Operation* qcOp = nullptr;
+    // Create the new operations (side-effect only, results not needed)

Then change all qcOp = rewriter.create<...> to just rewriter.create<...> (or use (void) cast if intentional).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp` at line 319,
Remove the unused variable qcOp (declared as Operation* qcOp = nullptr;) and
update all places that assign to it (e.g., qcOp = rewriter.create<...>(...)) to
call rewriter.create<...>(...) directly for their side effects (or use a (void)
cast if you deliberately want to ignore the return). Ensure no other code reads
qcOp and delete its declaration so the "set but not used" warning is resolved.
test/test_plugin.py (2)

560-565: ⚠️ Potential issue | 🟠 Major

Nested qc.ctrl CHECK patterns won't match flattened converter output.

ConvertQuantumCustomOp emits a single qc.ctrl with the full control list, not nested qc.ctrl { qc.ctrl { ... } }. The doubly-controlled RX check at lines 560-565 expects nested controls but the converter produces flattened output.

🧪 Proposed fix
-        //CHECK: qc.ctrl(%[[Q0_2:.*]]) {
-            qc.ctrl(%[[Q0_1:.*]]) {
-                qc.rx({{.*}}) %[[Q0_0:.*]] : !qc.qubit
-            } : !qc.qubit
-        } : !qc.qubit
+        //CHECK: qc.ctrl(%[[Q0_2:.*]], %[[Q0_1:.*]]) {
+            qc.rx({{.*}}) %[[Q0_0:.*]] : !qc.qubit
+        } : !qc.qubit
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test_plugin.py` around lines 560 - 565, The test expects nested qc.ctrl
blocks but ConvertQuantumCustomOp emits a single flattened qc.ctrl with the full
control list; update the CHECK in test_plugin.py to match a single qc.ctrl
containing qc.rx (i.e., replace the nested "qc.ctrl { qc.ctrl { qc.rx ... } }"
pattern with a flattened "qc.ctrl(...) { qc.rx(...) ... }" pattern that accepts
the full control list and the qc.rx operation, referencing
ConvertQuantumCustomOp, qc.ctrl and qc.rx in the adjusted CHECK.

788-797: ⚠️ Potential issue | 🟡 Minor

Toffoli CHECK pattern has a stray trailing comma and wrong nesting.

Line 793 has qc.ctrl(%[[Q0_0:.*]], %[[Q0_1:.*]],) with a trailing comma before the closing parenthesis, which will cause FileCheck to fail. Additionally, the pattern expects nested qc.ctrl but the converter emits a single flattened qc.ctrl.

🧪 Proposed fix
     check_after_qc = """
-        //CHECK: qc.ctrl(%[[Q0_0:.*]], %[[Q0_1:.*]]) {
-            qc.x %[[Q0_2:.*]] : !qc.qubit
-        } : !qc.qubit
-        //CHECK: qc.ctrl(%[[Q0_3:.*]]) {
-            qc.ctrl(%[[Q0_0:.*]], %[[Q0_1:.*]],) {
-                qc.x %[[Q0_2:.*]] : !qc.qubit
-            } : !qc.qubit
-        } : !qc.qubit
+        //CHECK: qc.ctrl(%[[Q0_0:.*]], %[[Q0_1:.*]]) {
+            qc.x %[[Q0_2:.*]] : !qc.qubit
+        } : !qc.qubit
+        //CHECK: qc.ctrl(%[[Q0_3:.*]], %[[Q0_0:.*]], %[[Q0_1:.*]]) {
+            qc.x %[[Q0_2:.*]] : !qc.qubit
+        } : !qc.qubit
     """
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test_plugin.py` around lines 788 - 797, The CHECK pattern in the
check_after_qc string is wrong: remove the stray trailing comma in the second
qc.ctrl and change the nested qc.ctrl expectation to match the converter's
flattened control construct; replace the nested pattern "qc.ctrl(%[[Q0_3:.*]]) {
qc.ctrl(%[[Q0_0:.*]], %[[Q0_1:.*]],) { qc.x %[[Q0_2:.*]] : !qc.qubit } :
!qc.qubit } : !qc.qubit" with a single flattened control like
"qc.ctrl(%[[Q0_0:.*]], %[[Q0_1:.*]], %[[Q0_3:.*]]) { qc.x %[[Q0_2:.*]] :
!qc.qubit } : !qc.qubit" so FileCheck matches the emitted Toffoli
representation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@include/mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.h`:
- Around line 17-21: The generated header QCToCatalystQuantum.h.inc is missing
from the build, so update the TableGen/include-side wiring for the
QCToCatalystQuantum pass: add the TableGen rule to emit
QCToCatalystQuantum.h.inc (or restore its name) and ensure the build target that
produces it is run before compiling code that includes it; specifically ensure
the pass name QCToCatalystQuantum is registered in your TableGen input and that
the GEN_PASS_DECL / GEN_PASS_REGISTRATION expansion for QCToCatalystQuantum is
being generated by the TableGen backend so the include
"QCToCatalystQuantum.h.inc" is produced for downstream translation units (update
build files or CMake target dependencies as needed).

In `@lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp`:
- Line 164: Rename the local variable QCOp to lowerCamelCase qcOp where it's
created with rewriter.create<qc::MeasureOp>(op.getLoc(), inQubit) and update all
subsequent uses (e.g., any reads, .getResult(), or replacements that reference
QCOp) to qcOp to satisfy the project's naming conventions and silence the CI
lint warning.
- Line 18: Remove the unused include directive: delete the line `#include
<cstddef>` from CatalystQuantumToQC.cpp so the file no longer contains an unused
header; verify compilation passes and no symbols from `<cstddef>` (e.g.,
`size_t`, `nullptr_t`) are relied on in functions or classes within this
translation unit before committing.

In `@lib/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.cpp`:
- Around line 95-107: The template helper function extractOperands
(template<typename OpAdaptor>) is unused; remove its definition and any related
forward declarations to clean up dead code—specifically delete the
extractOperands function block that returns ExtractedOperands and references
adaptor.getInQubits(), getPosCtrlInQubits(), getNegCtrlInQubits(), and
extractControlInfo; after removal, run a build to ensure no remaining references
to ExtractedOperands or extractOperands remain and remove any now-unneeded
includes or using declarations introduced solely for this helper.

In `@test/test_plugin.py`:
- Around line 43-44: DEFAULT_PIPELINE currently includes the back-conversion
step ("ToCatalystQuantum", ["builtin.module(qc-to-catalystquantum)"]) but
registerQCToCatalystQuantumPasses() in lib/mqt-plugin.cpp is commented out,
causing roundtrip tests to fail; fix by either removing/commenting the
("ToCatalystQuantum", ["builtin.module(qc-to-catalystquantum)"]) entry from
DEFAULT_PIPELINE in test/test_plugin.py or, if you prefer to keep the pipeline
unchanged, mark the failing roundtrip tests with pytest.mark.skip until
registerQCToCatalystQuantumPasses() is implemented—search for DEFAULT_PIPELINE
and registerQCToCatalystQuantumPasses to locate the relevant spots to change.

---

Duplicate comments:
In `@cmake/ExternalDependencies.cmake`:
- Around line 18-21: The defaults for MQT_CORE_REPO_OWNER and MQT_CORE_REV must
be reverted to the official upstream and a pinned commit SHA: update the
variable MQT_CORE_REPO_OWNER to "munich-quantum-toolkit" and replace the mutable
branch value in MQT_CORE_REV with a 40-character commit hash (hex) for a
reproducible pinned revision; leave any fork/branch values only as optional
overrides or documented examples in comments, not as the CACHE default, and
ensure MQT_CORE_REV matches the full SHA pattern [0-9a-f]{40}.

In `@lib/CMakeLists.txt`:
- Around line 25-26: The plugin target created by
add_llvm_library(${TARGET_NAME} MODULE mqt-plugin.cpp LINK_LIBS
CatalystQuantumToQC currently omits QCToCatalystQuantum, so when
registerQCToCatalystQuantumPasses() is re-enabled the reverse conversion won't
be available; fix this by adding QCToCatalystQuantum to the LINK_LIBS for the
target (i.e., ensure LINK_LIBS includes both CatalystQuantumToQC and
QCToCatalystQuantum for the ${TARGET_NAME} plugin built from mqt-plugin.cpp).

In `@lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp`:
- Around line 446-515: Adjoint handling is missing for parameterized rotation
and Ising gates: check op.getAdjoint() for RX/RY/RZ/PhaseShift (P) and their
controlled variants (CRX/CRY/CRZ/ControlledPhaseShift) and for
IsingXY/IsingXX/IsingYY/IsingZZ, and if adjoint is true, negate the rotation
parameter(s) before creating the qc ops; e.g., use a local angle =
op.getAdjoint() ? -paramsValues[0] : paramsValues[0] (and for IsingXY preserve
the added pi for the second argument while negating the phi), then pass that
angle into
CREATE_ONE_TARGET_ONE_PARAMETER_GATE_OP(RX)/rewriter.create<qc::RXOp>/CREATE_TWO_TARGET_ONE_PARAMETER_GATE_OP(RXX)/rewriter.create<qc::XXPlusYYOp>
etc., and for controlled forms negate the parameter supplied inside the lambda
similarly.
- Line 319: Remove the unused variable qcOp (declared as Operation* qcOp =
nullptr;) and update all places that assign to it (e.g., qcOp =
rewriter.create<...>(...)) to call rewriter.create<...>(...) directly for their
side effects (or use a (void) cast if you deliberately want to ignore the
return). Ensure no other code reads qcOp and delete its declaration so the "set
but not used" warning is resolved.

In `@lib/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.cpp`:
- Around line 315-326: InsertOp's result is being dropped so the updated
!quantum.reg is lost; change the creation to capture the returned value from
rewriter.create<catalyst::quantum::InsertOp> (e.g., auto newInsert =
rewriter.create<...>(...)) and then thread that result into the SSA flow by
replacing the original op's result or updating the conversion mapping (use
rewriter.replaceOp(op, newInsert.getResult(0)) or update your
ValueMapping/rewriter mapping so later uses see newInsert.getResult(0)); ensure
you remove the old op after replacing so no users remain referencing the
discarded value.
- Around line 37-41: Implement the pass's runOnOperation for the pass declared
by GEN_PASS_DEF_QCTOCATALYSTQUANTUM: create a new instance of ConversionTarget,
register legality for target dialects, populate a RewritePatternSet with the
existing patterns (ConvertQCAlloc, ConvertQCDealloc, ConvertQCExtract,
ConvertQCInject, etc.), and then call applyPartialConversion(getOperation(),
target, patterns). Ensure the pass class (the one generated by
GEN_PASS_DEF_QCTOCATALYSTQUANTUM) overrides runOnOperation(), constructs the
target and patterns, marks ops as legal/illegal as appropriate, and handles
failure by signalling signalPassFailure() when applyPartialConversion fails.

In `@lib/mqt-plugin.cpp`:
- Around line 11-13: The build fails because the TableGen-generated header
QCToCatalystQuantum.h.inc isn't being produced/visible; update
include/mlir/Conversion/QCToCatalystQuantum/CMakeLists.txt to run mlir_tablegen
to generate QCToCatalystQuantum.h.inc (invoking the correct TABLEGEN or
MLIR_TABLEGEN rule for the QCToCatalystQuantum tablegen file) and add the
generated include directory to the consuming target via
target_include_directories or target_include_directories(... SYSTEM BEFORE) so
that the target building lib/mqt-plugin.cpp can find
"mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.h" which expects the
.h.inc; ensure the generated file name matches QCToCatalystQuantum.h.inc and
that the target that builds mqt-plugin depends on the tablegen output target so
generation happens before compilation.
- Around line 46-47: Add a short TODO comment next to the commented-out
registerQCToCatalystQuantumPasses() call in lib/mqt-plugin.cpp to indicate the
back-conversion is incomplete and needs re-enabling or completion later;
reference the symbol registerQCToCatalystQuantumPasses() and mention
“back-conversion incomplete” and optionally link to the PR or issue number if
available so future maintainers can discover why it is disabled.

In `@test/test_plugin.py`:
- Around line 560-565: The test expects nested qc.ctrl blocks but
ConvertQuantumCustomOp emits a single flattened qc.ctrl with the full control
list; update the CHECK in test_plugin.py to match a single qc.ctrl containing
qc.rx (i.e., replace the nested "qc.ctrl { qc.ctrl { qc.rx ... } }" pattern with
a flattened "qc.ctrl(...) { qc.rx(...) ... }" pattern that accepts the full
control list and the qc.rx operation, referencing ConvertQuantumCustomOp,
qc.ctrl and qc.rx in the adjusted CHECK.
- Around line 788-797: The CHECK pattern in the check_after_qc string is wrong:
remove the stray trailing comma in the second qc.ctrl and change the nested
qc.ctrl expectation to match the converter's flattened control construct;
replace the nested pattern "qc.ctrl(%[[Q0_3:.*]]) { qc.ctrl(%[[Q0_0:.*]],
%[[Q0_1:.*]],) { qc.x %[[Q0_2:.*]] : !qc.qubit } : !qc.qubit } : !qc.qubit" with
a single flattened control like "qc.ctrl(%[[Q0_0:.*]], %[[Q0_1:.*]],
%[[Q0_3:.*]]) { qc.x %[[Q0_2:.*]] : !qc.qubit } : !qc.qubit" so FileCheck
matches the emitted Toffoli representation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e22f9b1a-28e1-47f3-807d-9502e02e7eec

📥 Commits

Reviewing files that changed from the base of the PR and between 4b76fa6 and 0b1a52a.

📒 Files selected for processing (20)
  • cmake/ExternalDependencies.cmake
  • include/mlir/Conversion/CMakeLists.txt
  • include/mlir/Conversion/CatalystQuantumToMQTOpt/CMakeLists.txt
  • include/mlir/Conversion/CatalystQuantumToQC/CMakeLists.txt
  • include/mlir/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.h
  • include/mlir/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.td
  • include/mlir/Conversion/MQTOptToCatalystQuantum/CMakeLists.txt
  • include/mlir/Conversion/QCToCatalystQuantum/CMakeLists.txt
  • include/mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.h
  • include/mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.td
  • lib/CMakeLists.txt
  • lib/Conversion/CMakeLists.txt
  • lib/Conversion/CatalystQuantumToMQTOpt/CatalystQuantumToMQTOpt.cpp
  • lib/Conversion/CatalystQuantumToQC/CMakeLists.txt
  • lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp
  • lib/Conversion/MQTOptToCatalystQuantum/MQTOptToCatalystQuantum.cpp
  • lib/Conversion/QCToCatalystQuantum/CMakeLists.txt
  • lib/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.cpp
  • lib/mqt-plugin.cpp
  • test/test_plugin.py
💤 Files with no reviewable changes (4)
  • include/mlir/Conversion/CatalystQuantumToMQTOpt/CMakeLists.txt
  • include/mlir/Conversion/MQTOptToCatalystQuantum/CMakeLists.txt
  • lib/Conversion/CatalystQuantumToMQTOpt/CatalystQuantumToMQTOpt.cpp
  • lib/Conversion/MQTOptToCatalystQuantum/MQTOptToCatalystQuantum.cpp

Comment on lines 17 to +21
#define GEN_PASS_DECL
#include "mlir/Conversion/CatalystQuantumToMQTOpt/CatalystQuantumToMQTOpt.h.inc"
#include "mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.h.inc"

#define GEN_PASS_REGISTRATION
#include "mlir/Conversion/CatalystQuantumToMQTOpt/CatalystQuantumToMQTOpt.h.inc"
#include "mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.h.inc"
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.

⚠️ Potential issue | 🔴 Critical

Generate QCToCatalystQuantum.h.inc before including it here.

CI already fails because this renamed header is not being produced. Please add or fix the matching include-side TableGen wiring for QCToCatalystQuantum before merging; otherwise every translation unit that includes this header will fail to compile.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/mlir/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.h` around
lines 17 - 21, The generated header QCToCatalystQuantum.h.inc is missing from
the build, so update the TableGen/include-side wiring for the
QCToCatalystQuantum pass: add the TableGen rule to emit
QCToCatalystQuantum.h.inc (or restore its name) and ensure the build target that
produces it is run before compiling code that includes it; specifically ensure
the pass name QCToCatalystQuantum is registered in your TableGen input and that
the GEN_PASS_DECL / GEN_PASS_REGISTRATION expansion for QCToCatalystQuantum is
being generated by the TableGen backend so the include
"QCToCatalystQuantum.h.inc" is produced for downstream translation units (update
build files or CMake target dependencies as needed).

#include <Quantum/IR/QuantumDialect.h>
#include <Quantum/IR/QuantumOps.h>
#include <cassert>
#include <cstddef>
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.

🧹 Nitpick | 🔵 Trivial

Remove unused include <cstddef>.

Static analysis indicates this header is not used directly.

-#include <cstddef>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#include <cstddef>
🧰 Tools
🪛 Cppcheck (2.20.0)

[information] 18-18: Include file

(missingInclude)


[information] 18-18: Include file

(missingIncludeSystem)

🪛 GitHub Check: 🇨‌ Lint / 🚨 Lint

[warning] 18-18: lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp:18:1 [misc-include-cleaner]
included header cstddef is not used directly

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp` at line 18,
Remove the unused include directive: delete the line `#include <cstddef>` from
CatalystQuantumToQC.cpp so the file no longer contains an unused header; verify
compilation passes and no symbols from `<cstddef>` (e.g., `size_t`, `nullptr_t`)
are relied on in functions or classes within this translation unit before
committing.

// Create the new operation
// Note: quantum.measure returns (i1, !quantum.bit)
// qc.measure returns i1
auto QCOp = rewriter.create<qc::MeasureOp>(op.getLoc(), inQubit);
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.

🧹 Nitpick | 🔵 Trivial

Fix variable naming: QCOp should be qcOp.

CI warns: "invalid case style for local variable 'QCOp'". Local variables should use lowerCamelCase.

-    auto QCOp = rewriter.create<qc::MeasureOp>(op.getLoc(), inQubit);
+    auto qcOp = rewriter.create<qc::MeasureOp>(op.getLoc(), inQubit);
🧰 Tools
🪛 GitHub Check: 🇨‌ Lint / 🚨 Lint

[warning] 164-164: lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp:164:10 [readability-identifier-naming]
invalid case style for local variable 'QCOp'

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/Conversion/CatalystQuantumToQC/CatalystQuantumToQC.cpp` at line 164,
Rename the local variable QCOp to lowerCamelCase qcOp where it's created with
rewriter.create<qc::MeasureOp>(op.getLoc(), inQubit) and update all subsequent
uses (e.g., any reads, .getResult(), or replacements that reference QCOp) to
qcOp to satisfy the project's naming conventions and silence the CI lint
warning.

Comment on lines +95 to +107
template <typename OpAdaptor>
ExtractedOperands<OpAdaptor>
extractOperands(OpAdaptor adaptor, ConversionPatternRewriter& rewriter,
Location loc) {
const ValueRange inQubits = adaptor.getInQubits();
const ValueRange posCtrlQubits = adaptor.getPosCtrlInQubits();
const ValueRange negCtrlQubits = adaptor.getNegCtrlInQubits();

const ControlInfo ctrlInfo =
extractControlInfo(posCtrlQubits, negCtrlQubits, rewriter, loc);

return {inQubits, ctrlInfo};
}
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.

🧹 Nitpick | 🔵 Trivial

Remove unused extractOperands helper.

The extractOperands template function is never called. Static analysis confirms: "The function 'extractOperands' is never used."

🧹 Proposed fix
-/// Helper function to extract operands and control info - for more complex
-/// cases
-template <typename OpAdaptor> struct ExtractedOperands {
-  ValueRange inQubits;
-  ControlInfo ctrlInfo;
-};
-
-template <typename OpAdaptor>
-ExtractedOperands<OpAdaptor>
-extractOperands(OpAdaptor adaptor, ConversionPatternRewriter& rewriter,
-                Location loc) {
-  const ValueRange inQubits = adaptor.getInQubits();
-  const ValueRange posCtrlQubits = adaptor.getPosCtrlInQubits();
-  const ValueRange negCtrlQubits = adaptor.getNegCtrlInQubits();
-
-  const ControlInfo ctrlInfo =
-      extractControlInfo(posCtrlQubits, negCtrlQubits, rewriter, loc);
-
-  return {inQubits, ctrlInfo};
-}
🧰 Tools
🪛 Cppcheck (2.20.0)

[style] 97-97: The function 'extractOperands' is never used.

(unusedFunction)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/Conversion/QCToCatalystQuantum/QCToCatalystQuantum.cpp` around lines 95 -
107, The template helper function extractOperands (template<typename OpAdaptor>)
is unused; remove its definition and any related forward declarations to clean
up dead code—specifically delete the extractOperands function block that returns
ExtractedOperands and references adaptor.getInQubits(), getPosCtrlInQubits(),
getNegCtrlInQubits(), and extractControlInfo; after removal, run a build to
ensure no remaining references to ExtractedOperands or extractOperands remain
and remove any now-unneeded includes or using declarations introduced solely for
this helper.

Comment on lines +43 to +44
("ToQC", ["builtin.module(catalystquantum-to-qc)"]),
("ToCatalystQuantum", ["builtin.module(qc-to-catalystquantum)"]),
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.

⚠️ Potential issue | 🟠 Major

Tests will fail until back-conversion is enabled.

The DEFAULT_PIPELINE includes "qc-to-catalystquantum" at line 44, but registerQCToCatalystQuantumPasses() is commented out in lib/mqt-plugin.cpp. Tests exercising the full roundtrip will fail until back-conversion is complete.

Consider either:

  1. Commenting out the back-conversion step in DEFAULT_PIPELINE for now
  2. Adding pytest.mark.skip to roundtrip tests until back-conversion is ready
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test_plugin.py` around lines 43 - 44, DEFAULT_PIPELINE currently
includes the back-conversion step ("ToCatalystQuantum",
["builtin.module(qc-to-catalystquantum)"]) but
registerQCToCatalystQuantumPasses() in lib/mqt-plugin.cpp is commented out,
causing roundtrip tests to fail; fix by either removing/commenting the
("ToCatalystQuantum", ["builtin.module(qc-to-catalystquantum)"]) entry from
DEFAULT_PIPELINE in test/test_plugin.py or, if you prefer to keep the pipeline
unchanged, mark the failing roundtrip tests with pytest.mark.skip until
registerQCToCatalystQuantumPasses() is implemented—search for DEFAULT_PIPELINE
and registerQCToCatalystQuantumPasses to locate the relevant spots to change.

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.

1 participant