Skip to content

Conversation

@burgholzer
Copy link
Member

Description

This is a very early draft of the dialect rewrite proposed in #1225
I am opening this now, as I will probably only get back to it in roughly two weeks and I want others to check this out directly.

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.

@burgholzer burgholzer added this to the MLIR Support milestone Oct 17, 2025
@burgholzer burgholzer self-assigned this Oct 17, 2025
@burgholzer burgholzer added refactor Anything related to code refactoring minor Minor version update c++ Anything related to C++ code MLIR Anything related to MLIR labels Oct 17, 2025
@burgholzer
Copy link
Member Author

At the moment, this PR is purely additive to the existing infrastructure.
It introduces:

  • the Quartz dialect (=MQTRef)
  • the Flux dialect (=MQTOpt)

For both of these dialects, the following operations are supported at the moment:

  • alloc
  • dealloc
  • static
  • measure
  • reset

The following conversions or translations are available:

  • qc::QuantumComputation -> Quartz
  • Quartz -> Flux
  • Flux -> Quartz
  • Quartz -> QIR

Convenience builders for all three program types (Quartz, Flux, QIR) have been created for testing.
An end-to-end compiler pipeline has been set up that runs

QuantumComputation -> Quartz -> Flux -> Optimization (within Flux) -> Quartz -> QIR

with canonicalizations applied at every stage.
A corresponding end-to-end googletest-based test suite has been set up that checks the existing features.

Noteworthy changes so far:

  • Quantum and classical register handling has been completely revamped. memref is no longer used anywhere. Register information is merely tagged on alloc (quantum registers) and measure (classical registers) operations as optional metadata.
  • The QIR conversion code has been improved with better compliance to the base profile spec and better output recording

@burgholzer burgholzer force-pushed the dialect-implementation branch from 0d43836 to 36b6748 Compare October 23, 2025 19:45
@denialhaag denialhaag requested review from denialhaag and removed request for denialhaag October 23, 2025 20:04
@denialhaag denialhaag self-assigned this Oct 23, 2025
@denialhaag denialhaag force-pushed the dialect-implementation branch 2 times, most recently from 9206426 to dd59932 Compare October 24, 2025 15:55
Copy link
Member Author

@burgholzer burgholzer left a comment

Choose a reason for hiding this comment

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

@denialhaag just some drive-by comments to knock you in the right direction 🙂

Copy link
Member Author

@burgholzer burgholzer left a comment

Choose a reason for hiding this comment

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

Thanks again for your continued work on this @denialhaag 🙌 this is great progress!

I think I managed to go through all of the latest changes and collect my thoughts.
I tried to keep the comments as redundancy-free as possible. So everything I annotated in one dialect or one conversion probably also holds for the others.

Copy link
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: 6

♻️ Duplicate comments (9)
mlir/include/mlir/Dialect/Quartz/Builder/QuartzProgramBuilder.h (2)

137-155: ClassicalRegister stores non-owning StringRef - document lifetime requirements.

The ClassicalRegister::name field is a StringRef, which doesn't own the underlying string data. If a caller passes a temporary std::string, the stored reference will dangle. Current usages appear to pass string literals, but the API permits unsafe usage.

Consider either:

  1. Changing to std::string name to own the data, or
  2. Adding documentation that callers must ensure the string outlives the builder

168-169: Returned reference may become invalid after subsequent allocations.

allocClassicalBitRegister returns ClassicalRegister&, but the backing store is SmallVector<ClassicalRegister> (line 874). If the vector reallocates during a later call, previously returned references become dangling pointers.

auto& c1 = builder.allocClassicalBitRegister(3, "c1");
auto& c2 = builder.allocClassicalBitRegister(3, "c2"); // may reallocate
builder.measure(q, c1[0]); // c1 potentially dangling!

Consider returning by value or using a stable container (std::deque, std::list).

mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp (1)

102-109: Reference invalidation applies here.

As noted in the header review, emplace_back returns a reference to an element in a SmallVector that may be invalidated by future allocations. This is the implementation side of the issue flagged in the header.

mlir/lib/Compiler/CompilerPipeline.cpp (1)

244-258: currentStage counter only increments when recordIntermediates is true.

The summary at line 252 uses currentStage to report processed stages, but this counter is only incremented inside the record != nullptr && config_.recordIntermediates blocks. If printIRAfterAllStages is enabled independently (without recordIntermediates), the summary will incorrectly report "Successfully processed 0 stages".

Consider tracking currentStage independently:

+  auto currentStage = 0;
+  const auto incrementStage = [&]() { ++currentStage; };
+
   // Stage 1: Quartz import
+  incrementStage();
   if (record != nullptr && config_.recordIntermediates) {
     record->afterQuartzImport = captureIR(module);
     if (config_.printIRAfterAllStages) {
-      prettyPrintStage(module, "Quartz Import", ++currentStage, totalStages);
+      prettyPrintStage(module, "Quartz Import", currentStage, totalStages);
     }
   }

Or ensure printIRAfterAllStages implies recordIntermediates at the config level.

mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (1)

120-159: Potential duplicate global symbol if called with the same label twice.

createResultLabel creates a global without checking if one already exists with the same name. If called multiple times with identical label and symbolPrefix, this will create duplicate globals, potentially causing LLVM verification errors.

Consider checking for existing globals before creating:

 LLVM::AddressOfOp createResultLabel(OpBuilder& builder, Operation* op,
                                     const StringRef label,
                                     const StringRef symbolPrefix) {
   // Save current insertion point
   const OpBuilder::InsertionGuard insertGuard(builder);

   // Create the declaration at the start of the module
   auto module = dyn_cast<ModuleOp>(op);
   if (!module) {
     module = op->getParentOfType<ModuleOp>();
   }
+  if (!module) {
+    llvm::reportFatalInternalError("Module not found");
+  }
+
+  const auto symbolName =
+      builder.getStringAttr((symbolPrefix + "_" + label).str());
+
+  // Check if global already exists
+  if (module.lookupSymbol<LLVM::GlobalOp>(symbolName)) {
+    // Reuse existing - create AddressOfOp to it
+    auto main = getMainFunction(op);
+    if (!main) {
+      llvm::reportFatalInternalError("Main function not found");
+    }
+    auto& firstBlock = *(main.getBlocks().begin());
+    builder.setInsertionPointToStart(&firstBlock);
+    return builder.create<LLVM::AddressOfOp>(
+        op->getLoc(), LLVM::LLVMPointerType::get(builder.getContext()),
+        symbolName);
+  }
+
   builder.setInsertionPointToStart(module.getBody());
-
-  const auto symbolName =
-      builder.getStringAttr((symbolPrefix + "_" + label).str());
mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp (1)

432-442: Potential index collision with default register name "c".

When measurements lack register info, they're assigned to a synthetic register named "c" with sequential indices starting from numResults. If the circuit also has explicit measurements to a register named "c", the indices could potentially overlap, leading to incorrect output labeling.

Consider using a distinct synthetic name (e.g., "__unnamed__" or "__default__") or tracking whether "c" is already used as an explicit register:

-      registerResultMap.insert({{"c", numResults}, resultValue});
+      registerResultMap.insert({{"__unnamed__", numResults}, resultValue});
mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h (3)

144-162: ClassicalRegister stores non-owning StringRef - document lifetime requirements.

The ClassicalRegister struct stores StringRef name, which is a non-owning reference. Since instances are stored in allocatedClassicalRegisters, there is a latent risk: if a caller passes a temporary std::string, the stored StringRef will become a dangling reference. Current test usages pass string literals (static storage), avoiding the issue.

Either document the lifetime requirement clearly or switch to owned storage:

 struct ClassicalRegister {
-  /// Name of the classical register
+  /// Name of the classical register.
+  /// @note The caller must ensure this string outlives the builder.
   StringRef name;

Or use std::string for safety:

-  StringRef name;
+  std::string name;

246-260: Documentation claims chaining but signature is void.

The Doxygen comment for OP_NAME doesn't explicitly mention a return type, but the pattern of the builder suggests chaining. The actual signature at line 260 is void OP_NAME(...), which doesn't support method chaining unlike other methods like dealloc() that return FluxProgramBuilder&.

Either update the signature to return FluxProgramBuilder& for consistency, or clarify in docs that this is a void operation.


968-992: ctrl example uses scalars where the API expects ValueRange.

The documentation example shows:

{controls_out, targets_out} = builder.ctrl(q0_in, q1_in, [&](auto& b) {

But the actual signature at line 990-992 takes ValueRange controls, ValueRange targets, not individual Value arguments. The lambda signature also differs.

Update the example to match the actual API:

auto [controls_out, targets_out] =
    builder.ctrl(ValueRange{q0_in}, ValueRange{q1_in},
                 [&](OpBuilder& b, ValueRange ts) {
                   auto q1_res = b.x(ts.front());
                   return ValueRange{q1_res};
                 });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5529be and 998242c.

📒 Files selected for processing (18)
  • mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h (1 hunks)
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h (1 hunks)
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h (1 hunks)
  • mlir/include/mlir/Dialect/Quartz/Builder/QuartzProgramBuilder.h (1 hunks)
  • mlir/include/mlir/Dialect/Utils/Utils.h (1 hunks)
  • mlir/lib/Compiler/CompilerPipeline.cpp (1 hunks)
  • mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp (1 hunks)
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp (1 hunks)
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp (1 hunks)
  • mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp (1 hunks)
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp (1 hunks)
  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp (1 hunks)
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp (1 hunks)
  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (1 hunks)
  • mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp (1 hunks)
  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp (1 hunks)
  • mlir/lib/Dialect/Quartz/IR/Operations/StandardGates/U2Op.cpp (1 hunks)
  • mlir/tools/mqt-cc/mqt-cc.cpp (1 hunks)
🧰 Additional context used
🧠 Learnings (17)
📓 Common learnings
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1360
File: .github/workflows/reusable-mlir-tests.yml:40-43
Timestamp: 2025-12-05T17:45:37.602Z
Learning: In the munich-quantum-toolkit/core repository, patch releases of LLVM dependencies don't require documentation updates, changelog entries, or additional tests beyond what's validated by passing CI checks.
📚 Learning: 2025-12-08T14:55:43.880Z
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp:78-100
Timestamp: 2025-12-08T14:55:43.880Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp), GPhaseOp is a zero-target operation (global phase). When a CtrlOp wraps a GPhaseOp, it only has control qubits and no targets. The CtrlInlineGPhase canonicalization pattern correctly produces outputs only for the positive controls, not targets.

Applied to files:

  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
  • mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp
📚 Learning: 2025-11-20T18:19:31.857Z
Learnt from: lsschmid
Repo: munich-quantum-toolkit/qmap PR: 832
File: src/hybridmap/HybridNeutralAtomMapper.cpp:1679-1715
Timestamp: 2025-11-20T18:19:31.857Z
Learning: In src/hybridmap/HybridNeutralAtomMapper.cpp, the compareSwapAndBridge and compareShuttlingAndFlyingAncilla methods intentionally use division by zero to produce infinity when distance reductions are zero or negative. This infinity value marks that option as highly unfavorable in cost comparisons, which is the intended behavior rather than a bug.

Applied to files:

  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
📚 Learning: 2025-12-07T09:10:31.820Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1355
File: src/qdmi/sc/Device.cpp:97-102
Timestamp: 2025-12-07T09:10:31.820Z
Learning: In the munich-quantum-toolkit/core repository, duplication of QDMI-related macros (such as IS_INVALID_ARGUMENT) across device implementations (e.g., in src/qdmi/sc/Device.cpp and src/qdmi/dd/Device.cpp) is acceptable as a temporary measure. The preferred long-term solution is to upstream these macros to the QDMI repository rather than creating local shared headers, so they can be reused across all dependent projects.

Applied to files:

  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-10-09T13:20:11.483Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/test/Dialect/MQTOpt/Transforms/lift-measurements.mlir:269-288
Timestamp: 2025-10-09T13:20:11.483Z
Learning: In the MQT MLIR dialect, the `rz` gate should not be included in the `DIAGONAL_GATES` set for the `ReplaceBasisStateControlsWithIfPattern` because its operator matrix does not have the required shape | 1 0 | / | 0 x | for the targets-as-controls optimization. It is only included in `LiftMeasurementsAboveGatesPatterns` where the matrix structure requirement differs.

Applied to files:

  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
  • mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-12-02T07:37:46.860Z
Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp:144-151
Timestamp: 2025-12-02T07:37:46.860Z
Learning: In MLIR transformation code (mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp and similar), when inserting operations before a target operation, prefer `rewriter.setInsertionPoint(op)` over `rewriter.setInsertionPointAfter(op->getPrevNode())`. The former is cleaner, avoids null pointer edge cases (when op is first in block), and is semantically clearer.

Applied to files:

  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp
  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
  • mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
  • mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp
📚 Learning: 2025-12-08T12:44:05.874Z
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp:60-70
Timestamp: 2025-12-08T12:44:05.874Z
Learning: In the Quartz dialect (mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp), negative controls are not supported at the current stage. The RemoveTrivialCtrl pattern correctly only checks getNumPosControls() when determining if a CtrlOp should be removed.

Applied to files:

  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp
  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp
  • mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
  • mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-10-09T13:13:51.224Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:171-180
Timestamp: 2025-10-09T13:13:51.224Z
Learning: In MQT Core MLIR, UnitaryInterface operations guarantee 1-1 correspondence between input and output qubits in the same order. When cloning or modifying unitary operations (e.g., removing controls), this correspondence is maintained by construction, so yielding getAllInQubits() in else-branches matches the result types from the operation's outputs.

Applied to files:

  • mlir/lib/Dialect/Quartz/IR/Operations/StandardGates/U2Op.cpp
  • mlir/include/mlir/Dialect/Quartz/Builder/QuartzProgramBuilder.h
  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
  • mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp
  • mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
  • mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-10-09T13:14:10.178Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:219-221
Timestamp: 2025-10-09T13:14:10.178Z
Learning: The MQT Core project (munich-quantum-toolkit/core repository) uses the C++20 standard, not C++17. C++20 features such as abbreviated function templates (e.g., `const auto&` parameters) are supported and valid in this codebase.

Applied to files:

  • mlir/tools/mqt-cc/mqt-cc.cpp
📚 Learning: 2025-12-05T17:45:37.602Z
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1360
File: .github/workflows/reusable-mlir-tests.yml:40-43
Timestamp: 2025-12-05T17:45:37.602Z
Learning: In the munich-quantum-toolkit/core repository, patch releases of LLVM dependencies don't require documentation updates, changelog entries, or additional tests beyond what's validated by passing CI checks.

Applied to files:

  • mlir/tools/mqt-cc/mqt-cc.cpp
  • mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp
📚 Learning: 2025-10-09T13:25:36.887Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReuseQubitsPattern.cpp:98-100
Timestamp: 2025-10-09T13:25:36.887Z
Learning: In MLIR code, when traversing parent operations to find a common block between two operations where one uses the result of another, explicit bounds checking is not necessary. MLIR's SSA properties and scope locality guarantees ensure that operations using results must be in compatible scopes and will always share a common ancestor in the operation hierarchy.

Applied to files:

  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-10-07T15:30:42.946Z
Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1237
File: mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h:219-231
Timestamp: 2025-10-07T15:30:42.946Z
Learning: In the Layout class for MLIR quantum routing (mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h), the swap method intentionally does NOT swap the hw fields in QubitInfo. This is correct because SSA values represent quantum states at fixed hardware locations, and only their program index associations change during a SWAP gate. The hw field indicates where an SSA value physically resides and remains constant.

Applied to files:

  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
📚 Learning: 2025-10-09T04:30:29.071Z
Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1237
File: mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Scheduler.h:72-140
Timestamp: 2025-10-09T04:30:29.071Z
Learning: In the CrawlScheduler (mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Scheduler.h), the persistent `layoutCopy` is updated across all lookahead layers via `remapQubitValue` calls. After each two-qubit gate is scheduled in a layer, its input qubits are remapped to output qubits, ensuring the next layer's crawl starts from those outputs. This progressive remapping through SSA values prevents the same operation from being scheduled in multiple lookahead layers.

Applied to files:

  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
📚 Learning: 2025-11-27T08:52:06.428Z
Learnt from: lsschmid
Repo: munich-quantum-toolkit/qmap PR: 832
File: src/hybridmap/HybridNeutralAtomMapper.cpp:1379-1383
Timestamp: 2025-11-27T08:52:06.428Z
Learning: In src/hybridmap/HybridNeutralAtomMapper.cpp, the getBestMovePos function throws std::runtime_error when no move position is found (finalBestPos.coords is empty). This behavior is acceptable for current use cases since tight architectures are not a concern. A future improvement would be to fall back to SWAP-based strategies instead of throwing.

Applied to files:

  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
📚 Learning: 2025-10-09T13:28:29.237Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/DeadGateEliminationPattern.cpp:42-45
Timestamp: 2025-10-09T13:28:29.237Z
Learning: In the MQTOpt MLIR dialect, linear types enforce single-use semantics where each qubit value can only be consumed once, preventing duplicate deallocations and making RAUW operations safe when replacing output qubits with corresponding input qubits in transformation patterns.

Applied to files:

  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h
📚 Learning: 2025-12-04T06:59:40.314Z
Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.cpp:84-85
Timestamp: 2025-12-04T06:59:40.314Z
Learning: In the MQTOpt MLIR routing passes (NaiveRoutingPassSC, AStarRoutingPassSC), the input IR is guaranteed to contain only 1-qubit and 2-qubit gates. All 3+-qubit gates must be decomposed before routing; otherwise the input IR is invalid. This invariant allows skipTwoQubitBlock in LayeredUnit.cpp to safely assert wires.size() == 2.

Applied to files:

  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
📚 Learning: 2025-11-24T10:19:41.147Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1326
File: python/mqt/core/__init__.py:22-22
Timestamp: 2025-11-24T10:19:41.147Z
Learning: In the munich-quantum-toolkit/core repository, Ruff is configured with 'ALL' rules enabled by default, and only specific rules are selectively disabled. When reviewing changes that enable previously-disabled rules (like PLC0415), noqa directives for those rules become necessary and should be retained.

Applied to files:

  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
🧬 Code graph analysis (8)
mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp (2)
mlir/include/mlir/Dialect/Utils/Utils.h (2)
  • mlir (18-42)
  • variantToValue (30-40)
mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXMinusYYOp.cpp (6)
  • op (37-70)
  • op (37-38)
  • build (75-82)
  • build (75-78)
  • getCanonicalizationPatterns (84-87)
  • getCanonicalizationPatterns (84-85)
mlir/include/mlir/Dialect/Quartz/Builder/QuartzProgramBuilder.h (2)
mlir/include/mlir/Dialect/Quartz/IR/QuartzDialect.h (1)
  • mlir (52-119)
mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp (25)
  • QuartzProgramBuilder (31-33)
  • initialize (35-53)
  • initialize (35-35)
  • allocQubit (55-64)
  • allocQubit (55-55)
  • staticQubit (66-75)
  • staticQubit (66-66)
  • allocQubitRegister (77-100)
  • allocQubitRegister (78-79)
  • allocClassicalBitRegister (102-109)
  • allocClassicalBitRegister (103-103)
  • measure (115-118)
  • measure (115-115)
  • measure (120-127)
  • measure (120-121)
  • reset (129-132)
  • reset (129-129)
  • barrier (374-377)
  • barrier (374-374)
  • ctrl (383-388)
  • ctrl (384-385)
  • dealloc (394-407)
  • dealloc (394-394)
  • finalize (413-430)
  • finalize (413-413)
mlir/include/mlir/Dialect/Flux/FluxUtils.h (5)
mlir/include/mlir/Dialect/Utils/Utils.h (1)
  • mlir (18-42)
mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp (2)
  • op (36-70)
  • op (36-37)
mlir/lib/Dialect/Flux/IR/Operations/StandardGates/ROp.cpp (4)
  • op (36-53)
  • op (36-37)
  • op (62-79)
  • op (62-63)
mlir/lib/Dialect/Flux/IR/Operations/StandardGates/UOp.cpp (6)
  • op (36-55)
  • op (36-37)
  • op (64-84)
  • op (64-65)
  • op (93-112)
  • op (93-94)
mlir/unittests/pipeline/test_compiler_pipeline.cpp (2)
  • op (72-92)
  • op (72-72)
mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp (1)
mlir/include/mlir/Dialect/Quartz/IR/QuartzDialect.h (11)
  • mlir (52-119)
  • getNumTargets (70-70)
  • getNumControls (71-71)
  • getNumPosControls (72-72)
  • getNumNegControls (73-73)
  • getQubit (75-83)
  • getTarget (84-92)
  • getPosControl (94-96)
  • getNegControl (98-100)
  • getNumParams (102-102)
  • getParameter (104-109)
mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp (1)
mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (8)
  • getOrCreateFunctionDeclaration (88-118)
  • getOrCreateFunctionDeclaration (88-90)
  • createPointerFromIndex (161-169)
  • createPointerFromIndex (161-162)
  • createResultLabel (120-159)
  • createResultLabel (120-122)
  • setQIRAttributes (56-86)
  • setQIRAttributes (56-56)
mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp (3)
mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp (3)
  • typeConverter (1150-1150)
  • nodiscard (109-109)
  • ctx (1146-1261)
mlir/include/mlir/Dialect/Quartz/IR/QuartzDialect.h (1)
  • nodiscard (111-117)
mlir/include/mlir/Dialect/Flux/IR/FluxDialect.h (1)
  • nodiscard (120-126)
mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp (1)
mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp (19)
  • op (520-538)
  • op (521-522)
  • op (560-576)
  • op (561-562)
  • op (598-617)
  • op (599-600)
  • op (647-671)
  • op (648-649)
  • op (698-717)
  • op (699-700)
  • op (1014-1039)
  • op (1015-1016)
  • op (1063-1115)
  • op (1064-1065)
  • op (1134-1143)
  • op (1135-1136)
  • context (1172-1233)
  • patterns (1180-1180)
  • typeConverter (1181-1181)
mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h (1)
mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (10)
  • getMainFunction (31-54)
  • getMainFunction (31-31)
  • setQIRAttributes (56-86)
  • setQIRAttributes (56-56)
  • getOrCreateFunctionDeclaration (88-118)
  • getOrCreateFunctionDeclaration (88-90)
  • createResultLabel (120-159)
  • createResultLabel (120-122)
  • createPointerFromIndex (161-169)
  • createPointerFromIndex (161-162)
🪛 ast-grep (0.40.0)
mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp

[warning] 172-172: The getTargetsIn function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsIn()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 179-179: The getTargetsOut function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsOut()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 211-211: The getTargetsOut function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsOut()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 212-212: The getTargetsIn function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsIn()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 225-225: The getTargetsIn function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsIn()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 226-226: The getTargetsOut function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsOut()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🐍 Test (macos-14) / 🐍 macos-14
  • GitHub Check: 🐍 Test (macos-15-intel) / 🐍 macos-15-intel
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (27)
mlir/include/mlir/Dialect/Flux/FluxUtils.h (2)

28-75: Inverse‑pair and merge helpers look structurally sound.

The inverse‑pair and zero‑parameter merge templates correctly:

  • Restrict to predecessors of the expected op type via getDefiningOp<...>(),
  • Update qubit uses through replaceOp (trivializing predecessors and the current op),
  • Preserve operand ordering and use consistent qubit indices.

Given the linear SSA discipline for qubit values in this codebase, the lack of explicit hasOneUse() checks on the predecessor is acceptable here.


188-205: Fix namespace for TOLERANCE to avoid compile errors.

Inside namespace mlir::flux, utils::TOLERANCE does not resolve to mlir::utils::TOLERANCE unless there is a using or a global ::utils. This is very likely a hard compile error:

if (std::abs(paramValue) > utils::TOLERANCE) {

and similarly below.

Use the fully qualified name instead:

-  if (std::abs(paramValue) > utils::TOLERANCE) {
+  if (std::abs(paramValue) > ::mlir::utils::TOLERANCE) {

and at line 224 as well.

If <cmath> is not already pulled in by other headers, it’s also worth adding it explicitly for std::abs.

⛔ Skipped due to learnings
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp:60-70
Timestamp: 2025-12-08T12:44:05.874Z
Learning: In the Quartz dialect (mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp), negative controls are not supported at the current stage. The RemoveTrivialCtrl pattern correctly only checks getNumPosControls() when determining if a CtrlOp should be removed.
mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp (2)

75-100: CtrlInlineGPhase matches the intended zero‑target GPhase semantics.

The GPhase inlining pattern correctly:

  • Restricts to GPhaseOp bodies,
  • Creates POps only for positive controls using getInputPosControl(i),
  • Replaces the CtrlOp with the resulting control outputs and erases the modifier.

Given that GPhaseOp is a global (zero‑target) phase and a controlled GPhase only has control qubits, producing outputs solely for positive controls is consistent with the design.


266-315: CtrlOp::verify is robust and aligns with Unitary/Region invariants.

The verifier enforces:

  • Exactly a unitary + yield in the body region,
  • Yield arity matching getNumTargets(),
  • No duplicate qubits among controls and body inputs/outputs,
  • No controlled BarrierOp.

This is a solid set of invariants for modifier correctness and should catch most structural issues early.

mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp (1)

60-69: Clarify semantics of RemoveTrivialCtrl and avoid replacing with a nested op.

RemoveTrivialCtrl currently does:

if (ctrlOp.getNumPosControls() > 0) {
  return failure();
}
rewriter.replaceOp(ctrlOp, ctrlOp.getBodyUnitary());

Here getBodyUnitary() is an op inside ctrlOp’s body region. Using it directly as the replacement is fragile: erasing ctrlOp will also destroy that nested op, and the intent (drop just the modifier vs. drop modifier + body) is unclear.

If the goal is to keep the body and remove only the control wrapper, I’d suggest cloning the body before erasing ctrlOp:

LogicalResult matchAndRewrite(CtrlOp ctrlOp,
                              PatternRewriter& rewriter) const override {
  if (ctrlOp.getNumPosControls() > 0) {
    return failure();
  }

-  rewriter.replaceOp(ctrlOp, ctrlOp.getBodyUnitary());
+  auto bodyUnitary = ctrlOp.getBodyUnitary();
+  OpBuilder::InsertionGuard guard(rewriter);
+  rewriter.setInsertionPoint(ctrlOp);
+  auto* clonedBody = rewriter.clone(*bodyUnitary.getOperation());
+  rewriter.replaceOp(ctrlOp, clonedBody->getResults());
   return success();
}

If instead the intended semantics really are “a CtrlOp without controls is a no‑op including its body”, using rewriter.eraseOp(ctrlOp) would communicate that more directly. In either case, relying on replaceOp with an op nested inside the one being erased is brittle.

⛔ Skipped due to learnings
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp:60-70
Timestamp: 2025-12-08T12:44:05.874Z
Learning: In the Quartz dialect (mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp), negative controls are not supported at the current stage. The RemoveTrivialCtrl pattern correctly only checks getNumPosControls() when determining if a CtrlOp should be removed.
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp:78-100
Timestamp: 2025-12-08T14:55:43.880Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp), GPhaseOp is a zero-target operation (global phase). When a CtrlOp wraps a GPhaseOp, it only has control qubits and no targets. The CtrlInlineGPhase canonicalization pattern correctly produces outputs only for the positive controls, not targets.
Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp:144-151
Timestamp: 2025-12-02T07:37:46.860Z
Learning: In MLIR transformation code (mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp and similar), when inserting operations before a target operation, prefer `rewriter.setInsertionPoint(op)` over `rewriter.setInsertionPointAfter(op->getPrevNode())`. The former is cleaner, avoids null pointer edge cases (when op is first in block), and is semantically clearer.
mlir/lib/Dialect/Quartz/IR/Operations/StandardGates/U2Op.cpp (1)

22-28: LGTM!

The U2Op::build overload correctly uses the variantToValue utility to convert variant parameters and delegates to the existing build path. This follows the same pattern as other gate operations in the codebase.

mlir/include/mlir/Dialect/Utils/Utils.h (1)

11-42: LGTM!

The header is now self-contained with all required MLIR includes. The TOLERANCE constant and variantToValue utility are well-documented and correctly implemented.

mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp (2)

33-71: LGTM!

The MergeSubsequentXXPlusYY canonicalization pattern correctly:

  1. Uses tolerance-based comparison for static beta values
  2. Falls back to SSA value equality when betas are not static constants
  3. Accumulates theta values using arith::AddFOp
  4. Trivializes the predecessor by replacing it with its input qubits

The implementation mirrors XXMinusYYOp appropriately.


75-87: LGTM!

The build overload and canonicalization pattern registration follow the established patterns in the codebase.

mlir/include/mlir/Dialect/Quartz/Builder/QuartzProgramBuilder.h (2)

229-798: Well-structured macro-generated gate API.

The macro-based approach for declaring gate operations with varying arities (zero/one/two/three parameters, one/two targets) provides a clean and consistent API surface. The controlled (c*) and multi-controlled (mc*) variants are properly generated.


800-863: LGTM!

The modifier (ctrl), deallocation (dealloc), and finalization (finalize) APIs are well-documented with clear examples showing both C++ usage and expected MLIR output.

mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp (3)

31-53: LGTM!

The constructor and initialization are well-structured. The constructor creates the module with an unknown location, and initialize() properly:

  1. Loads the Quartz dialect
  2. Sets up the main function with entry_point passthrough attribute
  3. Creates the entry block for subsequent operations

55-100: LGTM!

The qubit allocation methods correctly:

  • Validate inputs (non-negative index, positive size)
  • Track allocated qubits in allocatedQubits for automatic deallocation
  • Apply register metadata where appropriate (allocQubitRegister)

Note that staticQubit intentionally doesn't track qubits since static qubits represent pre-existing hardware resources rather than dynamically allocated ones.


394-407: LGTM!

The dealloc method correctly validates that the qubit was previously allocated and removes it from tracking before creating the DeallocOp. The fatal error on double-deallocation provides clear feedback to API users.

mlir/tools/mqt-cc/mqt-cc.cpp (2)

40-63: LGTM - cl::opt declarations are now correctly non-const.

The command-line option declarations are properly defined without the const qualifier, allowing LLVM's ParseCommandLineOptions to mutate them as required.


121-132: Critical: Undefined identifiers - variable names don't match declarations.

The cl::opt variables are declared as InputFilename, OutputFilename, ConvertToQIR, EnableTiming, EnableStatistics, and PrintIRAfterAllStages (lines 40-63), but the code references INPUT_FILENAME, OUTPUT_FILENAME, CONVERT_TO_QIR, ENABLE_TIMING, ENABLE_STATISTICS, and PRINT_IR_AFTER_ALL_STAGES. These are undefined identifiers and will cause compilation failures.

-  const auto module = loadMLIRFile(INPUT_FILENAME, &context);
+  const auto module = loadMLIRFile(InputFilename, &context);
   if (!module) {
-    errs() << "Failed to load input file: " << INPUT_FILENAME << "\n";
+    errs() << "Failed to load input file: " << InputFilename << "\n";
     return 1;
   }

   // Configure the compiler pipeline
   QuantumCompilerConfig config;
-  config.convertToQIR = CONVERT_TO_QIR;
-  config.enableTiming = ENABLE_TIMING;
-  config.enableStatistics = ENABLE_STATISTICS;
-  config.printIRAfterAllStages = PRINT_IR_AFTER_ALL_STAGES;
+  config.convertToQIR = ConvertToQIR;
+  config.enableTiming = EnableTiming;
+  config.enableStatistics = EnableStatistics;
+  config.printIRAfterAllStages = PrintIRAfterAllStages;

Also fix line 142:

-  if (writeOutput(module.get(), OUTPUT_FILENAME).failed()) {
-    errs() << "Failed to write output file: " << OUTPUT_FILENAME << "\n";
+  if (writeOutput(module.get(), OutputFilename).failed()) {
+    errs() << "Failed to write output file: " << OutputFilename << "\n";

Likely an incorrect or invalid review comment.

mlir/lib/Compiler/CompilerPipeline.cpp (2)

65-81: LGTM - Clean pass manager configuration.

The addCleanupPasses and configurePassManager methods are well-structured and straightforward.


262-267: LGTM - captureIR implementation is clean.

The redundant flush() call has been removed as addressed from previous review.

mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (2)

31-54: LGTM - getMainFunction with proper null safety.

The function correctly handles the case where op is neither a ModuleOp nor has a ModuleOp ancestor, returning nullptr at line 37.


88-118: LGTM - getOrCreateFunctionDeclaration is well-implemented.

The function properly checks for existing symbols, uses InsertionGuard to preserve insertion point, and includes null safety with a fatal error for missing module.

mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp (2)

249-253: LGTM - Improved error handling.

The error handling now uses llvm::errs() with return failure() instead of report_fatal_error, allowing graceful failure. Consider using op.emitError() for consistency with MLIR diagnostics if desired.


909-960: LGTM - Block structure enforcement is well-documented.

The ensureBlocks method clearly documents the 4-block QIR base profile structure and handles the operation categorization appropriately.

mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h (2)

155-161: LGTM - Proper bounds checking with clear error message.

The operator[] now includes proper bounds checking with both lower and upper bounds validation, and uses llvm::reportFatalUsageError for a clear error message.


56-75: LGTM - Well-documented builder class with clear usage example.

The class documentation clearly explains the linear type enforcement and provides a helpful usage example demonstrating the value semantics pattern.

mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp (1)

121-141: Linear qubit tracking via validateQubitValue / updateQubitTracking looks solid.

The combination of validating inputs, erasing consumed qubits, and inserting new outputs gives you a clear linear-usage discipline for qubit SSA values, and it’s consistently reused by measurement, reset, barriers, and controlled/unitary helpers. This should catch use-after-consume errors early and keep the builder’s state coherent.

mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp (1)

70-78: Stateful conversion pattern for Quartz→Flux (incl. nested ctrl) looks correct and coherent.

LoweringState plus the templated convert* helpers and ConvertQuartzCtrlOp/ConvertQuartzYieldOp form a consistent story:

  • Top-level ops update qubitMap directly; nested ops use targetsIn/targetsOut keyed by inCtrlOp.
  • Zero-target GPhase conversion correctly produces empty targetsOut for nested ctrl, matching Flux’s semantics for controlled global phase (controls only, no targets).
  • ConvertQuartzYieldOp cleans up per-level state and decrements inCtrlOp, preventing cross-talk between nested regions.

Within this excerpt, the Quartz-to-Flux lowering logic for gates, barrier, and ctrl/yield appears sound.

Also applies to: 121-471, 1060-1144

mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp (1)

37-257: Flux→Quartz lowering cleanly captures the value→reference semantic shift.

The templated convert* helpers together with the specific patterns (alloc/static/measure/reset, gate families, barrier, ctrl/yield) and FluxToQuartzTypeConverter form a coherent inverse of Quartz→Flux:

  • OpAdaptors supply already-converted !quartz.qubit operands, so no extra state is needed.
  • Gate conversions replace Flux outputs with the same Quartz references, matching in-place mutation.
  • ConvertFluxMeasureOp and ConvertFluxResetOp correctly collapse Flux’s extra SSA results down to Quartz’s reference-only interface.
  • ConvertFluxCtrlOp and ConvertFluxYieldOp appropriately drop SSA results while preserving region semantics by cloning the body and wiring outputs back to the original qubit references.

Within this excerpt, the Flux-to-Quartz conversion logic looks consistent and correct.

Also applies to: 271-281, 371-459, 761-909

Comment on lines 413 to 430
OwningOpRef<ModuleOp> QuartzProgramBuilder::finalize() {
// Automatically deallocate all remaining allocated qubits
for (const Value qubit : allocatedQubits) {
create<DeallocOp>(loc, qubit);
}

// Clear the tracking set
allocatedQubits.clear();

// Create constant 0 for successful exit code
auto exitCode = create<arith::ConstantOp>(loc, getI64IntegerAttr(0));

// Add return statement with exit code 0 to the main function
create<func::ReturnOp>(loc, ValueRange{exitCode});

// Transfer ownership to the caller
return module;
}
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 8, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider deterministic deallocation order in finalize().

The iteration over allocatedQubits (a DenseSet) has non-deterministic order. While this is functionally correct since all remaining qubits are independent, it may produce different IR output across runs, which can complicate debugging and testing.

Consider collecting the qubits into a sorted container before deallocation if deterministic output is desired:

 OwningOpRef<ModuleOp> QuartzProgramBuilder::finalize() {
-  // Automatically deallocate all remaining allocated qubits
-  for (const Value qubit : allocatedQubits) {
+  // Collect qubits for deterministic deallocation order
+  SmallVector<Value> qubitsToDealloc(allocatedQubits.begin(), 
+                                      allocatedQubits.end());
+  // Sort by some stable criterion if needed (e.g., operation order)
+  for (const Value qubit : qubitsToDealloc) {
     create<DeallocOp>(loc, qubit);
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp around lines
413-430, the loop deallocating qubits iterates over a DenseSet (allocatedQubits)
which yields a non-deterministic order; to produce deterministic IR, copy
allocatedQubits into a SmallVector, sort that vector by a stable key (e.g.,
compare Value::getAsOpaquePointer() or cast to uintptr_t) and then iterate the
sorted vector to create DeallocOp for each qubit, then clear allocatedQubits as
before.

Copy link
Member Author

Choose a reason for hiding this comment

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

@denialhaag what @coderabbitai highlights here might actually be one of the reasons for our program equivalence checks not working too well or non-deterministically before I refactored them to be less strict. Maybe this could be resolved in some other way by establishing a deterministic order.

Copy link
Contributor

Choose a reason for hiding this comment

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

@burgholzer You're absolutely right to highlight this connection! The non-deterministic iteration order over DenseSet could indeed produce different deallocation sequences across runs, which would make IR comparison fragile and potentially explain earlier equivalence check issues.

There are a few ways to establish deterministic order:

  1. Track insertion order (most predictable): Maintain a SmallVector<Value> alongside the DenseSet to preserve allocation order:

    // In header
    llvm::DenseSet<Value> allocatedQubits;
    SmallVector<Value> qubitAllocationOrder;
    
    // In allocQubit/allocQubitRegister
    allocatedQubits.insert(qubit);
    qubitAllocationOrder.push_back(qubit);
    
    // In finalize()
    for (const Value qubit : qubitAllocationOrder) {
      if (allocatedQubits.contains(qubit)) {
        create<DeallocOp>(loc, qubit);
      }
    }
  2. Sort by pointer value (simpler but less semantic):

    SmallVector<Value> qubitsToDealloc(allocatedQubits.begin(), allocatedQubits.end());
    llvm::sort(qubitsToDealloc, [](Value a, Value b) {
      return a.getAsOpaquePointer() < b.getAsOpaquePointer();
    });

The first approach is cleaner and preserves allocation semantics (LIFO/FIFO as desired), while the second is a minimal change but ties output to pointer addresses.

Question: Should we fix this in the current PR, or would you prefer to track it separately? If it was indeed contributing to equivalence check flakiness, it might be worth addressing now. I'm happy to help implement either approach if you'd like.


🧠 Learnings used
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp:80-100
Timestamp: 2025-12-08T23:58:09.648Z
Learning: In the Quartz dialect (mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp), quartz.ctrl uses reference semantics and does not return values, unlike flux.ctrl which uses value semantics and returns transformed qubits. When inlining a GPhaseOp in the CtrlInlineGPhase pattern, it's correct to create POp operations for positive controls and erase the CtrlOp without collecting or replacing result values.

Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp:96-117
Timestamp: 2025-12-08T23:41:55.972Z
Learning: In the QIR (Quantum Intermediate Representation) Builder (mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp), the `ptrCache` is intentionally shared between qubit and result pointer creation (in `staticQubit()` and `measure()` methods) because QIR uses opaque pointers and `inttoptr` conversions for both qubits and results. For any given index N, the LLVM IR pointer representation is identical whether it represents a qubit or a result, so the pointer only needs to be created once and can be safely reused across both contexts.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Operations/StandardGates/BarrierOp.cpp:45-54
Timestamp: 2025-12-09T00:55:11.926Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux), qubits follow single-user/linear-type semantics where each qubit value can only be consumed once, similar to MQTOpt. This invariant makes it safe to dereference getUsers().begin() in canonicalization patterns like MergeSubsequentBarrier in BarrierOp.cpp, as there will be at most one user per qubit output.

Copy link
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: 9

♻️ Duplicate comments (10)
mlir/include/mlir/Dialect/Quartz/Builder/QuartzProgramBuilder.h (2)

125-155: ClassicalRegister should own the register name string or document lifetime requirements.

The ClassicalRegister struct stores StringRef name, which is a non-owning reference. Since ClassicalRegister instances are stored in allocatedClassicalRegisters and allocClassicalBitRegister accepts StringRef, there's a latent risk: if a caller passes a temporary std::string, the stored StringRef will become a dangling reference.

Current usages pass string literals (static storage), but the API permits unsafe usage. Consider either owning the string (e.g., std::string name) or documenting that callers must ensure string lifetime exceeds the builder's lifetime.


168-169: Reference invalidation risk: returned reference may dangle after subsequent allocations.

allocClassicalBitRegister returns ClassicalRegister&, but the backing store is SmallVector<ClassicalRegister> (line 874). If the vector grows during a later allocation, all previously returned references become invalid.

auto& c1 = builder.allocClassicalBitRegister(3, "c1");
auto& c2 = builder.allocClassicalBitRegister(3, "c2"); // may reallocate
builder.measure(q, c1[0]); // c1 potentially dangling!

Consider returning by value instead, or use a stable container (e.g., std::list, std::deque):

-  ClassicalRegister& allocClassicalBitRegister(int64_t size,
-                                               StringRef name = "c");
+  ClassicalRegister allocClassicalBitRegister(int64_t size,
+                                              StringRef name = "c");
mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp (1)

102-109: Reference invalidation implementation concern.

This is the implementation of the API issue flagged in the header. The emplace_back returns a reference to an element in allocatedClassicalRegisters (a SmallVector), which can be invalidated if the vector grows on subsequent calls.

If returning by value (as suggested in the header review), update this to:

-QuartzProgramBuilder::ClassicalRegister&
+QuartzProgramBuilder::ClassicalRegister
 QuartzProgramBuilder::allocClassicalBitRegister(int64_t size, StringRef name) {
   if (size <= 0) {
     llvm::reportFatalUsageError("Size must be positive");
   }
 
-  return allocatedClassicalRegisters.emplace_back(name, size);
+  allocatedClassicalRegisters.emplace_back(name, size);
+  return {name, size};
 }
mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h (2)

132-162: ClassicalRegister stores non-owning StringRef — document lifetime requirements or use owning storage.

The ClassicalRegister struct stores StringRef name, which is a non-owning reference. Since instances are stored in allocatedClassicalRegisters (a SmallVector), callers passing temporary strings (e.g., std::string temporaries) will cause dangling references. Current tests pass string literals (static storage), masking this issue.

Either switch to owning storage or document the requirement clearly:

 struct ClassicalRegister {
-    /// Name of the classical register
-    StringRef name;
+    /// Name of the classical register (must have lifetime >= builder)
+    /// @note Callers must ensure the string outlives the builder instance.
+    ///       Prefer string literals or strings with static storage duration.
+    StringRef name;

Alternatively, use std::string for owned storage if dynamic names are needed.


968-992: Documentation example doesn't match API signature.

The ctrl example shows scalar Value arguments, but the actual signature requires ValueRange:

// Example shows:
{controls_out, targets_out} = builder.ctrl(q0_in, q1_in, [&](auto& b) { ... });

// Actual signature:
std::pair<ValueRange, ValueRange> ctrl(ValueRange controls, ValueRange targets, ...);

Update the example to reflect the actual API:

- * {controls_out, targets_out} = builder.ctrl(q0_in, q1_in, [&](auto& b) {
- *   auto q1_res = b.x(q1_in);
- *   return {q1_res};
- * });
+ * auto [controls_out, targets_out] = builder.ctrl(
+ *     ValueRange{q0_in}, ValueRange{q1_in},
+ *     [&](OpBuilder& b, ValueRange targets) -> ValueRange {
+ *       auto q1_res = b.create<XOp>(loc, targets[0]).getQubitOut();
+ *       return ValueRange{q1_res};
+ *     });
mlir/lib/Compiler/CompilerPipeline.cpp (1)

106-258: currentStage counter still only increments when recordIntermediates is true.

This issue was flagged in a previous review but remains unfixed. The summary message at line 252 uses currentStage to report processed stages, but this counter is only incremented inside config_.recordIntermediates blocks (lines 112, 125, 138, 152, 167, 181, 195, 209, 224, 238). If a user enables printIRAfterAllStages without enabling recordIntermediates, the summary incorrectly reports "Successfully processed 0 stages".

Consider tracking currentStage independently of the recordIntermediates flag:

   // Stage 1: Quartz import
+  if (config_.printIRAfterAllStages) {
+    prettyPrintStage(module, "Quartz Import", ++currentStage, totalStages);
+  }
   if (record != nullptr && config_.recordIntermediates) {
     record->afterQuartzImport = captureIR(module);
-    if (config_.printIRAfterAllStages) {
-      prettyPrintStage(module, "Quartz Import", ++currentStage, totalStages);
-    }
   }

Apply similar changes to all stages to ensure currentStage increments whenever printIRAfterAllStages is enabled, regardless of recordIntermediates.

mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (1)

139-144: Potential duplicate global symbol if called with the same label twice.

This function creates a global without checking if one already exists with the same name. If called multiple times with identical label and symbolPrefix, this will create duplicate globals, potentially causing LLVM verification errors.

Consider using module.lookupSymbol<LLVM::GlobalOp>(symbolName) before creating a new global and reusing the existing one if found.

mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp (1)

96-117: Consider using separate caches for qubit and result pointers.

The ptrCache is shared between staticQubit() (qubit indices) and measure()/allocClassicalBitRegister() (result indices). While QIR uses identical inttoptr semantics for both, sharing the cache could mask bugs where qubit and result pointers are accidentally conflated.

-  /// Cache static pointers for reuse
-  DenseMap<int64_t, Value> ptrCache;
+  /// Cache static qubit pointers for reuse
+  DenseMap<int64_t, Value> qubitPtrCache;
+  /// Cache static result pointers for reuse
+  DenseMap<int64_t, Value> resultPtrCache;
mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp (2)

432-442: Default register name "c" can collide with user-defined registers

Measurements without register metadata are stored under the synthetic register name "c" with index numResults. If the user also has an explicit classical register "c", entries will be merged into the same group in registerResultMap/registerGroups, potentially scrambling array sizes and labels in addOutputRecording.

Consider using a reserved/synthetic name that cannot collide with user code (e.g. "__unnamed__"), and centralize it as a constant.

For example:

-      registerResultMap.insert({{"c", numResults}, resultValue});
+      static constexpr char kDefaultRegisterName[] = "__unnamed__";
+      registerResultMap.insert({{StringRef(kDefaultRegisterName), numResults},
+                                resultValue});

Also applies to: 1057-1112


515-521: Use MLIR diagnostics instead of llvm::errs() for unsupported controlled GPhase

For GPhaseOp inside a ctrl region you currently write to llvm::errs() and return failure(). This again fails the conversion without attaching an error to the offending op.

Emitting an error on op makes it clear to users why the conversion failed and which operation is at fault:

   auto& state = getState();
   if (state.inCtrlOp != 0) {
-    llvm::errs() << "Controlled GPhaseOps cannot be converted to QIR.\n";
-    return failure();
+    op.emitError("controlled global phase is not supported in QIR");
+    return failure();
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5529be and 998242c.

📒 Files selected for processing (18)
  • mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h (1 hunks)
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h (1 hunks)
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h (1 hunks)
  • mlir/include/mlir/Dialect/Quartz/Builder/QuartzProgramBuilder.h (1 hunks)
  • mlir/include/mlir/Dialect/Utils/Utils.h (1 hunks)
  • mlir/lib/Compiler/CompilerPipeline.cpp (1 hunks)
  • mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp (1 hunks)
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp (1 hunks)
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp (1 hunks)
  • mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp (1 hunks)
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp (1 hunks)
  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp (1 hunks)
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp (1 hunks)
  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (1 hunks)
  • mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp (1 hunks)
  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp (1 hunks)
  • mlir/lib/Dialect/Quartz/IR/Operations/StandardGates/U2Op.cpp (1 hunks)
  • mlir/tools/mqt-cc/mqt-cc.cpp (1 hunks)
🧰 Additional context used
🧠 Learnings (17)
📓 Common learnings
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1360
File: .github/workflows/reusable-mlir-tests.yml:40-43
Timestamp: 2025-12-05T17:45:37.602Z
Learning: In the munich-quantum-toolkit/core repository, patch releases of LLVM dependencies don't require documentation updates, changelog entries, or additional tests beyond what's validated by passing CI checks.
📚 Learning: 2025-10-09T13:13:51.224Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:171-180
Timestamp: 2025-10-09T13:13:51.224Z
Learning: In MQT Core MLIR, UnitaryInterface operations guarantee 1-1 correspondence between input and output qubits in the same order. When cloning or modifying unitary operations (e.g., removing controls), this correspondence is maintained by construction, so yielding getAllInQubits() in else-branches matches the result types from the operation's outputs.

Applied to files:

  • mlir/lib/Dialect/Quartz/IR/Operations/StandardGates/U2Op.cpp
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp
  • mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/include/mlir/Dialect/Quartz/Builder/QuartzProgramBuilder.h
  • mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp
  • mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
  • mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp
📚 Learning: 2025-12-08T14:55:43.880Z
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp:78-100
Timestamp: 2025-12-08T14:55:43.880Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp), GPhaseOp is a zero-target operation (global phase). When a CtrlOp wraps a GPhaseOp, it only has control qubits and no targets. The CtrlInlineGPhase canonicalization pattern correctly produces outputs only for the positive controls, not targets.

Applied to files:

  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
  • mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp
📚 Learning: 2025-11-20T18:19:31.857Z
Learnt from: lsschmid
Repo: munich-quantum-toolkit/qmap PR: 832
File: src/hybridmap/HybridNeutralAtomMapper.cpp:1679-1715
Timestamp: 2025-11-20T18:19:31.857Z
Learning: In src/hybridmap/HybridNeutralAtomMapper.cpp, the compareSwapAndBridge and compareShuttlingAndFlyingAncilla methods intentionally use division by zero to produce infinity when distance reductions are zero or negative. This infinity value marks that option as highly unfavorable in cost comparisons, which is the intended behavior rather than a bug.

Applied to files:

  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
📚 Learning: 2025-12-07T09:10:31.820Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1355
File: src/qdmi/sc/Device.cpp:97-102
Timestamp: 2025-12-07T09:10:31.820Z
Learning: In the munich-quantum-toolkit/core repository, duplication of QDMI-related macros (such as IS_INVALID_ARGUMENT) across device implementations (e.g., in src/qdmi/sc/Device.cpp and src/qdmi/dd/Device.cpp) is acceptable as a temporary measure. The preferred long-term solution is to upstream these macros to the QDMI repository rather than creating local shared headers, so they can be reused across all dependent projects.

Applied to files:

  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-10-09T13:20:11.483Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/test/Dialect/MQTOpt/Transforms/lift-measurements.mlir:269-288
Timestamp: 2025-10-09T13:20:11.483Z
Learning: In the MQT MLIR dialect, the `rz` gate should not be included in the `DIAGONAL_GATES` set for the `ReplaceBasisStateControlsWithIfPattern` because its operator matrix does not have the required shape | 1 0 | / | 0 x | for the targets-as-controls optimization. It is only included in `LiftMeasurementsAboveGatesPatterns` where the matrix structure requirement differs.

Applied to files:

  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-12-02T07:37:46.860Z
Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp:144-151
Timestamp: 2025-12-02T07:37:46.860Z
Learning: In MLIR transformation code (mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp and similar), when inserting operations before a target operation, prefer `rewriter.setInsertionPoint(op)` over `rewriter.setInsertionPointAfter(op->getPrevNode())`. The former is cleaner, avoids null pointer edge cases (when op is first in block), and is semantically clearer.

Applied to files:

  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp
  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
  • mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp
📚 Learning: 2025-12-08T12:44:05.874Z
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp:60-70
Timestamp: 2025-12-08T12:44:05.874Z
Learning: In the Quartz dialect (mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp), negative controls are not supported at the current stage. The RemoveTrivialCtrl pattern correctly only checks getNumPosControls() when determining if a CtrlOp should be removed.

Applied to files:

  • mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp
  • mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp
  • mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
  • mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp
📚 Learning: 2025-11-27T08:52:06.428Z
Learnt from: lsschmid
Repo: munich-quantum-toolkit/qmap PR: 832
File: src/hybridmap/HybridNeutralAtomMapper.cpp:1379-1383
Timestamp: 2025-11-27T08:52:06.428Z
Learning: In src/hybridmap/HybridNeutralAtomMapper.cpp, the getBestMovePos function throws std::runtime_error when no move position is found (finalBestPos.coords is empty). This behavior is acceptable for current use cases since tight architectures are not a concern. A future improvement would be to fall back to SWAP-based strategies instead of throwing.

Applied to files:

  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
📚 Learning: 2025-10-09T13:28:29.237Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/DeadGateEliminationPattern.cpp:42-45
Timestamp: 2025-10-09T13:28:29.237Z
Learning: In the MQTOpt MLIR dialect, linear types enforce single-use semantics where each qubit value can only be consumed once, preventing duplicate deallocations and making RAUW operations safe when replacing output qubits with corresponding input qubits in transformation patterns.

Applied to files:

  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h
📚 Learning: 2025-12-04T06:59:40.314Z
Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.cpp:84-85
Timestamp: 2025-12-04T06:59:40.314Z
Learning: In the MQTOpt MLIR routing passes (NaiveRoutingPassSC, AStarRoutingPassSC), the input IR is guaranteed to contain only 1-qubit and 2-qubit gates. All 3+-qubit gates must be decomposed before routing; otherwise the input IR is invalid. This invariant allows skipTwoQubitBlock in LayeredUnit.cpp to safely assert wires.size() == 2.

Applied to files:

  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
📚 Learning: 2025-10-09T13:14:10.178Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:219-221
Timestamp: 2025-10-09T13:14:10.178Z
Learning: The MQT Core project (munich-quantum-toolkit/core repository) uses the C++20 standard, not C++17. C++20 features such as abbreviated function templates (e.g., `const auto&` parameters) are supported and valid in this codebase.

Applied to files:

  • mlir/tools/mqt-cc/mqt-cc.cpp
📚 Learning: 2025-12-05T17:45:37.602Z
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1360
File: .github/workflows/reusable-mlir-tests.yml:40-43
Timestamp: 2025-12-05T17:45:37.602Z
Learning: In the munich-quantum-toolkit/core repository, patch releases of LLVM dependencies don't require documentation updates, changelog entries, or additional tests beyond what's validated by passing CI checks.

Applied to files:

  • mlir/tools/mqt-cc/mqt-cc.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
  • mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp
📚 Learning: 2025-10-09T13:25:36.887Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReuseQubitsPattern.cpp:98-100
Timestamp: 2025-10-09T13:25:36.887Z
Learning: In MLIR code, when traversing parent operations to find a common block between two operations where one uses the result of another, explicit bounds checking is not necessary. MLIR's SSA properties and scope locality guarantees ensure that operations using results must be in compatible scopes and will always share a common ancestor in the operation hierarchy.

Applied to files:

  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp
  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-10-07T15:30:42.946Z
Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1237
File: mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h:219-231
Timestamp: 2025-10-07T15:30:42.946Z
Learning: In the Layout class for MLIR quantum routing (mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Layout.h), the swap method intentionally does NOT swap the hw fields in QubitInfo. This is correct because SSA values represent quantum states at fixed hardware locations, and only their program index associations change during a SWAP gate. The hw field indicates where an SSA value physically resides and remains constant.

Applied to files:

  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
📚 Learning: 2025-10-09T04:30:29.071Z
Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1237
File: mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Scheduler.h:72-140
Timestamp: 2025-10-09T04:30:29.071Z
Learning: In the CrawlScheduler (mlir/include/mlir/Dialect/MQTOpt/Transforms/Transpilation/Scheduler.h), the persistent `layoutCopy` is updated across all lookahead layers via `remapQubitValue` calls. After each two-qubit gate is scheduled in a layer, its input qubits are remapped to output qubits, ensuring the next layer's crawl starts from those outputs. This progressive remapping through SSA values prevents the same operation from being scheduled in multiple lookahead layers.

Applied to files:

  • mlir/include/mlir/Dialect/Flux/FluxUtils.h
📚 Learning: 2025-11-24T10:19:41.147Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1326
File: python/mqt/core/__init__.py:22-22
Timestamp: 2025-11-24T10:19:41.147Z
Learning: In the munich-quantum-toolkit/core repository, Ruff is configured with 'ALL' rules enabled by default, and only specific rules are selectively disabled. When reviewing changes that enable previously-disabled rules (like PLC0415), noqa directives for those rules become necessary and should be retained.

Applied to files:

  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
🧬 Code graph analysis (6)
mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp (2)
mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp (16)
  • op (520-538)
  • op (521-522)
  • op (560-576)
  • op (561-562)
  • op (598-617)
  • op (599-600)
  • op (647-671)
  • op (648-649)
  • op (698-717)
  • op (699-700)
  • op (1014-1039)
  • op (1015-1016)
  • op (1063-1115)
  • op (1064-1065)
  • op (1134-1143)
  • op (1135-1136)
mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (10)
  • getOrCreateFunctionDeclaration (88-118)
  • getOrCreateFunctionDeclaration (88-90)
  • createPointerFromIndex (161-169)
  • createPointerFromIndex (161-162)
  • createResultLabel (120-159)
  • createResultLabel (120-122)
  • getMainFunction (31-54)
  • getMainFunction (31-31)
  • setQIRAttributes (56-86)
  • setQIRAttributes (56-56)
mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp (1)
mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (8)
  • getOrCreateFunctionDeclaration (88-118)
  • getOrCreateFunctionDeclaration (88-90)
  • createPointerFromIndex (161-169)
  • createPointerFromIndex (161-162)
  • createResultLabel (120-159)
  • createResultLabel (120-122)
  • setQIRAttributes (56-86)
  • setQIRAttributes (56-56)
mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp (3)
mlir/include/mlir/Dialect/Quartz/IR/QuartzDialect.h (11)
  • mlir (52-119)
  • getNumTargets (70-70)
  • getNumControls (71-71)
  • getNumPosControls (72-72)
  • getNumNegControls (73-73)
  • getQubit (75-83)
  • getTarget (84-92)
  • getPosControl (94-96)
  • getNegControl (98-100)
  • getNumParams (102-102)
  • getParameter (104-109)
mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp (24)
  • op (37-56)
  • op (37-38)
  • op (65-72)
  • op (65-66)
  • op (81-100)
  • op (81-82)
  • op (109-124)
  • op (109-110)
  • getNumQubits (133-133)
  • getNumQubits (133-133)
  • getNumTargets (135-135)
  • getNumTargets (135-135)
  • getNumControls (137-139)
  • getNumControls (137-137)
  • getNumPosControls (141-141)
  • getNumPosControls (141-141)
  • getNumNegControls (143-145)
  • getNumNegControls (143-143)
  • getNumParams (233-233)
  • getNumParams (233-233)
  • getParameter (235-237)
  • getParameter (235-235)
  • getCanonicalizationPatterns (317-322)
  • getCanonicalizationPatterns (317-318)
mlir/lib/Dialect/Flux/IR/Operations/StandardGates/BarrierOp.cpp (16)
  • getNumQubits (79-79)
  • getNumQubits (79-79)
  • getNumTargets (81-81)
  • getNumTargets (81-81)
  • getNumControls (83-83)
  • getNumControls (83-83)
  • getNumPosControls (85-85)
  • getNumPosControls (85-85)
  • getNumNegControls (87-87)
  • getNumNegControls (87-87)
  • getNumParams (141-141)
  • getNumParams (141-141)
  • getParameter (143-145)
  • getParameter (143-143)
  • getCanonicalizationPatterns (157-160)
  • getCanonicalizationPatterns (157-158)
mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp (5)
mlir/include/mlir/Dialect/Utils/Utils.h (1)
  • mlir (18-42)
mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp (20)
  • op (78-94)
  • op (78-79)
  • getBodyUnitary (99-101)
  • getBodyUnitary (99-99)
  • getNumQubits (103-103)
  • getNumQubits (103-103)
  • getNumTargets (105-105)
  • getNumTargets (105-105)
  • getNumControls (107-109)
  • getNumControls (107-107)
  • getNumPosControls (111-111)
  • getNumPosControls (111-111)
  • getNumNegControls (113-115)
  • getNumNegControls (113-113)
  • build (149-160)
  • build (149-150)
  • build (162-172)
  • build (162-164)
  • getCanonicalizationPatterns (209-212)
  • getCanonicalizationPatterns (209-210)
mlir/lib/Dialect/Flux/IR/Operations/StandardGates/BarrierOp.cpp (24)
  • op (35-74)
  • op (35-36)
  • getNumQubits (79-79)
  • getNumQubits (79-79)
  • getNumTargets (81-81)
  • getNumTargets (81-81)
  • getNumControls (83-83)
  • getNumControls (83-83)
  • getNumPosControls (85-85)
  • getNumPosControls (85-85)
  • getNumNegControls (87-87)
  • getNumNegControls (87-87)
  • getInputQubit (89-89)
  • getInputQubit (89-89)
  • getOutputQubit (91-91)
  • getOutputQubit (91-91)
  • getInputTarget (93-98)
  • getInputTarget (93-93)
  • getOutputTarget (100-105)
  • getOutputTarget (100-100)
  • build (147-155)
  • build (147-148)
  • getCanonicalizationPatterns (157-160)
  • getCanonicalizationPatterns (157-158)
mlir/lib/Dialect/Flux/IR/Operations/StandardGates/POp.cpp (4)
  • build (55-59)
  • build (55-56)
  • getCanonicalizationPatterns (61-64)
  • getCanonicalizationPatterns (61-62)
mlir/lib/Dialect/Flux/IR/Operations/StandardGates/GPhaseOp.cpp (4)
  • build (54-58)
  • build (54-55)
  • getCanonicalizationPatterns (60-63)
  • getCanonicalizationPatterns (60-61)
mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp (1)
mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h (1)
  • index (144-160)
mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h (1)
mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (10)
  • getMainFunction (31-54)
  • getMainFunction (31-31)
  • setQIRAttributes (56-86)
  • setQIRAttributes (56-56)
  • getOrCreateFunctionDeclaration (88-118)
  • getOrCreateFunctionDeclaration (88-90)
  • createResultLabel (120-159)
  • createResultLabel (120-122)
  • createPointerFromIndex (161-169)
  • createPointerFromIndex (161-162)
🪛 ast-grep (0.40.0)
mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp

[warning] 172-172: The getTargetsIn function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsIn()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 179-179: The getTargetsOut function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsOut()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 211-211: The getTargetsOut function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsOut()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 212-212: The getTargetsIn function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsIn()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 225-225: The getTargetsIn function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsIn()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 226-226: The getTargetsOut function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsOut()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🐍 Test (macos-15-intel) / 🐍 macos-15-intel
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (64)
mlir/lib/Dialect/Quartz/IR/Operations/StandardGates/U2Op.cpp (1)

22-28: LGTM!

The build overload correctly converts variant parameters to Values using the variantToValue utility and delegates to the existing build method. This follows the established pattern for parameterized gate operations in the dialect.

mlir/include/mlir/Dialect/Utils/Utils.h (1)

11-42: LGTM!

The utility header is now self-contained with all necessary includes. The variantToValue function provides a clean abstraction for handling mixed literal/dynamic parameters across gate operations, and TOLERANCE is appropriately defined for floating-point comparisons.

mlir/lib/Dialect/Flux/IR/Operations/StandardGates/XXPlusYYOp.cpp (2)

27-71: LGTM!

The MergeSubsequentXXPlusYY canonicalization pattern correctly:

  • Validates qubit identity across consecutive operations
  • Uses tolerance-based comparison for static beta values (addressing earlier review feedback)
  • Falls back to SSA value equality for dynamic beta values (line 56)
  • Properly accumulates theta parameters and trivializes the predecessor

75-87: LGTM!

The build overload and canonicalization registration follow the established patterns for parameterized gate operations in the Flux dialect.

mlir/include/mlir/Dialect/Quartz/Builder/QuartzProgramBuilder.h (2)

49-68: LGTM on the builder class structure and initialization API.

The QuartzProgramBuilder design provides a clean, type-safe interface for constructing quantum programs with automatic resource tracking.


235-290: Well-structured macro-generated API for zero-target and one-target gates.

The macros effectively reduce boilerplate while maintaining comprehensive documentation for each gate variant (base, controlled, multi-controlled).

mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp (4)

31-53: LGTM on initialization flow.

The builder correctly initializes the MLIR context, loads the Quartz dialect, creates a main function with the entry_point passthrough attribute, and sets up the entry block for operation insertion.


55-100: LGTM on qubit allocation methods.

The allocQubit, staticQubit, and allocQubitRegister methods properly validate inputs and track allocations in allocatedQubits for automatic deallocation during finalization.


394-407: LGTM on deallocation with double-deallocation protection.

The dealloc method correctly uses erase to both check existence and remove from tracking in a single operation, providing clear error messaging for invalid deallocations.


413-430: LGTM on finalization logic.

The finalize method properly auto-deallocates remaining qubits, clears tracking state, creates exit code 0, and transfers module ownership to the caller.

mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h (3)

1-23: LGTM!

Copyright header and includes are appropriate. The necessary MLIR and LLVM headers are included for the builder functionality.


26-55: Well-documented class with clear usage example.

The class-level documentation clearly explains linear type enforcement, the no-cloning theorem mapping, and provides a concise usage example showing the initialize → operations → finalize lifecycle.


1035-1066: LGTM on private state tracking.

The combination of validQubits DenseSet and allocatedClassicalRegisters SmallVector provides appropriate tracking for the linear type enforcement documented in the class.

mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp (9)

35-57: Constructor and initialization look correct.

The constructor creates an unknown location and ModuleOp (builtin dialect, no issue). The initialize() method loads the Flux dialect before creating Flux operations. The lifecycle is properly sequenced.


69-82: Good input validation with fatal error reporting.

The staticQubit method properly validates the non-negative index precondition and uses reportFatalUsageError for clear diagnostics.


84-106: LGTM on register allocation.

Size validation is present, and the loop correctly creates individual AllocOps with register metadata attached.


108-115: Reference to emplaced element is safe but fragile.

Returning a reference to emplace_back result is safe here since subsequent allocations won't invalidate prior references (SmallVector preserves validity for existing elements). However, if the vector reallocates during a future emplace_back, only new references would be invalidated, not existing ones stored by callers.


121-141: Clean linear type tracking implementation.

The validateQubitValue and updateQubitTracking helpers correctly implement the linear type semantics: validate-before-consume and atomic swap of tracking state.


147-179: Measurement and reset correctly update tracking.

Both measure overloads and reset properly call updateQubitTracking to swap the consumed input for the produced output, maintaining linear type invariants.


187-245: Gate macro implementations are well-structured.

The macros correctly delegate controlled variants to ctrl() and handle qubit tracking updates. The pattern of creating the op, extracting results, and updating tracking is consistent across all variants.


527-534: Barrier correctly tracks all qubits via zip iteration.

The llvm::zip ensures 1-1 correspondence between input and output qubits when updating tracking, consistent with the UnitaryInterface guarantees mentioned in the learnings.


540-556: ctrl implementation handles tracking for both controls and targets.

The implementation correctly updates tracking for all control and target qubits after creating the CtrlOp. The llvm::zip iterations maintain the 1-1 correspondence.

mlir/lib/Conversion/FluxToQuartz/FluxToQuartz.cpp (1)

1-911: LGTM! Well-structured conversion pass implementation.

The Flux-to-Quartz conversion pass is well-organized with clear separation of concerns:

  • Template helpers (lines 37-257) provide reusable conversion logic for different operation signatures
  • Type converter (lines 271-282) handles the semantic shift from value to reference types
  • Conversion patterns (lines 284-822) systematically cover all operations with consistent macro-driven code generation
  • Pass implementation (lines 824-909) correctly configures the conversion target and legality constraints

The documentation is thorough, explaining the value-to-reference semantic transformation and providing concrete examples. Previous review concerns have been addressed.

mlir/tools/mqt-cc/mqt-cc.cpp (1)

121-144: Variable name mismatches prevent compilation.

The code references non-existent identifiers. The cl::opt variables declared at lines 40-63 use camelCase names (e.g., InputFilename, OutputFilename, ConvertToQIR), but lines 121-144 reference SCREAMING_SNAKE_CASE names (e.g., INPUT_FILENAME, OUTPUT_FILENAME, CONVERT_TO_QIR) that don't exist.

Apply this diff to fix the identifier references:

   // Load the input .mlir file
-  const auto module = loadMLIRFile(INPUT_FILENAME, &context);
+  const auto module = loadMLIRFile(InputFilename, &context);
   if (!module) {
-    errs() << "Failed to load input file: " << INPUT_FILENAME << "\n";
+    errs() << "Failed to load input file: " << InputFilename << "\n";
     return 1;
   }

   // Configure the compiler pipeline
   QuantumCompilerConfig config;
-  config.convertToQIR = CONVERT_TO_QIR;
-  config.enableTiming = ENABLE_TIMING;
-  config.enableStatistics = ENABLE_STATISTICS;
-  config.printIRAfterAllStages = PRINT_IR_AFTER_ALL_STAGES;
+  config.convertToQIR = ConvertToQIR;
+  config.enableTiming = EnableTiming;
+  config.enableStatistics = EnableStatistics;
+  config.printIRAfterAllStages = PrintIRAfterAllStages;

   // Run the compilation pipeline
   if (const QuantumCompilerPipeline pipeline(config);
       pipeline.runPipeline(module.get()).failed()) {
     errs() << "Compilation pipeline failed\n";
     return 1;
   }

   // Write the output
-  if (writeOutput(module.get(), OUTPUT_FILENAME).failed()) {
-    errs() << "Failed to write output file: " << OUTPUT_FILENAME << "\n";
+  if (writeOutput(module.get(), OutputFilename).failed()) {
+    errs() << "Failed to write output file: " << OutputFilename << "\n";
     return 1;
   }

Likely an incorrect or invalid review comment.

mlir/include/mlir/Dialect/Flux/FluxUtils.h (8)

1-17: Header setup looks good—all previous issues resolved!

The include guard is now in place with #pragma once, and the file correctly includes necessary dependencies without global using namespace statements.


19-43: LGTM!

The inverse-pair removal logic is correct. Both operations are properly trivialized by replacing them with their respective inputs.


45-75: LGTM!

The two-target inverse-pair removal correctly verifies that both qubits are connected between the operations before removing them.


77-108: LGTM!

The merge logic correctly creates a square operation and removes the predecessor. As discussed in previous reviews, the current implementation is functionally correct—the rewriter properly updates the use-def chain when prevOp is replaced.


110-140: LGTM!

The parameter merging logic is correct, and the comment at line 131 helpfully documents the operand indexing convention.


142-178: LGTM!

The two-target merge correctly validates that both operations act on the same qubits before merging their parameters. The operand indexing comment at line 168 is helpful.


180-205: Excellent—floating-point comparison issue resolved!

The tolerance-based check using std::abs(paramValue) > utils::TOLERANCE correctly handles near-zero parameters that may result from floating-point arithmetic, addressing the previously flagged fragile equality comparison.


207-232: Excellent—floating-point comparison issue resolved here too!

Consistent with the one-target version, this function now uses tolerance-based comparison for robust zero detection.

mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp (12)

1-39: LGTM!

License header, includes, and namespace setup are appropriate. The using namespace directives are scoped within the mlir namespace, which is acceptable practice.


40-78: LGTM!

The LoweringState struct is well-documented with clear explanation of the reference-to-value semantics transformation. The state fields appropriately track qubit mappings and nested control operation context.


485-496: LGTM!

The type converter correctly handles the primary conversion from !quartz.qubit to !flux.qubit while passing through other types unchanged via identity conversion.


516-539: LGTM!

The alloc conversion correctly establishes the initial qubit mapping and preserves register metadata. Using try_emplace is appropriate since each allocation creates a new SSA value.


556-577: LGTM!

The dealloc conversion correctly looks up the latest qubit value and removes the mapping. The implementation assumes well-formed input where the qubit was previously allocated, which is reasonable.


594-618: LGTM!

The static qubit conversion correctly creates the flux.static operation and establishes the mapping. Note the slight inconsistency: AllocOp uses try_emplace while StaticOp uses direct assignment. Both are functionally equivalent for valid SSA input.


643-672: LGTM!

Excellent handling of the semantic difference between dialects. The conversion correctly extracts the classical bit result for replacement while updating the qubit mapping with the post-measurement qubit value.


720-993: LGTM!

The macro-generated converters effectively reduce boilerplate while maintaining consistent patterns. Documentation is inline, and macros are properly undefined after use.


1010-1040: Missing nested ctrl handling for barrier operations.

Unlike the single/two-target converters, ConvertQuartzBarrierOp doesn't check state.inCtrlOp and always uses qubitMap directly. If a barrier can appear inside a quartz.ctrl region, the qubits should be looked up from targetsIn instead.

If barriers inside ctrl regions are intentionally unsupported, consider adding a check:

   LogicalResult
   matchAndRewrite(quartz::BarrierOp op, OpAdaptor /*adaptor*/,
                   ConversionPatternRewriter& rewriter) const override {
     auto& state = getState();
+    // Barriers inside ctrl regions are not yet supported
+    if (state.inCtrlOp != 0) {
+      return op.emitError("barrier inside ctrl region not supported");
+    }
     auto& qubitMap = state.qubitMap;

Please verify whether barriers can appear inside ctrl regions in the Quartz dialect.


1060-1116: LGTM!

The CtrlOp conversion correctly handles nested control operations through the inCtrlOp counter and targetsIn/targetsOut maps. The sequence of incrementing the nesting counter before storing targets ensures correct level association. Region cloning preserves the body operations for subsequent conversion.


1130-1144: LGTM!

The YieldOp conversion correctly retrieves the converted target values from targetsOut, creates the flux yield, and cleans up state by erasing the map entry before decrementing the nesting counter.


1169-1234: LGTM!

The pass implementation is well-structured:

  • Stack-allocated LoweringState is safe since patterns execute synchronously
  • Conversion target correctly marks Quartz illegal and Flux legal
  • All operation patterns are registered with state tracking
  • Standard MLIR function and control-flow type conversion patterns are included
  • Using applyPartialConversion allows non-Quartz operations to remain unchanged
mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (4)

31-54: LGTM!

The function correctly handles the null module case with an early return and properly searches for the entry_point attribute.


56-86: LGTM!

The QIR attribute construction correctly implements the base profile metadata requirements with proper version (2.0), resource requirements, and management flags.


88-118: LGTM!

The function correctly manages the symbol lookup, has proper null safety for the module, and appropriately marks irreversible operations.


161-169: LGTM!

The utility correctly creates an LLVM pointer from an integer index using the standard constant + inttoptr pattern.

mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h (5)

22-29: LGTM!

The use of inline constexpr for QIR function name constants ensures single definition across translation units.


31-70: LGTM!

The macro effectively generates QIR function name constants for all standard gates and their controlled variants.


74-127: LGTM!

The getter functions correctly map control counts to QIR function names. The reportFatalUsageError call in the default case properly handles unsupported control counts (the function is marked [[noreturn]]).


137-146: LGTM!

The metadata struct is well-designed with appropriate defaults and clear documentation.


148-235: LGTM!

The function declarations are well-documented with accurate details about their behavior, including the correct QIR version (2.0) in setQIRAttributes documentation.

mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp (8)

37-94: LGTM!

The initialization correctly sets up the 4-block QIR Base Profile structure with proper block wiring, entry_point attribute, and initialization call placement.


135-163: LGTM!

The classical register allocation correctly manages the measurements block insertion and tracks register-to-result mappings.


165-229: LGTM!

The measure implementations correctly handle the measurements block insertion and result tracking. The runtime error for missing register slots (line 216) properly guards against invalid usage.


231-247: LGTM!

The reset operation correctly targets the measurements block and uses proper function declaration handling.


253-318: LGTM!

The function correctly manages insertion points for constants (entry block) and calls (body block), with proper argument ordering (controls, targets, parameters) matching QIR conventions.


320-543: LGTM!

The macro-generated gate operations effectively reduce boilerplate while correctly handling control variants through createCallOp and the appropriate getFnName* selectors.


549-618: LGTM!

The output recording correctly groups measurements by register, sorts for deterministic output, and generates properly labeled QIR output calls.


620-627: LGTM!

The finalization correctly generates output recording before setting QIR attributes and returning module ownership.

mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp (1)

99-207: CtrlOp helpers and verification look consistent and robust.

getBodyUnitary, qubit/control/parameter accessors, builders, and verify() (region shape, uniqueness checks, Barrier disallowance) are coherent and align with the intended CtrlOp semantics in Quartz. No changes needed here from my side.

mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp (1)

129-315: CtrlOp accessors, builders, and verification are well-structured.

The qubit/control/target accessors with bounds checks, parameter forwarding, builders that clone or build the body and yield targets, and verify() (body region shape, yield arity, uniqueness, Barrier restriction) form a consistent and safe API for Flux CtrlOp. The GPhase/Id inlining patterns also align with the documented zero-target GPhase semantics.

mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp (1)

870-1261: Overall Quartz→QIR lowering structure looks solid

The staged conversion (func→LLVM, Quartz→QIR, then arith/cf→LLVM plus final cast reconciliation) combined with LoweringState for metadata and output recording is well-structured and aligns with QIR base-profile requirements (static qubits/results, labeled outputs). No functional issues spotted in the high-level pipeline.

Comment on lines +160 to +165
Value fluxQubit;
if (inCtrlOp == 0) {
fluxQubit = qubitMap[quartzQubit];
} else {
fluxQubit = state.targetsIn[inCtrlOp].front();
}
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 8, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding assertions for defensive debugging.

The code assumes that when inCtrlOp != 0, the targetsIn vector has the expected number of elements. While this should hold for well-formed IR, assertions would help catch issues during development:

   Value fluxQubit;
   if (inCtrlOp == 0) {
     fluxQubit = qubitMap[quartzQubit];
   } else {
+    assert(!state.targetsIn[inCtrlOp].empty() &&
+           "Expected target qubit for one-target operation");
     fluxQubit = state.targetsIn[inCtrlOp].front();
   }

This pattern applies to all similar .front() calls in the conversion helpers.

🤖 Prompt for AI Agents
In mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp around lines 160-165, the
branch that uses state.targetsIn[inCtrlOp].front() assumes targetsIn has the
expected elements; add a defensive assertion before calling .front() to verify
the container is non-empty and that inCtrlOp is a valid index (e.g.,
assert(inCtrlOp >= 0 && static_cast<size_t>(inCtrlOp) < state.targetsIn.size()
&& !state.targetsIn[inCtrlOp].empty()) or use LLVM/MLIR assertion/reporting
APIs), and include a clear message indicating which quartzQubit/inCtrlOp failed
to help debugging; apply the same pattern to all other .front() uses in the
conversion helpers.

Copy link
Member Author

Choose a reason for hiding this comment

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

@denialhaag I suppose the assertion proposed by @coderabbitai doesn't really hurt us here (and in other places)

Copy link
Contributor

Choose a reason for hiding this comment

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

@burgholzer You're right—adding these assertions would be helpful for catching malformed IR during development. For MLIR code, you'd typically use assert() from <cassert> (which is already included) rather than LLVM's reporting APIs, since these are internal invariants.

The suggested assertion would look like:

if (inCtrlOp != 0) {
  assert(!state.targetsIn[inCtrlOp].empty() &&
         "Expected target qubit for one-target operation");
  fluxQubit = state.targetsIn[inCtrlOp].front();
}

This pattern should be applied consistently to similar .front() accesses in the other conversion helper templates (convertOneTargetOneParameter, convertTwoTargetZeroParameter, etc.) where state.targetsIn[inCtrlOp] is accessed. The assertions will be active in debug builds and help catch issues early if the IR structure doesn't match expectations.


🧠 Learnings used
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp:80-100
Timestamp: 2025-12-08T23:58:09.648Z
Learning: In the Quartz dialect (mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp), quartz.ctrl uses reference semantics and does not return values, unlike flux.ctrl which uses value semantics and returns transformed qubits. When inlining a GPhaseOp in the CtrlInlineGPhase pattern, it's correct to create POp operations for positive controls and erase the CtrlOp without collecting or replacing result values.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Operations/StandardGates/BarrierOp.cpp:45-54
Timestamp: 2025-12-09T00:55:11.926Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux), qubits follow single-user/linear-type semantics where each qubit value can only be consumed once, similar to MQTOpt. This invariant makes it safe to dereference getUsers().begin() in canonicalization patterns like MergeSubsequentBarrier in BarrierOp.cpp, as there will be at most one user per qubit output.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp:78-100
Timestamp: 2025-12-08T14:55:43.899Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp), GPhaseOp is a zero-target operation (global phase). When a CtrlOp wraps a GPhaseOp, it only has control qubits and no targets. The CtrlInlineGPhase canonicalization pattern correctly produces outputs only for the positive controls, not targets.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp:60-70
Timestamp: 2025-12-08T12:44:05.883Z
Learning: In the Quartz dialect (mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp), negative controls are not supported at the current stage. The RemoveTrivialCtrl pattern correctly only checks getNumPosControls() when determining if a CtrlOp should be removed.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp:78-101
Timestamp: 2025-12-08T23:16:20.680Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp), negative controls are not supported at the current stage. The CtrlInlineGPhase canonicalization pattern correctly only checks getNumPosControls() and processes only positive controls when inlining a GPhaseOp.

Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.cpp:84-85
Timestamp: 2025-12-04T06:59:40.314Z
Learning: In the MQTOpt MLIR routing passes (NaiveRoutingPassSC, AStarRoutingPassSC), the input IR is guaranteed to contain only 1-qubit and 2-qubit gates. All 3+-qubit gates must be decomposed before routing; otherwise the input IR is invalid. This invariant allows skipTwoQubitBlock in LayeredUnit.cpp to safely assert wires.size() == 2.

Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:171-180
Timestamp: 2025-10-09T13:13:51.224Z
Learning: In MQT Core MLIR, UnitaryInterface operations guarantee 1-1 correspondence between input and output qubits in the same order. When cloning or modifying unitary operations (e.g., removing controls), this correspondence is maintained by construction, so yielding getAllInQubits() in else-branches matches the result types from the operation's outputs.

Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/test/Dialect/MQTOpt/Transforms/lift-measurements.mlir:269-288
Timestamp: 2025-10-09T13:20:11.483Z
Learning: In the MQT MLIR dialect, the `rz` gate should not be included in the `DIAGONAL_GATES` set for the `ReplaceBasisStateControlsWithIfPattern` because its operator matrix does not have the required shape | 1 0 | / | 0 x | for the targets-as-controls optimization. It is only included in `LiftMeasurementsAboveGatesPatterns` where the matrix structure requirement differs.

Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp:144-151
Timestamp: 2025-12-02T07:37:46.860Z
Learning: In MLIR transformation code (mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp and similar), when inserting operations before a target operation, prefer `rewriter.setInsertionPoint(op)` over `rewriter.setInsertionPointAfter(op->getPrevNode())`. The former is cleaner, avoids null pointer edge cases (when op is first in block), and is semantically clearer.

Learnt from: lsschmid
Repo: munich-quantum-toolkit/qmap PR: 832
File: src/hybridmap/HybridNeutralAtomMapper.cpp:1379-1383
Timestamp: 2025-11-27T08:52:06.428Z
Learning: In src/hybridmap/HybridNeutralAtomMapper.cpp, the getBestMovePos function throws std::runtime_error when no move position is found (finalBestPos.coords is empty). This behavior is acceptable for current use cases since tight architectures are not a concern. A future improvement would be to fall back to SWAP-based strategies instead of throwing.

Comment on lines +910 to +960
// Return if there are already multiple blocks
if (main.getBlocks().size() > 1) {
return;
}

// Get the existing block
auto* bodyBlock = &main.front();
OpBuilder builder(main.getBody());

// Create the required blocks
auto* entryBlock = builder.createBlock(&main.getBody());
// Move the entry block before the body block
main.getBlocks().splice(Region::iterator(bodyBlock), main.getBlocks(),
entryBlock);
Block* measurementsBlock = builder.createBlock(&main.getBody());
Block* outputBlock = builder.createBlock(&main.getBody());

auto& bodyBlockOps = bodyBlock->getOperations();
auto& outputBlockOps = outputBlock->getOperations();
auto& measurementsBlockOps = measurementsBlock->getOperations();

// Move operations to appropriate blocks
for (auto it = bodyBlock->begin(); it != bodyBlock->end();) {
// Ensure iterator remains valid after potential move
if (auto& op = *it++;
isa<DeallocOp>(op) || isa<ResetOp>(op) || isa<MeasureOp>(op)) {
// Move irreversible quantum operations to measurements block
measurementsBlockOps.splice(measurementsBlock->end(), bodyBlockOps,
Block::iterator(op));
} else if (isa<LLVM::ReturnOp>(op)) {
// Move return to output block
outputBlockOps.splice(outputBlock->end(), bodyBlockOps,
Block::iterator(op));
} else if (op.hasTrait<OpTrait::ConstantLike>()) {
// Move constant like operations to the entry block
entryBlock->getOperations().splice(entryBlock->end(), bodyBlockOps,
Block::iterator(op));
}
// All other operations (gates, etc.) stay in body block
}

// Add unconditional jumps between blocks
builder.setInsertionPointToEnd(entryBlock);
builder.create<LLVM::BrOp>(main->getLoc(), bodyBlock);

builder.setInsertionPointToEnd(bodyBlock);
builder.create<LLVM::BrOp>(main->getLoc(), measurementsBlock);

builder.setInsertionPointToEnd(measurementsBlock);
builder.create<LLVM::BrOp>(main->getLoc(), outputBlock);
}
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 8, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Clarify assumptions about block layout for addOutputRecording

ensureBlocks only restructures functions that start with a single block, but addOutputRecording assumes the last block of main is the output block. If this pass is ever run on a module where main already has multiple blocks with a different layout, output recording could be inserted into an unexpected block.

If the pass is intended to run only on single-block mains produced by your pipeline, consider documenting that precondition on the pass, or enforcing/normalizing the 4-block layout unconditionally before calling addOutputRecording.

Also applies to: 1041-1056

Copy link
Member Author

Choose a reason for hiding this comment

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

@denialhaag, I think we should follow the advice by @coderabbitai here and at least document the pre-condition. Hopefully, this is something that we can lift rather soon when we improve our QIR support

Copy link
Contributor

Choose a reason for hiding this comment

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

@burgholzer Good call on documenting this pre-condition! Here are some suggestions for where to add this:

  1. In the pass description comment (line ~900 in QuartzToQIR.cpp):

    /**
     * @brief Pass for converting Quartz dialect operations to QIR
     *
     * @details
     * This pass converts Quartz dialect quantum operations to QIR (Quantum
     * Intermediate Representation) by lowering them to LLVM dialect operations
     * that call QIR runtime functions.
     *
     * @pre The entry function must have a single block. The pass will
     *      restructure it into a 4-block layout (entry → body → measurements 
     *      → output). Multi-block functions are currently unsupported.
     * ...
     */
  2. In the TableGen pass definition (QuartzToQIR.td):
    Add a note in the description about the expected input structure.

  3. In ensureBlocks and addOutputRecording function docs:
    Explicitly state the assumptions about block structure.

This will help future maintainers understand the constraint when enhancing QIR support. Would you like me to generate a specific documentation snippet you can use?


🧠 Learnings used
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp:80-100
Timestamp: 2025-12-08T23:58:09.648Z
Learning: In the Quartz dialect (mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp), quartz.ctrl uses reference semantics and does not return values, unlike flux.ctrl which uses value semantics and returns transformed qubits. When inlining a GPhaseOp in the CtrlInlineGPhase pattern, it's correct to create POp operations for positive controls and erase the CtrlOp without collecting or replacing result values.

Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.cpp:84-85
Timestamp: 2025-12-04T06:59:40.314Z
Learning: In the MQTOpt MLIR routing passes (NaiveRoutingPassSC, AStarRoutingPassSC), the input IR is guaranteed to contain only 1-qubit and 2-qubit gates. All 3+-qubit gates must be decomposed before routing; otherwise the input IR is invalid. This invariant allows skipTwoQubitBlock in LayeredUnit.cpp to safely assert wires.size() == 2.

Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:171-180
Timestamp: 2025-10-09T13:13:51.224Z
Learning: In MQT Core MLIR, UnitaryInterface operations guarantee 1-1 correspondence between input and output qubits in the same order. When cloning or modifying unitary operations (e.g., removing controls), this correspondence is maintained by construction, so yielding getAllInQubits() in else-branches matches the result types from the operation's outputs.

Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp:96-117
Timestamp: 2025-12-08T23:41:55.972Z
Learning: In the QIR (Quantum Intermediate Representation) Builder (mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp), the `ptrCache` is intentionally shared between qubit and result pointer creation (in `staticQubit()` and `measure()` methods) because QIR uses opaque pointers and `inttoptr` conversions for both qubits and results. For any given index N, the LLVM IR pointer representation is identical whether it represents a qubit or a result, so the pointer only needs to be created once and can be safely reused across both contexts.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Operations/StandardGates/BarrierOp.cpp:45-54
Timestamp: 2025-12-09T00:55:11.926Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux), qubits follow single-user/linear-type semantics where each qubit value can only be consumed once, similar to MQTOpt. This invariant makes it safe to dereference getUsers().begin() in canonicalization patterns like MergeSubsequentBarrier in BarrierOp.cpp, as there will be at most one user per qubit output.

Comment on lines +575 to +590
OwningOpRef<ModuleOp> FluxProgramBuilder::finalize() {
// Automatically deallocate all remaining valid qubits
for (const auto qubit : validQubits) {
create<DeallocOp>(loc, qubit);
}

validQubits.clear();

// Create constant 0 for successful exit code
auto exitCode = create<arith::ConstantOp>(loc, getI64IntegerAttr(0));

// Add return statement with exit code 0 to the main function
create<func::ReturnOp>(loc, ValueRange{exitCode});

return module;
}
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 8, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Non-deterministic deallocation order from DenseSet iteration.

validQubits is a DenseSet, and iterating over it produces a non-deterministic order. This means the generated DeallocOp sequence in the finalized IR can vary between runs, potentially affecting:

  • IR diff comparisons
  • Reproducible test outputs
  • Debug sessions

Consider using a deterministic container (e.g., SetVector) or sorting qubits by some stable key before deallocation:

+  // Sort qubits for deterministic deallocation order
+  SmallVector<Value> sortedQubits(validQubits.begin(), validQubits.end());
+  llvm::sort(sortedQubits, [](Value a, Value b) {
+    // Sort by operation order in the IR for reproducibility
+    return a.getDefiningOp()->isBeforeInBlock(b.getDefiningOp());
+  });
+
   // Automatically deallocate all remaining valid qubits
-  for (const auto qubit : validQubits) {
+  for (const auto qubit : sortedQubits) {
     create<DeallocOp>(loc, qubit);
   }

Alternatively, if order doesn't matter semantically, document that deallocation order is undefined.

📝 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
OwningOpRef<ModuleOp> FluxProgramBuilder::finalize() {
// Automatically deallocate all remaining valid qubits
for (const auto qubit : validQubits) {
create<DeallocOp>(loc, qubit);
}
validQubits.clear();
// Create constant 0 for successful exit code
auto exitCode = create<arith::ConstantOp>(loc, getI64IntegerAttr(0));
// Add return statement with exit code 0 to the main function
create<func::ReturnOp>(loc, ValueRange{exitCode});
return module;
}
OwningOpRef<ModuleOp> FluxProgramBuilder::finalize() {
// Sort qubits for deterministic deallocation order
SmallVector<Value> sortedQubits(validQubits.begin(), validQubits.end());
llvm::sort(sortedQubits, [](Value a, Value b) {
// Sort by operation order in the IR for reproducibility
return a.getDefiningOp()->isBeforeInBlock(b.getDefiningOp());
});
// Automatically deallocate all remaining valid qubits
for (const auto qubit : sortedQubits) {
create<DeallocOp>(loc, qubit);
}
validQubits.clear();
// Create constant 0 for successful exit code
auto exitCode = create<arith::ConstantOp>(loc, getI64IntegerAttr(0));
// Add return statement with exit code 0 to the main function
create<func::ReturnOp>(loc, ValueRange{exitCode});
return module;
}

Copy link
Member

Choose a reason for hiding this comment

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

For reference, this is related to #1264 (comment).

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the specific review comment thread from the PR
gh api repos/munich-quantum-toolkit/core/pulls/1264/comments \
  --jq '.[] | select(.id == 2599709667) | {id, body, user: .user.login, path, line, created_at}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Try fetching the comment directly using the review comments API
gh api repos/munich-quantum-toolkit/core/pulls/1264/comments/2599709667 \
  --jq '{id, body, user: .user.login, path, line, created_at}'

Length of output: 130


🏁 Script executed:

#!/bin/bash
# Also try to get recent comments on this file to understand the context
gh api repos/munich-quantum-toolkit/core/pulls/1264/comments \
  --jq '.[] | select(.path == "mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp") | {id, body, user: .user.login, line, created_at}' | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for comments mentioning "deterministic" or "DenseSet" in the PR
gh api graphql -f query='
{
  repository(owner: "munich-quantum-toolkit", name: "core") {
    pullRequest(number: 1264) {
      reviews(last: 50) {
        nodes {
          comments(last: 100) {
            nodes {
              id
              body
              path
              line
              author {
                login
              }
            }
          }
        }
      }
    }
  }
}' --jq '.data.repository.pullRequest.reviews.nodes[].comments.nodes[] | select(.body | test("deterministic|DenseSet"; "i")) | {id, author: .author.login, path, line, body_preview: .body[0:200]}'

Length of output: 706


🏁 Script executed:

#!/bin/bash
# Try to get all comments and search for the specific ID pattern
gh pr view 1264 --repo munich-quantum-toolkit/core --json comments \
  --jq '.comments[] | select(.body | contains("2599709667") or contains("deterministic") or contains("DenseSet")) | {author: .author.login, body}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get the full comment from QuartzProgramBuilder.cpp to understand the related discussion
gh api graphql -f query='
{
  repository(owner: "munich-quantum-toolkit", name: "core") {
    pullRequest(number: 1264) {
      reviews(last: 50) {
        nodes {
          comments(last: 100) {
            nodes {
              id
              databaseId
              body
              path
              line
              author {
                login
              }
            }
          }
        }
      }
    }
  }
}' --jq '.data.repository.pullRequest.reviews.nodes[].comments.nodes[] | select(.path == "mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp" and (.body | test("deterministic|DenseSet"; "i"))) | {id, databaseId, author: .author.login, path, line, body}'

Length of output: 2003


@denialhaag Thank you for the reference! You're absolutely right - this is the exact same pattern in both builders:

  • QuartzProgramBuilder iterates over allocatedQubits (DenseSet)
  • FluxProgramBuilder iterates over validQubits (DenseSet)

Both exhibit non-deterministic deallocation order in their finalize() methods.

Since this was noted as a nitpick in the Quartz builder case, would you prefer to:

  1. Keep as-is - Accept that deallocation order is undefined (since qubits are independent, it's semantically correct)
  2. Apply consistently - If you'd like deterministic IR output for testing/debugging, the same sorting approach could be applied to both builders
  3. Document the behavior - Add a comment noting that deallocation order is unspecified

The functional correctness isn't affected either way, so it's primarily a question of whether deterministic IR output is valuable for your use cases. Let me know your preference! 😊


🧠 Learnings used
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp:78-100
Timestamp: 2025-12-08T14:55:43.880Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp), GPhaseOp is a zero-target operation (global phase). When a CtrlOp wraps a GPhaseOp, it only has control qubits and no targets. The CtrlInlineGPhase canonicalization pattern correctly produces outputs only for the positive controls, not targets.

Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:171-180
Timestamp: 2025-10-09T13:13:51.224Z
Learning: In MQT Core MLIR, UnitaryInterface operations guarantee 1-1 correspondence between input and output qubits in the same order. When cloning or modifying unitary operations (e.g., removing controls), this correspondence is maintained by construction, so yielding getAllInQubits() in else-branches matches the result types from the operation's outputs.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp:78-101
Timestamp: 2025-12-08T23:16:16.276Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp), negative controls are not supported at the current stage. The CtrlInlineGPhase canonicalization pattern correctly only checks getNumPosControls() and processes only positive controls when inlining a GPhaseOp.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1360
File: .github/workflows/reusable-mlir-tests.yml:40-43
Timestamp: 2025-12-05T17:45:37.602Z
Learning: In the munich-quantum-toolkit/core repository, patch releases of LLVM dependencies don't require documentation updates, changelog entries, or additional tests beyond what's validated by passing CI checks.

Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:219-221
Timestamp: 2025-10-09T13:14:10.178Z
Learning: The MQT Core project (munich-quantum-toolkit/core repository) uses the C++20 standard, not C++17. C++20 features such as abbreviated function templates (e.g., `const auto&` parameters) are supported and valid in this codebase.

Learnt from: flowerthrower
Repo: munich-quantum-toolkit/core-plugins-catalyst PR: 1
File: CHANGELOG.md:18-18
Timestamp: 2025-12-01T11:00:40.342Z
Learning: In the munich-quantum-toolkit/core-plugins-catalyst repository, the CHANGELOG.md intentionally references the parent MQT Core repository's release notes (https://github.com/munich-quantum-toolkit/core/releases) because the plugin repository is based on work performed in the parent repository.

Learnt from: marcelwa
Repo: munich-quantum-toolkit/core PR: 1243
File: test/python/qdmi/qiskit/test_qdmi_qiskit_backend.py:0-0
Timestamp: 2025-11-04T14:28:32.371Z
Learning: In the munich-quantum-toolkit/core repository, at least one FoMaC device is always available during testing, so skip logic for missing devices in QDMI Qiskit backend tests is not necessary.

Copy link
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: 6

♻️ Duplicate comments (2)
mlir/tools/mqt-cc/mqt-cc.cpp (1)

40-63: The const qualifier on cl::opt declarations conflicts with LLVM's CommandLine library requirements.

All six command-line option variables are declared const, but LLVM's ParseCommandLineOptions (line 105) requires mutable storage to write parsed values. This was flagged in a previous review and marked as addressed, but the const qualifier is still present in the current code.

Remove const from all option declarations:

-const cl::opt<std::string> INPUT_FILENAME(cl::Positional,
+cl::opt<std::string> INPUT_FILENAME(cl::Positional,
                                           cl::desc("<input .mlir file>"),
                                           cl::init("-"));

-const cl::opt<std::string> OUTPUT_FILENAME("o", cl::desc("Output filename"),
+cl::opt<std::string> OUTPUT_FILENAME("o", cl::desc("Output filename"),
                                            cl::value_desc("filename"),
                                            cl::init("-"));

-const cl::opt<bool> CONVERT_TO_QIR("emit-qir",
+cl::opt<bool> CONVERT_TO_QIR("emit-qir",
                                    cl::desc("Convert to QIR at the end"),
                                    cl::init(false));

-const cl::opt<bool> ENABLE_TIMING("mlir-timing",
+cl::opt<bool> ENABLE_TIMING("mlir-timing",
                                   cl::desc("Enable pass timing statistics"),
                                   cl::init(false));

-const cl::opt<bool> ENABLE_STATISTICS("mlir-statistics",
+cl::opt<bool> ENABLE_STATISTICS("mlir-statistics",
                                       cl::desc("Enable pass statistics"),
                                       cl::init(false));

-const cl::opt<bool>
+cl::opt<bool>
     PRINT_IR_AFTER_ALL_STAGES("mlir-print-ir-after-all-stages",
                               cl::desc("Print IR after each compiler stage"),
                               cl::init(false));
mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (1)

120-161: Guard against duplicate globals when creating result labels.

createResultLabel always creates a new LLVM::GlobalOp for the same symbolPrefix + "_" + label without checking if a global with that name already exists. If two call sites request the same label (e.g., identical register/label names from different builders or passes), this will produce duplicate global symbols and can cause LLVM IR verification failures.

Consider reusing an existing global when present:

-  const auto globalOp = builder.create<LLVM::GlobalOp>(
-      op->getLoc(), llvmArrayType, /*isConstant=*/true, LLVM::Linkage::Internal,
-      symbolName, stringInitializer);
-  globalOp->setAttr("addr_space", builder.getI32IntegerAttr(0));
-  globalOp->setAttr("dso_local", builder.getUnitAttr());
+  if (auto existingGlobal =
+          module.lookupSymbol<LLVM::GlobalOp>(symbolName)) {
+    // Reuse existing global
+    (void)existingGlobal;
+  } else {
+    const auto globalOp = builder.create<LLVM::GlobalOp>(
+        op->getLoc(), llvmArrayType, /*isConstant=*/true,
+        LLVM::Linkage::Internal, symbolName, stringInitializer);
+    globalOp->setAttr("addr_space", builder.getI32IntegerAttr(0));
+    globalOp->setAttr("dso_local", builder.getUnitAttr());
+  }

The subsequent AddressOfOp can then always be created as it is now.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 998242c and fc1c4a4.

📒 Files selected for processing (9)
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h (1 hunks)
  • mlir/include/mlir/Dialect/Utils/Utils.h (1 hunks)
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp (1 hunks)
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp (1 hunks)
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp (1 hunks)
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp (1 hunks)
  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (1 hunks)
  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp (1 hunks)
  • mlir/tools/mqt-cc/mqt-cc.cpp (1 hunks)
🧰 Additional context used
🧠 Learnings (17)
📓 Common learnings
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1360
File: .github/workflows/reusable-mlir-tests.yml:40-43
Timestamp: 2025-12-05T17:45:37.602Z
Learning: In the munich-quantum-toolkit/core repository, patch releases of LLVM dependencies don't require documentation updates, changelog entries, or additional tests beyond what's validated by passing CI checks.
Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.cpp:84-85
Timestamp: 2025-12-04T06:59:40.314Z
Learning: In the MQTOpt MLIR routing passes (NaiveRoutingPassSC, AStarRoutingPassSC), the input IR is guaranteed to contain only 1-qubit and 2-qubit gates. All 3+-qubit gates must be decomposed before routing; otherwise the input IR is invalid. This invariant allows skipTwoQubitBlock in LayeredUnit.cpp to safely assert wires.size() == 2.
📚 Learning: 2025-10-09T13:14:10.178Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:219-221
Timestamp: 2025-10-09T13:14:10.178Z
Learning: The MQT Core project (munich-quantum-toolkit/core repository) uses the C++20 standard, not C++17. C++20 features such as abbreviated function templates (e.g., `const auto&` parameters) are supported and valid in this codebase.

Applied to files:

  • mlir/tools/mqt-cc/mqt-cc.cpp
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
📚 Learning: 2025-12-05T17:45:37.602Z
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1360
File: .github/workflows/reusable-mlir-tests.yml:40-43
Timestamp: 2025-12-05T17:45:37.602Z
Learning: In the munich-quantum-toolkit/core repository, patch releases of LLVM dependencies don't require documentation updates, changelog entries, or additional tests beyond what's validated by passing CI checks.

Applied to files:

  • mlir/tools/mqt-cc/mqt-cc.cpp
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-12-08T12:44:05.874Z
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp:60-70
Timestamp: 2025-12-08T12:44:05.874Z
Learning: In the Quartz dialect (mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp), negative controls are not supported at the current stage. The RemoveTrivialCtrl pattern correctly only checks getNumPosControls() when determining if a CtrlOp should be removed.

Applied to files:

  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-12-08T14:55:43.880Z
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp:78-100
Timestamp: 2025-12-08T14:55:43.880Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp), GPhaseOp is a zero-target operation (global phase). When a CtrlOp wraps a GPhaseOp, it only has control qubits and no targets. The CtrlInlineGPhase canonicalization pattern correctly produces outputs only for the positive controls, not targets.

Applied to files:

  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
📚 Learning: 2025-12-08T23:16:16.276Z
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp:78-101
Timestamp: 2025-12-08T23:16:16.276Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp), negative controls are not supported at the current stage. The CtrlInlineGPhase canonicalization pattern correctly only checks getNumPosControls() and processes only positive controls when inlining a GPhaseOp.

Applied to files:

  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-10-09T13:13:51.224Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:171-180
Timestamp: 2025-10-09T13:13:51.224Z
Learning: In MQT Core MLIR, UnitaryInterface operations guarantee 1-1 correspondence between input and output qubits in the same order. When cloning or modifying unitary operations (e.g., removing controls), this correspondence is maintained by construction, so yielding getAllInQubits() in else-branches matches the result types from the operation's outputs.

Applied to files:

  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-10-09T13:20:11.483Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/test/Dialect/MQTOpt/Transforms/lift-measurements.mlir:269-288
Timestamp: 2025-10-09T13:20:11.483Z
Learning: In the MQT MLIR dialect, the `rz` gate should not be included in the `DIAGONAL_GATES` set for the `ReplaceBasisStateControlsWithIfPattern` because its operator matrix does not have the required shape | 1 0 | / | 0 x | for the targets-as-controls optimization. It is only included in `LiftMeasurementsAboveGatesPatterns` where the matrix structure requirement differs.

Applied to files:

  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-12-02T07:37:46.860Z
Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp:144-151
Timestamp: 2025-12-02T07:37:46.860Z
Learning: In MLIR transformation code (mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp and similar), when inserting operations before a target operation, prefer `rewriter.setInsertionPoint(op)` over `rewriter.setInsertionPointAfter(op->getPrevNode())`. The former is cleaner, avoids null pointer edge cases (when op is first in block), and is semantically clearer.

Applied to files:

  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
📚 Learning: 2025-11-24T10:19:41.147Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1326
File: python/mqt/core/__init__.py:22-22
Timestamp: 2025-11-24T10:19:41.147Z
Learning: In the munich-quantum-toolkit/core repository, Ruff is configured with 'ALL' rules enabled by default, and only specific rules are selectively disabled. When reviewing changes that enable previously-disabled rules (like PLC0415), noqa directives for those rules become necessary and should be retained.

Applied to files:

  • mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
📚 Learning: 2025-11-27T08:52:06.428Z
Learnt from: lsschmid
Repo: munich-quantum-toolkit/qmap PR: 832
File: src/hybridmap/HybridNeutralAtomMapper.cpp:1379-1383
Timestamp: 2025-11-27T08:52:06.428Z
Learning: In src/hybridmap/HybridNeutralAtomMapper.cpp, the getBestMovePos function throws std::runtime_error when no move position is found (finalBestPos.coords is empty). This behavior is acceptable for current use cases since tight architectures are not a concern. A future improvement would be to fall back to SWAP-based strategies instead of throwing.

Applied to files:

  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
📚 Learning: 2025-10-09T13:28:29.237Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/DeadGateEliminationPattern.cpp:42-45
Timestamp: 2025-10-09T13:28:29.237Z
Learning: In the MQTOpt MLIR dialect, linear types enforce single-use semantics where each qubit value can only be consumed once, preventing duplicate deallocations and making RAUW operations safe when replacing output qubits with corresponding input qubits in transformation patterns.

Applied to files:

  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
📚 Learning: 2025-12-04T06:59:40.314Z
Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.cpp:84-85
Timestamp: 2025-12-04T06:59:40.314Z
Learning: In the MQTOpt MLIR routing passes (NaiveRoutingPassSC, AStarRoutingPassSC), the input IR is guaranteed to contain only 1-qubit and 2-qubit gates. All 3+-qubit gates must be decomposed before routing; otherwise the input IR is invalid. This invariant allows skipTwoQubitBlock in LayeredUnit.cpp to safely assert wires.size() == 2.

Applied to files:

  • mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp
  • mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp
📚 Learning: 2025-10-09T13:25:36.887Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReuseQubitsPattern.cpp:98-100
Timestamp: 2025-10-09T13:25:36.887Z
Learning: In MLIR code, when traversing parent operations to find a common block between two operations where one uses the result of another, explicit bounds checking is not necessary. MLIR's SSA properties and scope locality guarantees ensure that operations using results must be in compatible scopes and will always share a common ancestor in the operation hierarchy.

Applied to files:

  • mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp
  • mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
📚 Learning: 2025-12-01T11:00:40.342Z
Learnt from: flowerthrower
Repo: munich-quantum-toolkit/core-plugins-catalyst PR: 1
File: CHANGELOG.md:18-18
Timestamp: 2025-12-01T11:00:40.342Z
Learning: In the munich-quantum-toolkit/core-plugins-catalyst repository, the CHANGELOG.md intentionally references the parent MQT Core repository's release notes (https://github.com/munich-quantum-toolkit/core/releases) because the plugin repository is based on work performed in the parent repository.

Applied to files:

  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
📚 Learning: 2025-11-04T14:28:32.371Z
Learnt from: marcelwa
Repo: munich-quantum-toolkit/core PR: 1243
File: test/python/qdmi/qiskit/test_qdmi_qiskit_backend.py:0-0
Timestamp: 2025-11-04T14:28:32.371Z
Learning: In the munich-quantum-toolkit/core repository, at least one FoMaC device is always available during testing, so skip logic for missing devices in QDMI Qiskit backend tests is not necessary.

Applied to files:

  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
📚 Learning: 2025-12-07T09:10:31.820Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1355
File: src/qdmi/sc/Device.cpp:97-102
Timestamp: 2025-12-07T09:10:31.820Z
Learning: In the munich-quantum-toolkit/core repository, duplication of QDMI-related macros (such as IS_INVALID_ARGUMENT) across device implementations (e.g., in src/qdmi/sc/Device.cpp and src/qdmi/dd/Device.cpp) is acceptable as a temporary measure. The preferred long-term solution is to upstream these macros to the QDMI repository rather than creating local shared headers, so they can be reused across all dependent projects.

Applied to files:

  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
  • mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h
🧬 Code graph analysis (8)
mlir/tools/mqt-cc/mqt-cc.cpp (1)
mlir/include/mlir/Compiler/CompilerPipeline.h (2)
  • mlir (17-145)
  • config (90-90)
mlir/include/mlir/Dialect/Utils/Utils.h (1)
mlir/include/mlir/Dialect/Flux/FluxUtils.h (1)
  • mlir (17-205)
mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp (2)
mlir/include/mlir/Dialect/Quartz/IR/QuartzDialect.h (11)
  • mlir (52-119)
  • getNumTargets (70-70)
  • getNumControls (71-71)
  • getNumPosControls (72-72)
  • getNumNegControls (73-73)
  • getQubit (75-83)
  • getTarget (84-92)
  • getPosControl (94-96)
  • getNegControl (98-100)
  • getNumParams (102-102)
  • getParameter (104-109)
mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp (26)
  • op (37-55)
  • op (37-38)
  • op (64-77)
  • op (64-65)
  • op (86-105)
  • op (86-87)
  • op (114-129)
  • op (114-115)
  • getNumQubits (138-138)
  • getNumQubits (138-138)
  • getNumTargets (140-140)
  • getNumTargets (140-140)
  • getNumControls (142-144)
  • getNumControls (142-142)
  • getNumPosControls (146-146)
  • getNumPosControls (146-146)
  • getNumNegControls (148-150)
  • getNumNegControls (148-148)
  • getNumParams (238-238)
  • getNumParams (238-238)
  • getParameter (240-242)
  • getParameter (240-240)
  • build (244-255)
  • build (244-246)
  • build (257-269)
  • build (257-260)
mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp (1)
mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (10)
  • getOrCreateFunctionDeclaration (88-118)
  • getOrCreateFunctionDeclaration (88-90)
  • createPointerFromIndex (164-172)
  • createPointerFromIndex (164-165)
  • createResultLabel (120-162)
  • createResultLabel (120-122)
  • getMainFunction (31-54)
  • getMainFunction (31-31)
  • setQIRAttributes (56-86)
  • setQIRAttributes (56-56)
mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (1)
mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp (22)
  • op (229-287)
  • op (230-231)
  • op (310-315)
  • op (311-312)
  • op (339-362)
  • op (340-341)
  • op (393-454)
  • op (394-395)
  • op (476-491)
  • op (477-478)
  • op (511-520)
  • op (512-513)
  • op (820-825)
  • op (821-822)
  • op (834-849)
  • op (835-836)
  • main (907-958)
  • main (907-907)
  • main (972-1001)
  • main (972-972)
  • main (1039-1111)
  • main (1039-1040)
mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp (2)
mlir/include/mlir/Dialect/Utils/Utils.h (1)
  • mlir (19-43)
mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp (14)
  • op (36-53)
  • op (36-37)
  • op (61-74)
  • op (61-62)
  • op (83-99)
  • op (83-84)
  • getBodyUnitary (104-106)
  • getBodyUnitary (104-104)
  • getNumPosControls (116-116)
  • getNumPosControls (116-116)
  • build (154-165)
  • build (154-155)
  • build (167-178)
  • build (167-169)
mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp (3)
mlir/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h (3)
  • QIRProgramBuilder (63-136)
  • index (141-157)
  • registerIndex (135-135)
mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (6)
  • getOrCreateFunctionDeclaration (88-118)
  • getOrCreateFunctionDeclaration (88-90)
  • createPointerFromIndex (164-172)
  • createPointerFromIndex (164-165)
  • createResultLabel (120-162)
  • createResultLabel (120-122)
mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h (1)
  • numResults (142-142)
mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h (1)
mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (10)
  • getMainFunction (31-54)
  • getMainFunction (31-31)
  • setQIRAttributes (56-86)
  • setQIRAttributes (56-56)
  • getOrCreateFunctionDeclaration (88-118)
  • getOrCreateFunctionDeclaration (88-90)
  • createResultLabel (120-162)
  • createResultLabel (120-122)
  • createPointerFromIndex (164-172)
  • createPointerFromIndex (164-165)
🪛 ast-grep (0.40.0)
mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp

[warning] 177-177: The getTargetsIn function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsIn()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 184-184: The getTargetsOut function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsOut()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 216-216: The getTargetsOut function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsOut()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 217-217: The getTargetsIn function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsIn()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 230-230: The getTargetsIn function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsIn()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 231-231: The getTargetsOut function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsOut()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)

🪛 Cppcheck (2.18.0)
mlir/tools/mqt-cc/mqt-cc.cpp

[information] 11-11: Include file

(missingInclude)


[information] 12-12: Include file

(missingInclude)


[information] 13-13: Include file

(missingInclude)


[information] 15-15: Include file

(missingIncludeSystem)


[information] 16-16: Include file

(missingIncludeSystem)


[information] 17-17: Include file

(missingIncludeSystem)


[information] 18-18: Include file

(missingIncludeSystem)


[information] 19-19: Include file

(missingIncludeSystem)


[information] 20-20: Include file

(missingIncludeSystem)


[information] 21-21: Include file

(missingIncludeSystem)


[information] 22-22: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)


[information] 26-26: Include file

(missingIncludeSystem)


[information] 27-27: Include file

(missingIncludeSystem)


[information] 28-28: Include file

(missingIncludeSystem)


[information] 29-29: Include file

(missingIncludeSystem)


[information] 30-30: Include file

(missingIncludeSystem)


[information] 31-31: Include file

(missingIncludeSystem)


[information] 32-32: Include file

(missingIncludeSystem)

mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp

[information] 11-11: Include file

(missingInclude)


[information] 13-13: Include file

(missingIncludeSystem)


[information] 14-14: Include file

(missingIncludeSystem)


[information] 15-15: Include file

(missingIncludeSystem)


[information] 16-16: Include file

(missingIncludeSystem)


[information] 17-17: Include file

(missingIncludeSystem)


[information] 18-18: Include file

(missingIncludeSystem)


[information] 19-19: Include file

(missingIncludeSystem)


[information] 20-20: Include file

(missingIncludeSystem)


[information] 21-21: Include file

(missingIncludeSystem)


[information] 22-22: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)

mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp

[information] 11-11: Include file

(missingInclude)


[information] 13-13: Include file

(missingInclude)


[information] 14-14: Include file

(missingInclude)


[information] 16-16: Include file

(missingIncludeSystem)


[information] 17-17: Include file

(missingIncludeSystem)


[information] 18-18: Include file

(missingIncludeSystem)


[information] 19-19: Include file

(missingIncludeSystem)


[information] 20-20: Include file

(missingIncludeSystem)


[information] 21-21: Include file

(missingIncludeSystem)


[information] 22-22: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)


[information] 26-26: Include file

(missingIncludeSystem)


[information] 27-27: Include file

(missingIncludeSystem)


[information] 28-28: Include file

(missingIncludeSystem)


[information] 29-29: Include file

(missingIncludeSystem)


[information] 30-30: Include file

(missingIncludeSystem)


[information] 31-31: Include file

(missingIncludeSystem)


[information] 32-32: Include file

(missingIncludeSystem)


[information] 33-33: Include file

(missingIncludeSystem)


[information] 34-34: Include file

(missingIncludeSystem)


[information] 35-35: Include file

(missingIncludeSystem)


[information] 36-36: Include file

(missingIncludeSystem)


[information] 37-37: Include file

(missingIncludeSystem)


[information] 38-38: Include file

(missingIncludeSystem)


[information] 39-39: Include file

(missingIncludeSystem)


[information] 40-40: Include file

(missingIncludeSystem)


[information] 41-41: Include file

(missingIncludeSystem)


[information] 42-42: Include file

(missingIncludeSystem)


[information] 43-43: Include file

(missingIncludeSystem)


[information] 44-44: Include file

(missingIncludeSystem)


[information] 45-45: Include file

(missingIncludeSystem)


[information] 46-46: Include file

(missingIncludeSystem)


[information] 47-47: Include file

(missingIncludeSystem)


[information] 48-48: Include file

(missingIncludeSystem)


[information] 49-49: Include file

(missingIncludeSystem)


[information] 50-50: Include file

(missingIncludeSystem)


[information] 58-58: Include file

(missingInclude)


[information] Limiting analysis of branches. Use --check-level=exhaustive to analyze all branches.

(normalCheckLevelMaxBranches)

mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp

[information] 11-11: Include file

(missingInclude)


[information] 13-13: Include file

(missingIncludeSystem)


[information] 14-14: Include file

(missingIncludeSystem)


[information] 15-15: Include file

(missingIncludeSystem)


[information] 16-16: Include file

(missingIncludeSystem)


[information] 17-17: Include file

(missingIncludeSystem)


[information] 18-18: Include file

(missingIncludeSystem)


[information] 19-19: Include file

(missingIncludeSystem)


[information] 20-20: Include file

(missingIncludeSystem)


[information] 21-21: Include file

(missingIncludeSystem)


[information] 22-22: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)


[information] 26-26: Include file

(missingIncludeSystem)


[information] 27-27: Include file

(missingIncludeSystem)


[style] 56-56: The function 'setQIRAttributes' is never used.

(unusedFunction)


[style] 88-88: The function 'getOrCreateFunctionDeclaration' is never used.

(unusedFunction)


[style] 120-120: The function 'createResultLabel' is never used.

(unusedFunction)


[style] 164-164: The function 'createPointerFromIndex' is never used.

(unusedFunction)

mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp

[information] 11-11: Include file

(missingInclude)


[information] 13-13: Include file

(missingIncludeSystem)


[information] 14-14: Include file

(missingIncludeSystem)


[information] 15-15: Include file

(missingIncludeSystem)


[information] 16-16: Include file

(missingIncludeSystem)


[information] 17-17: Include file

(missingIncludeSystem)


[information] 18-18: Include file

(missingIncludeSystem)


[information] 19-19: Include file

(missingIncludeSystem)


[information] 20-20: Include file

(missingIncludeSystem)


[information] 21-21: Include file

(missingIncludeSystem)


[information] 22-22: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)

mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp

[information] 11-11: Include file

(missingInclude)


[information] 13-13: Include file

(missingInclude)


[information] 15-15: Include file

(missingIncludeSystem)


[information] 16-16: Include file

(missingIncludeSystem)


[information] 17-17: Include file

(missingIncludeSystem)


[information] 18-18: Include file

(missingIncludeSystem)


[information] 19-19: Include file

(missingIncludeSystem)


[information] 20-20: Include file

(missingIncludeSystem)


[information] 21-21: Include file

(missingIncludeSystem)


[information] 22-22: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)


[information] 26-26: Include file

(missingIncludeSystem)


[information] 27-27: Include file

(missingIncludeSystem)


[information] 28-28: Include file

(missingIncludeSystem)


[information] 29-29: Include file

(missingIncludeSystem)


[information] 30-30: Include file

(missingIncludeSystem)


[information] 31-31: Include file

(missingIncludeSystem)


[information] 32-32: Include file

(missingIncludeSystem)


[information] 33-33: Include file

(missingIncludeSystem)

mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp

[information] 11-11: Include file

(missingInclude)


[information] 13-13: Include file

(missingInclude)


[information] 14-14: Include file

(missingInclude)


[information] 16-16: Include file

(missingIncludeSystem)


[information] 17-17: Include file

(missingIncludeSystem)


[information] 18-18: Include file

(missingIncludeSystem)


[information] 19-19: Include file

(missingIncludeSystem)


[information] 20-20: Include file

(missingIncludeSystem)


[information] 21-21: Include file

(missingIncludeSystem)


[information] 22-22: Include file

(missingIncludeSystem)


[information] 23-23: Include file

(missingIncludeSystem)


[information] 24-24: Include file

(missingIncludeSystem)


[information] 25-25: Include file

(missingIncludeSystem)


[information] 26-26: Include file

(missingIncludeSystem)


[information] 27-27: Include file

(missingIncludeSystem)


[information] 28-28: Include file

(missingIncludeSystem)


[information] 29-29: Include file

(missingIncludeSystem)


[information] 30-30: Include file

(missingIncludeSystem)


[information] 31-31: Include file

(missingIncludeSystem)


[information] 38-38: Include file

(missingInclude)

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: 🐍 Test (macos-15-intel) / 🐍 macos-15-intel
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🐍 Test (macos-14) / 🐍 macos-14
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🇨‌ Lint / 🚨 Lint
🔇 Additional comments (23)
mlir/include/mlir/Dialect/Utils/Utils.h (2)

11-21: LGTM: Includes and TOLERANCE constant are correct.

The missing includes flagged in previous reviews have been properly addressed. The TOLERANCE value of 1e-12 is appropriate for floating-point near-zero checks in quantum gate optimization patterns.


23-41: LGTM: variantToValue implementation is correct and well-documented.

The function correctly handles both alternatives of the variant parameter by creating an arith::ConstantOp for doubles or forwarding precomputed Values. This provides a clean abstraction for gate builders that accept parameters as either literals or SSA Values.

mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp (7)

34-56: LGTM: MergeNestedCtrl correctly merges control qubits.

The pattern properly combines controls from both the outer and inner CtrlOps. The previous issue with explicit eraseOp(bodyCtrlOp) has been resolved—replaceOpWithNewOp now handles cleanup automatically.


61-78: LGTM: RemoveTrivialCtrl correctly unwraps control-less operations.

The pattern properly clones the body operation before replacing the CtrlOp, avoiding the use-after-free issue flagged in earlier reviews. The check for only positive controls is correct given that negative controls are not yet supported. Based on learnings.


83-106: LGTM: CtrlInlineGPhase correctly inlines controlled global phase.

The pattern correctly handles the GPhaseOp case by creating POps for each positive control. Since GPhaseOp is a zero-target operation (global phase), the CtrlOp wrapping it only has control qubits and no targets, so the replacement correctly produces outputs only for the positive controls. Based on learnings.


111-130: LGTM: CtrlInlineId correctly unwraps controlled identity.

The pattern properly creates a standalone IdOp and includes both all controls and the target output in the replacement operands. The reservation size correctly accounts for all positive controls plus the single target.


134-208: LGTM: Accessor implementations are correct and consistent.

All accessor methods now include proper bounds checking with descriptive error messages via reportFatalUsageError. The delegation of negative control accessors to the body unitary is appropriate. The target accessor bounds checking issue flagged in previous reviews has been resolved.


210-269: LGTM: Mapping accessors and builders are correctly implemented.

The input/output mapping methods properly iterate through all controls and targets. Both build overloads correctly construct the body region with a unitary operation and YieldOp terminator, using InsertionGuard to avoid side effects on the caller's insertion point.


271-327: LGTM: Verification and canonicalization registration are comprehensive.

The verify method thoroughly validates the CtrlOp structure:

  • Body region has exactly 2 operations (unitary + yield terminator)
  • No duplicate qubits in inputs or outputs
  • Semantic constraint that BarrierOp cannot be controlled

The canonicalization pattern registration correctly includes all four patterns defined in this file.

mlir/lib/Conversion/QuartzToFlux/QuartzToFlux.cpp (5)

1-39: Well-structured file with clean organization.

The copyright headers, includes, and namespace setup follow project conventions. The include ordering (project headers first, then LLVM/MLIR system headers) is correct.

Note: The static analysis hints about missing includes are false positives—the headers exist but aren't visible to the standalone analyzer.


40-108: Clean state management design and base pattern class.

The LoweringState struct correctly captures the essential state for tracking the reference-to-value semantic conversion:

  • qubitMap for top-level qubit tracking
  • inCtrlOp counter with targetsIn/targetsOut maps for nested control operations

The StatefulOpConversionPattern template properly addresses the earlier review feedback—the inherited base constructors (using statement) were removed, ensuring all instances are constructed with a valid state_ pointer.


1168-1233: Pass implementation is comprehensive and follows MLIR best practices.

The pass correctly:

  • Creates LoweringState on the stack (safe since applyPartialConversion runs synchronously)
  • Configures the conversion target with Quartz illegal and Flux legal
  • Registers all conversion patterns with the shared state
  • Handles function signatures, returns, calls, and branch operations
  • Signals pass failure appropriately on conversion errors

719-992: Macro-generated patterns are clean and consistent.

The use of macros to generate boilerplate conversion patterns is well-executed:

  • Each macro properly delegates to the corresponding template helper
  • Macros are #undef'd immediately after use to prevent pollution
  • Documentation in macro comments correctly describes the transformations

This significantly reduces code duplication while maintaining readability.


1009-1039: ConvertQuartzBarrierOp pattern is correct; quartz.barrier cannot appear inside quartz.ctrl by design.

BarrierOp explicitly rejects control operands with fatal errors in getPosControl() and getNegControl() methods, and the operation definition has no region support. Unlike controllable gates, barriers are fundamentally non-controllable operations and cannot legally appear within a quartz.ctrl region. The pattern correctly ignores state.inCtrlOp because that context is impossible for this operation.

Likely an incorrect or invalid review comment.

mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp (6)

34-54: LGTM! Past use-after-free issue resolved.

The MergeNestedCtrl pattern correctly merges nested controls by calling replaceOpWithNewOp without an explicit eraseOp, which resolves the previously flagged use-after-free issue.


59-75: LGTM! Correctly handles trivial control removal.

The pattern appropriately checks only getNumPosControls() since negative controls are not supported in the Quartz dialect at this stage. When no positive controls exist, the body operation is correctly cloned to replace the CtrlOp.

Based on learnings, negative controls are not supported in Quartz.


104-120: LGTM! Accessor methods correctly implemented.

The accessor methods properly compute qubit counts and delegate to the body unitary where appropriate. The delegation pattern for getNumNegControls() exposes negative controls from the nested body operation, which is consistent with the overall design even though negative controls are not directly supported at the CtrlOp level.


122-152: LGTM! Qubit and parameter accessors correctly implemented.

The accessor methods properly handle indexing with appropriate bounds checking. The getQubit() method correctly maps indices to positive controls first, then delegates to body qubits. All delegation patterns to the body unitary are appropriate.


154-178: LGTM! Builder methods follow MLIR best practices.

Both build() overloads correctly construct the operation with proper insertion guards, region setup, body population (via cloning or callback), and terminator creation.


180-218: LGTM! Verification and pattern registration correctly implemented.

The verify() method comprehensively validates the operation structure (body region layout, qubit uniqueness, and semantic constraints like forbidding controlled barriers). The getCanonicalizationPatterns() method appropriately registers all three canonicalization patterns.

mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp (1)

56-86: QIR metadata attribute assembly looks consistent and complete.

The passthrough payload correctly encodes entry_point, labeled output schema, base_profile, resource counts, and QIR 2.0 version plus dynamic management flags; this matches the documented QIRMetadata contract and what QuartzToQIR/QIRProgramBuilder expect.

mlir/lib/Conversion/QuartzToQIR/QuartzToQIR.cpp (1)

130-182: Centralized unitary lowering with convertUnitaryToCallOp looks solid.

Using LoweringState for control tracking and building the QIR call signature from (controls, targets, params) keeps all gate conversions consistent, and the ctrl cleanup here is safe given the CtrlOp verifier guarantees a single controlled unitary in the region.

mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp (1)

42-94: Main function and block layout initialization aligns with QIR Base Profile expectations.

The 4-block setup (entry/body/measurements/output), early __quantum__rt__initialize call, and unconditional branches plus final return form a clean base on which later builders and output-recording logic operate; the use of insertion guards keeps user code insertion points predictable.

Comment on lines +191 to +203
* For QIR functions that are irreversible (measurement, reset, deallocation),
* the "irreversible" attribute is added automatically.
*
* @param builder The builder to use for creating operations
* @param op The operation requesting the function (for context)
* @param fnName The name of the QIR function
* @param fnType The LLVM function type signature
* @return The LLVM function declaration
*/
LLVM::LLVMFuncOp getOrCreateFunctionDeclaration(OpBuilder& builder,
Operation* op, StringRef fnName,
Type fnType);

Copy link
Contributor

@coderabbitai coderabbitai bot Dec 8, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align header documentation with the current implementation.

Two docstrings are slightly out of sync with QIRUtils.cpp:

  • getOrCreateFunctionDeclaration: the comment says irreversible “measurement, reset, deallocation” are annotated, but the implementation currently only special-cases QIR_MEASURE and QIR_RESET. Either extend the implementation or drop “deallocation” from the doc.
  • createResultLabel: the comment claims the AddressOfOp is inserted at the builder’s current insertion point, but the implementation always inserts it at the start of the main function’s entry block (restoring the caller’s insertion via InsertionGuard). Updating the wording to describe “module-level global + address-of in main’s entry block” would avoid confusion.
🤖 Prompt for AI Agents
In mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h around lines 191 to 203, the
header comments are out of sync with the implementation: for
getOrCreateFunctionDeclaration remove the claim that "deallocation" is annotated
(implementation only special-cases QIR_MEASURE and QIR_RESET) or update
implementation to also handle deallocation — pick one and make the doc and code
consistent; for createResultLabel update the wording to state that the global is
created at module level and the AddressOfOp is inserted at the start of main's
entry block (the builder's insertion point is temporarily restored via an
insertion guard), rather than claiming the AddressOfOp is inserted at the
builder's current insertion point. Ensure the docstrings explicitly reflect the
actual behavior and any restored insertion point semantics.

Copy link
Member Author

Choose a reason for hiding this comment

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

@denialhaag the @coderabbitai correctly points out that the docstring here is outdated.

Copy link
Contributor

Choose a reason for hiding this comment

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

@burgholzer @denialhaag You're absolutely right—the docstrings are out of sync with the implementation. Here's what needs to be fixed:

1. getOrCreateFunctionDeclaration (around line 191-203)

Current doc says: marks "measurement, reset, deallocation" as irreversible
Actual behavior: only marks QIR_MEASURE and QIR_RESET

Suggested fix: Either:

  • Update the doc to say "measurement and reset" (simpler), or
  • Extend the implementation to also handle deallocation functions if that's intended behavior

2. createResultLabel (docstring not fully shown in diff)

Current doc says: inserts AddressOfOp at builder's current insertion point
Actual behavior: creates a module-level global string constant and inserts the AddressOfOp at the start of main's entry block (using InsertionGuard to restore the original position)

Suggested fix: Update the docstring to say:

/**
 * @brief Create a module-level string label and return an AddressOfOp to it
 *
 * @details
 * Creates a global string constant at the module level (with addr_space and
 * dso_local attributes) and inserts an AddressOfOp at the start of the main
 * function's entry block. The builder's original insertion point is preserved
 * via an InsertionGuard.
 * ...
 */

These changes will bring the documentation in line with the actual implementation.


🧠 Learnings used
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp:96-117
Timestamp: 2025-12-08T23:41:55.972Z
Learning: In the QIR (Quantum Intermediate Representation) Builder (mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp), the `ptrCache` is intentionally shared between qubit and result pointer creation (in `staticQubit()` and `measure()` methods) because QIR uses opaque pointers and `inttoptr` conversions for both qubits and results. For any given index N, the LLVM IR pointer representation is identical whether it represents a qubit or a result, so the pointer only needs to be created once and can be safely reused across both contexts.

Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:171-180
Timestamp: 2025-10-09T13:13:51.224Z
Learning: In MQT Core MLIR, UnitaryInterface operations guarantee 1-1 correspondence between input and output qubits in the same order. When cloning or modifying unitary operations (e.g., removing controls), this correspondence is maintained by construction, so yielding getAllInQubits() in else-branches matches the result types from the operation's outputs.

Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Quartz/IR/Operations/StandardGates/PhaseOp.cpp:0-0
Timestamp: 2025-12-08T23:44:44.157Z
Learning: In MLIR code (mlir/ directories), avoid using const qualifiers on core MLIR data types like Value, Type, Attribute, Operation*, Block*, Region*, etc. in function parameters and signatures. MLIR's design rationale explicitly discourages const for these types. See https://mlir.llvm.org/docs/Rationale/UsageOfConst/ for details.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Operations/StandardGates/BarrierOp.cpp:45-54
Timestamp: 2025-12-09T00:55:11.926Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux), qubits follow single-user/linear-type semantics where each qubit value can only be consumed once, similar to MQTOpt. This invariant makes it safe to dereference getUsers().begin() in canonicalization patterns like MergeSubsequentBarrier in BarrierOp.cpp, as there will be at most one user per qubit output.

Comment on lines 165 to 205
Value QIRProgramBuilder::measure(const Value qubit, const int64_t resultIndex) {
if (resultIndex < 0) {
llvm::reportFatalUsageError("Result index must be non-negative");
}

// Save current insertion point
const OpBuilder::InsertionGuard guard(builder);

// Insert in measurements block (before branch)
builder.setInsertionPoint(measurementsBlock->getTerminator());

const auto key = std::make_pair("c", resultIndex);
if (const auto it = registerResultMap.find(key);
it != registerResultMap.end()) {
return it->second;
}

Value resultValue{};
if (const auto it = ptrCache.find(resultIndex); it != ptrCache.end()) {
resultValue = it->second;
} else {
resultValue = createPointerFromIndex(builder, loc, resultIndex);
ptrCache[resultIndex] = resultValue;
registerResultMap.insert({key, resultValue});
}

// Update result count
if (std::cmp_greater_equal(resultIndex, metadata_.numResults)) {
metadata_.numResults = static_cast<size_t>(resultIndex) + 1;
}

// Create mz call
auto ptrType = LLVM::LLVMPointerType::get(builder.getContext());
const auto mzSignature = LLVM::LLVMFunctionType::get(
LLVM::LLVMVoidType::get(builder.getContext()), {ptrType, ptrType});
auto mzDecl =
getOrCreateFunctionDeclaration(builder, module, QIR_MEASURE, mzSignature);
builder.create<LLVM::CallOp>(loc, mzDecl, ValueRange{qubit, resultValue});

return resultValue;
}
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 8, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Measurements reused from ptrCache are never recorded in registerResultMap.

In measure(const Value qubit, const int64_t resultIndex), when ptrCache already has an entry for resultIndex, you reuse the pointer but don't insert the corresponding ("c", resultIndex) key into registerResultMap. Since generateOutputRecording() iterates only over registerResultMap, such measurements will never be emitted as array_record_output / result_record_output, even though an mz call is generated.

To make recording consistent (and match the QuartzToQIR lowering behavior), registerResultMap should be updated regardless of whether the pointer came from ptrCache or was freshly created, e.g.:

-  Value resultValue{};
-  if (const auto it = ptrCache.find(resultIndex); it != ptrCache.end()) {
-    resultValue = it->second;
-  } else {
-    resultValue = createPointerFromIndex(builder, loc, resultIndex);
-    ptrCache[resultIndex] = resultValue;
-    registerResultMap.insert({key, resultValue});
-  }
+  Value resultValue{};
+  if (const auto it = ptrCache.find(resultIndex); it != ptrCache.end()) {
+    resultValue = it->second;
+  } else {
+    resultValue = createPointerFromIndex(builder, loc, resultIndex);
+    ptrCache[resultIndex] = resultValue;
+  }
+  registerResultMap.insert({key, resultValue});

You may also want to decide whether the early if (registerResultMap.find(key) != end()) return it->second; should still bypass emitting a new mz call, or if repeated calls with the same resultIndex should produce additional measurements.

Copy link
Member Author

Choose a reason for hiding this comment

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

@denialhaag I am pretty sure that @coderabbitai is wrong here. I remember that I did this on purpose. Most likely because repeated measurements on the same classical bit override each other and the output recording only should contain one in that case.

Copy link
Contributor

Choose a reason for hiding this comment

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

@burgholzer Thank you for the clarification! You're absolutely right that the early return at line 175 handles the duplicate measurement case elegantly:

if (const auto it = registerResultMap.find(key); it != registerResultMap.end()) {
  return it->second;  // Prevents duplicate mz calls and duplicate output recording
}

This ensures repeated calls to measure(qubit, resultIndex) with the same resultIndex return the cached pointer without generating additional mz calls or duplicate output entries—exactly the override behavior you intended.

However, I believe CodeRabbit is flagging a different edge case: when ptrCache[resultIndex] exists (perhaps populated by staticQubit(resultIndex) due to the shared cache, or by allocClassicalBitRegister for a different register name) but registerResultMap[("c", resultIndex)] does not exist.

In that scenario:

  1. The early return is not triggered (no entry in registerResultMap yet)
  2. The pointer is reused from ptrCache
  3. A new mz call is generated
  4. But the pointer is not inserted into registerResultMap (because the insert is inside the else block at line 191)
  5. Therefore, generateOutputRecording() won't emit output for this measurement

Question: Is this behavior intentional? For example:

  • Is mixing allocClassicalBitRegister("my_reg", ...) with raw measure(qubit, index) not a supported usage pattern?
  • Or should mz calls without corresponding output recording be expected when indices overlap?

If this edge case is indeed intended (e.g., to avoid mixing allocation styles or because overlapping indices should skip output recording), then CodeRabbit's suggestion can be disregarded. Otherwise, moving the registerResultMap.insert outside the else block would ensure consistency.


✏️ Learnings added
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp:0-0
Timestamp: 2025-12-10T02:19:50.634Z
Learning: In QIRProgramBuilder::measure(Value, int64_t), the intentional design prevents duplicate measurements: the early return when `registerResultMap.find(key)` succeeds avoids generating multiple `mz` calls to the same classical bit index and ensures output recording contains only one entry per index. This implements an "override" semantic where repeated measurements to the same resultIndex reuse the cached pointer without additional side effects.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp:96-117
Timestamp: 2025-12-08T23:41:55.972Z
Learning: In the QIR (Quantum Intermediate Representation) Builder (mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp), the `ptrCache` is intentionally shared between qubit and result pointer creation (in `staticQubit()` and `measure()` methods) because QIR uses opaque pointers and `inttoptr` conversions for both qubits and results. For any given index N, the LLVM IR pointer representation is identical whether it represents a qubit or a result, so the pointer only needs to be created once and can be safely reused across both contexts.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Operations/StandardGates/BarrierOp.cpp:45-54
Timestamp: 2025-12-09T00:55:11.926Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux), qubits follow single-user/linear-type semantics where each qubit value can only be consumed once, similar to MQTOpt. This invariant makes it safe to dereference getUsers().begin() in canonicalization patterns like MergeSubsequentBarrier in BarrierOp.cpp, as there will be at most one user per qubit output.

Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:171-180
Timestamp: 2025-10-09T13:13:51.224Z
Learning: In MQT Core MLIR, UnitaryInterface operations guarantee 1-1 correspondence between input and output qubits in the same order. When cloning or modifying unitary operations (e.g., removing controls), this correspondence is maintained by construction, so yielding getAllInQubits() in else-branches matches the result types from the operation's outputs.

Comment on lines +128 to +132
QuantumCompilerConfig config;
config.convertToQIR = CONVERT_TO_QIR;
config.enableTiming = ENABLE_TIMING;
config.enableStatistics = ENABLE_STATISTICS;
config.printIRAfterAllStages = PRINT_IR_AFTER_ALL_STAGES;
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 8, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "recordIntermediates" --type cpp -C 3

Repository: munich-quantum-toolkit/core

Length of output: 8392


🏁 Script executed:

rg -n "recordIntermediates" --type h -C 3

Repository: munich-quantum-toolkit/core

Length of output: 1882


🏁 Script executed:

# Also check the QuantumCompilerConfig struct definition
fd "CompilerPipeline.h" --type f -x cat -n {}

Repository: munich-quantum-toolkit/core

Length of output: 5390


Add --record-intermediates CLI option to expose the intermediate IR recording feature.

The recordIntermediates field in QuantumCompilerConfig is designed for debugging and testing workflows and is actively used throughout the pipeline to capture IR snapshots at 10 different stages. However, unlike the other four config options (convertToQIR, enableTiming, enableStatistics, printIRAfterAllStages), it has no CLI flag to enable it. Consider adding a --record-intermediates flag to maintain consistency and allow users to access this debugging feature from the command line.

🤖 Prompt for AI Agents
In mlir/tools/mqt-cc/mqt-cc.cpp around lines 128 to 132, the
QuantumCompilerConfig.recordIntermediates field is not exposed via the CLI; add
a new command-line flag --record-intermediates (boolean, default false)
alongside the existing flags, parse it in the argument handling, and assign its
value to config.recordIntermediates so that the intermediate-IR recording
feature can be enabled from the command line; also add a brief help/description
for the new flag consistent with the other option help texts.

Copy link
Member Author

Choose a reason for hiding this comment

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

@denialhaag I like the suggestion by @coderabbitai to add such a CLI flag.

Copy link
Contributor

Choose a reason for hiding this comment

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

@burgholzer, I agree—adding the --record-intermediates CLI flag would provide a consistent interface for this debugging feature! Here's how to implement it:

1. Add the CLI flag alongside the existing options (after line 62):

const cl::opt<bool>
    RECORD_INTERMEDIATES("record-intermediates",
                        cl::desc("Record intermediate IR at each compiler stage"),
                        cl::init(false));

2. Wire it to the config struct (after line 128):

QuantumCompilerConfig config;
config.convertToQIR = CONVERT_TO_QIR;
config.recordIntermediates = RECORD_INTERMEDIATES;  // Add this line
config.enableTiming = ENABLE_TIMING;
config.enableStatistics = ENABLE_STATISTICS;
config.printIRAfterAllStages = PRINT_IR_AFTER_ALL_STAGES;

This will enable users to capture IR snapshots at all 10 pipeline stages via:

mqt-cc input.mlir --record-intermediates

The recorded IR will be populated in the CompilationRecord structure for debugging and testing workflows.


🧠 Learnings used
Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1360
File: .github/workflows/reusable-mlir-tests.yml:40-43
Timestamp: 2025-12-05T17:45:37.602Z
Learning: In the munich-quantum-toolkit/core repository, patch releases of LLVM dependencies don't require documentation updates, changelog entries, or additional tests beyond what's validated by passing CI checks.

Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:219-221
Timestamp: 2025-10-09T13:14:10.178Z
Learning: The MQT Core project (munich-quantum-toolkit/core repository) uses the C++20 standard, not C++17. C++20 features such as abbreviated function templates (e.g., `const auto&` parameters) are supported and valid in this codebase.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Operations/StandardGates/BarrierOp.cpp:45-54
Timestamp: 2025-12-09T00:55:11.926Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux), qubits follow single-user/linear-type semantics where each qubit value can only be consumed once, similar to MQTOpt. This invariant makes it safe to dereference getUsers().begin() in canonicalization patterns like MergeSubsequentBarrier in BarrierOp.cpp, as there will be at most one user per qubit output.

Copy link
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: 3

♻️ Duplicate comments (6)
mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp (1)

165-205: Cache hit path skips registerResultMap insertion, breaking output recording.

When measure(Value qubit, int64_t resultIndex) finds a cached pointer (line 183-184), it reuses the pointer but never inserts the key-value pair into registerResultMap. The insert() call on line 188 is only executed in the else branch when creating a new pointer.

Since generateOutputRecording() (lines 551-620) iterates only over registerResultMap, measurements that reuse cached pointers will never be recorded as output, even though an mz call is emitted (line 202). This causes silent data loss in the output recording.

Apply this fix to insert into registerResultMap regardless of cache status:

   Value resultValue{};
   if (const auto it = ptrCache.find(resultIndex); it != ptrCache.end()) {
     resultValue = it->second;
   } else {
     resultValue = createPointerFromIndex(builder, loc, resultIndex);
     ptrCache[resultIndex] = resultValue;
-    registerResultMap.insert({key, resultValue});
   }
+  registerResultMap.insert({key, resultValue});

Note: The early return at lines 177-180 already prevents duplicate insertions for the same key, so moving the insert outside the else branch is safe.

mlir/include/mlir/Dialect/Flux/IR/FluxDialect.h (1)

64-148: Consider adding range-style accessors for qubits as a future convenience.

Given earlier discussion about getInputQubits() / getOutputQubits() (and possibly target/control ranges), this trait would be a natural place to expose inexpensive ArrayRef<Value>-style views over the qubits instead of forcing callers to repeatedly index via getInputQubit/getOutputQubit.

Not blocking for this PR, but worth considering either here or in a follow‑up to avoid ad‑hoc containers in clients.

mlir/lib/Dialect/Quartz/Builder/QuartzProgramBuilder.cpp (1)

413-430: Non-deterministic deallocation order from DenseSet iteration.

The iteration over allocatedQubits (a DenseSet) has non-deterministic order. While functionally correct, this may produce different IR output across runs, complicating debugging and testing.

Consider collecting qubits into a sorted container before deallocation for deterministic output:

 OwningOpRef<ModuleOp> QuartzProgramBuilder::finalize() {
-  // Automatically deallocate all remaining allocated qubits
-  for (Value qubit : allocatedQubits) {
+  // Collect qubits for deterministic deallocation order
+  SmallVector<Value> qubitsToDealloc(allocatedQubits.begin(),
+                                      allocatedQubits.end());
+  // Sort by opaque pointer for stable ordering
+  llvm::sort(qubitsToDealloc, [](Value a, Value b) {
+    return a.getAsOpaquePointer() < b.getAsOpaquePointer();
+  });
+  for (Value qubit : qubitsToDealloc) {
     create<DeallocOp>(loc, qubit);
   }
mlir/include/mlir/Dialect/Quartz/IR/QuartzDialect.h (1)

64-119: Code duplication with Flux dialect's TargetAndParameterArityTrait.

This trait is largely identical to the Flux dialect's version in mlir/include/mlir/Dialect/Flux/IR/FluxDialect.h (lines 51-148). The main differences are:

  • Quartz uses getQubit/getTarget (reference semantics)
  • Flux uses getInputQubit/getOutputQubit and getInputTarget/getOutputTarget (value semantics with SSA)

Consider extracting common static methods and parameter handling into a shared base trait to reduce maintenance burden.

mlir/lib/Dialect/Flux/IR/Operations/StandardGates/BarrierOp.cpp (1)

35-73: Guard against empty user ranges and only-first-user checks in MergeSubsequentBarrier

The loop dereferences getUsers().begin() unconditionally and only inspects that first user:

if (llvm::isa<BarrierOp>(
        *op.getOutputForInput(qubitsIn[i]).getUsers().begin())) {

This has two problems:

  • If that output currently has no users, dereferencing begin() is UB.
  • If there were ever multiple users, only the first is considered.

A safer and still simple approach is to grab the user range once, check emptiness, and ensure the (single) user is a BarrierOp, e.g.:

-    for (size_t i = 0; i < qubitsIn.size(); ++i) {
-      if (llvm::isa<BarrierOp>(
-              *op.getOutputForInput(qubitsIn[i]).getUsers().begin())) {
+    for (size_t i = 0; i < qubitsIn.size(); ++i) {
+      auto users = op.getOutputForInput(qubitsIn[i]).getUsers();
+      if (!users.empty() && llvm::hasSingleElement(users) &&
+          llvm::isa<BarrierOp>(*users.begin())) {
         anythingToMerge = true;
         newQubitsOutMap[i] = qubitsIn[i];
       } else {
         newQubitsIn.push_back(qubitsIn[i]);
         indicesToFill.push_back(i);
       }
     }

This avoids UB and makes the intended “single barrier user” precondition explicit.

mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp (1)

569-583: (Optional) Deallocation order from DenseSet is non-deterministic

finalize() deallocates qubits by iterating validQubits, which is a DenseSet:

for (const auto qubit : validQubits) {
  create<DeallocOp>(loc, qubit);
}

This makes the DeallocOp sequence order-dependent on hash iteration, which can vary between runs and slightly complicate IR diffs. If deterministic IR is desirable, you could sort the qubits (e.g., by definition order) before deallocating; otherwise, consider adding a brief comment that the deallocation order is intentionally unspecified.

Comment on lines +35 to +38
FluxProgramBuilder::FluxProgramBuilder(MLIRContext* context)
: OpBuilder(context), ctx(context), loc(getUnknownLoc()),
module(create<ModuleOp>(loc)) {}

Copy link
Contributor

@coderabbitai coderabbitai bot Dec 9, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the FluxProgramBuilder.cpp file
find . -name "FluxProgramBuilder.cpp" -type f

Repository: munich-quantum-toolkit/core

Length of output: 125


🏁 Script executed:

# Get a broader search in case file structure differs
find . -name "*FluxProgramBuilder*" -type f

Repository: munich-quantum-toolkit/core

Length of output: 187


🏁 Script executed:

# Search for any Flux-related builder files
find . -path "*Flux*" -name "*.cpp" -o -path "*Flux*" -name "*.h" | head -20

Repository: munich-quantum-toolkit/core

Length of output: 1188


🏁 Script executed:

# Read the FluxProgramBuilder.cpp file
cat -n mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp | head -60

Repository: munich-quantum-toolkit/core

Length of output: 2299


🏁 Script executed:

# Also read the header for context on the class
cat -n mlir/include/mlir/Dialect/Flux/Builder/FluxProgramBuilder.h

Repository: munich-quantum-toolkit/core

Length of output: 50385


🏁 Script executed:

# Search for other ModuleOp creation patterns in the codebase
rg "ModuleOp" --type cpp -A 2 -B 2 | head -100

Repository: munich-quantum-toolkit/core

Length of output: 7309


🏁 Script executed:

# Search for OpBuilder::create usage patterns
rg "create<ModuleOp>" --type cpp -A 2 -B 2

Repository: munich-quantum-toolkit/core

Length of output: 1482


🏁 Script executed:

# Check for module creation in tests or other builder files
fd -e cpp -e h | xargs grep -l "ModuleOp::create" | head -5

Repository: munich-quantum-toolkit/core

Length of output: 53


🏁 Script executed:

# Search for OpBuilder class definition in MLIR includes
find . -path "*mlir*" -name "*.h" | xargs grep -l "class OpBuilder" | head -3

Repository: munich-quantum-toolkit/core

Length of output: 53


🏁 Script executed:

# Check if there are any issues or discussions about this pattern
rg "insertion point" --type cpp -i | grep -i "module\|builder" | head -20

Repository: munich-quantum-toolkit/core

Length of output: 1396


🏁 Script executed:

# Look for any tests that use FluxProgramBuilder constructor
rg "FluxProgramBuilder" --type cpp -A 5 | head -80

Repository: munich-quantum-toolkit/core

Length of output: 6219


🌐 Web query:

MLIR OpBuilder create ModuleOp insertion point requirement

💡 Result:

Short answer: OpBuilder must have a valid insertion point (i.e., an insertion Block + iterator) when you call create. You cannot create an operation with no insertion point (clearInsertionPoint() makes creation an error), and ModuleOp will be inserted into the Block/Region corresponding to that insertion point (so create the ModuleOp where you want it in the top-level region). [1][2]

References:

  • OpBuilder: create(), InsertPoint, clearInsertionPoint() (insertion point must be set). [1]
  • ModuleOp: insertion/insert behavior (operations are placed into the module body before the terminator). [3]

🌐 Web query:

MLIR ModuleOp::create static method API

💡 Result:

Signature (C++):
static ModuleOp ModuleOp::create(mlir::Location loc, llvm::Optionalllvm::StringRef name = llvm::None);

What it does (concise):

  • Creates a new builtin.module operation at the given Location.
  • Optional name becomes the module sym_name (module attribute) when provided.
  • The created ModuleOp contains the single region/block structure expected for modules (with a module terminator). Use OpBuilder::create or push_back to insert the created op into a parent/IR as needed.

References:
[1] mlir/include/mlir/IR/Module.h — ModuleOp declaration.
[2] mlir class reference (ModuleOp::create / build) (generated docs).
[3] Example usage in MLIR examples (creating an empty module via ModuleOp::create).


🏁 Script executed:

# Check OpBuilder constructor in includes
find . -path "*mlir*" -name "*.h" | xargs grep -A 10 "OpBuilder.*MLIRContext" | head -40

Repository: munich-quantum-toolkit/core

Length of output: 53


🏁 Script executed:

# Look at the test setup more carefully
grep -A 20 "buildFluxIR\|FluxProgramBuilder builder" mlir/unittests/pipeline/test_compiler_pipeline.cpp | head -50

Repository: munich-quantum-toolkit/core

Length of output: 1654


Use ModuleOp::create for top-level module creation in constructor

The constructor currently creates the module via create<ModuleOp>(loc), which requires the OpBuilder to have a valid insertion point. At construction time, no insertion point is set.

Use the static factory method instead:

-FluxProgramBuilder::FluxProgramBuilder(MLIRContext* context)
-    : OpBuilder(context), ctx(context), loc(getUnknownLoc()),
-      module(create<ModuleOp>(loc)) {}
+FluxProgramBuilder::FluxProgramBuilder(MLIRContext* context)
+    : OpBuilder(context), ctx(context), loc(getUnknownLoc()),
+      module(ModuleOp::create(loc)) {}

ModuleOp::create(loc) is the proper MLIR API for creating root-level modules without requiring an insertion point. The initialize() method can then safely set the insertion point within the module as you do at line 44.

📝 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
FluxProgramBuilder::FluxProgramBuilder(MLIRContext* context)
: OpBuilder(context), ctx(context), loc(getUnknownLoc()),
module(create<ModuleOp>(loc)) {}
FluxProgramBuilder::FluxProgramBuilder(MLIRContext* context)
: OpBuilder(context), ctx(context), loc(getUnknownLoc()),
module(ModuleOp::create(loc)) {}
🤖 Prompt for AI Agents
In mlir/lib/Dialect/Flux/Builder/FluxProgramBuilder.cpp around lines 35 to 38,
the constructor uses create<ModuleOp>(loc) which requires an OpBuilder insertion
point that isn't set at construction; replace that with the root-level factory
ModuleOp::create(loc) to create the top-level module without needing an
insertion point, then keep the existing initialize() logic that sets the
insertion point inside the module.

Copy link
Member Author

Choose a reason for hiding this comment

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

@denialhaag I am pretty sure that @coderabbitai is actually onto something here. The latest CLion update enabled a new clang-tidy rule that also seems to suggest the usage of the static factory methods. This applies very generally across the code base.

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

@burgholzer Thank you for confirming this! Let me search for other instances of this pattern across the codebase that might need the same fix.


🏁 Script executed:

#!/bin/bash
# Search for similar patterns where create<ModuleOp> is used in constructors
echo "=== Searching for create<ModuleOp> usage in builders ==="
rg "create<ModuleOp>" --type cpp -B 3 -A 1

echo -e "\n=== Checking other ProgramBuilder files ==="
fd "ProgramBuilder.cpp" --type f -x grep -n "create<ModuleOp>" {}

echo -e "\n=== Looking for ModuleOp constructor initialization patterns ==="
rg "ModuleOp.*\(.*create<ModuleOp>" --type cpp -B 2 -A 1

Length of output: 1652


@burgholzer You're absolutely right! I found three instances of this pattern across the codebase, all in the ProgramBuilder constructors:

  1. FluxProgramBuilder.cpp (line 36) - current file
  2. QuartzProgramBuilder.cpp (line 37)
  3. QIRProgramBuilder.cpp (line 39)

All three should be updated to use ModuleOp::create(loc) instead of create<ModuleOp>(loc) (or builder.create<ModuleOp>(...) in QIR's case).

Recommended fixes:

FluxProgramBuilder.cpp:

FluxProgramBuilder::FluxProgramBuilder(MLIRContext* context)
    : OpBuilder(context), ctx(context), loc(getUnknownLoc()),
      module(ModuleOp::create(loc)) {}

QuartzProgramBuilder.cpp:

QuartzProgramBuilder::QuartzProgramBuilder(MLIRContext* context)
    : OpBuilder(context), ctx(context), loc(getUnknownLoc()),
      module(ModuleOp::create(loc)) {}

QIRProgramBuilder.cpp:

QIRProgramBuilder::QIRProgramBuilder(MLIRContext* context)
    : builder(context),
      module(ModuleOp::create(UnknownLoc::get(context))),
      loc(UnknownLoc::get(context)) {}

This is a good catch by the new clang-tidy rule! Using the static factory method is the correct MLIR idiom for creating top-level modules before setting an insertion point.


🧠 Learnings used
Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp:144-151
Timestamp: 2025-12-02T07:37:46.860Z
Learning: In MLIR transformation code (mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/sc/NaiveRoutingPass.cpp and similar), when inserting operations before a target operation, prefer `rewriter.setInsertionPoint(op)` over `rewriter.setInsertionPointAfter(op->getPrevNode())`. The former is cleaner, avoids null pointer edge cases (when op is first in block), and is semantically clearer.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Operations/StandardGates/BarrierOp.cpp:45-54
Timestamp: 2025-12-09T00:55:11.926Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux), qubits follow single-user/linear-type semantics where each qubit value can only be consumed once, similar to MQTOpt. This invariant makes it safe to dereference getUsers().begin() in canonicalization patterns like MergeSubsequentBarrier in BarrierOp.cpp, as there will be at most one user per qubit output.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp:78-100
Timestamp: 2025-12-08T14:55:43.899Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux/IR/Modifiers/CtrlOp.cpp), GPhaseOp is a zero-target operation (global phase). When a CtrlOp wraps a GPhaseOp, it only has control qubits and no targets. The CtrlInlineGPhase canonicalization pattern correctly produces outputs only for the positive controls, not targets.

Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:171-180
Timestamp: 2025-10-09T13:13:51.224Z
Learning: In MQT Core MLIR, UnitaryInterface operations guarantee 1-1 correspondence between input and output qubits in the same order. When cloning or modifying unitary operations (e.g., removing controls), this correspondence is maintained by construction, so yielding getAllInQubits() in else-branches matches the result types from the operation's outputs.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp:80-100
Timestamp: 2025-12-08T23:58:09.648Z
Learning: In the Quartz dialect (mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp), quartz.ctrl uses reference semantics and does not return values, unlike flux.ctrl which uses value semantics and returns transformed qubits. When inlining a GPhaseOp in the CtrlInlineGPhase pattern, it's correct to create POp operations for positive controls and erase the CtrlOp without collecting or replacing result values.

Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp:96-117
Timestamp: 2025-12-08T23:41:55.972Z
Learning: In the QIR (Quantum Intermediate Representation) Builder (mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp), the `ptrCache` is intentionally shared between qubit and result pointer creation (in `staticQubit()` and `measure()` methods) because QIR uses opaque pointers and `inttoptr` conversions for both qubits and results. For any given index N, the LLVM IR pointer representation is identical whether it represents a qubit or a result, so the pointer only needs to be created once and can be safely reused across both contexts.

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

Labels

c++ Anything related to C++ code minor Minor version update MLIR Anything related to MLIR refactor Anything related to code refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants