✨ Add power modifier to MLIR#1603
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
f017b29 to
4964ed0
Compare
|
|
||
| // Move supporting ops (constants, arithmetic) out of the body so their | ||
| // Values are accessible from outside and survive PowOp erasure. | ||
| for (auto& bodyOp : llvm::make_early_inc_range(*op.getBody())) { |
There was a problem hiding this comment.
Because inv creates a negf operation inside its body we must move out these operations when we want to apply gate specific canonicalizations. Currently I do this here unconditionally - which feels pretty wrong. Isnt there a way for hoisting up operations which can/should be moved out of the body? Should maybe inv and pow run such a hositing up thing?
There was a problem hiding this comment.
I am pretty sure that these operations are automatically hoisted up as part of canonicalization.
I remember this being problematic for nested Control and Inverse modifiers already. But we never had to handle that explicitly.
There was a problem hiding this comment.
Code for inv { pow (2) { rx(0.123) }}:
module {
func.func @testPow() {
%q0_0 = qco.alloc : !qco.qubit
%q0_1 = qco.inv (%a = %q0_0){
%a_1 = qco.pow (2.000000e+00) (%b = %a) {
%c_0 = arith.constant 0.123 : f64
%b_1 = qco.rx(%c_0) %b : !qco.qubit -> !qco.qubit
qco.yield %b_1
} : {!qco.qubit} -> {!qco.qubit}
qco.yield %a_1
} : {!qco.qubit} -> {!qco.qubit}
qco.sink %q0_1 : !qco.qubit
return
}
}will lead to this state (swap pow and inv and then do ReplaceWithKnownGates for inv and rx:
module {
func.func @testPow() {
%cst = arith.constant 1.230000e-01 : f64
%0 = qco.alloc : !qco.qubit
%1 = qco.pow(2.000000e+00) (%arg0 = %0) {
%2 = arith.negf %cst : f64
%3 = qco.rx(%2) %arg0 : !qco.qubit -> !qco.qubit
qco.yield %3
} : {!qco.qubit} -> {!qco.qubit}
qco.sink %1 : !qco.qubit
return
}
}If %2 is not explicitly moved out of the pow modifier it is not possible to merge pow and rx with the known canonicalization. So leaving the code out leads to segfault (ask me how I know lol).
There was a problem hiding this comment.
So the segfault possibly occurs, because we want to create pow(r) { rx(θ) } → rx(r*θ) with r = %2. %2 only exists inside the body of pow which gets replaced/deleted when doing replaceOpWithNewOp, creating a dangling reference to %2.
There was a problem hiding this comment.
Hm. This very much depends on the sequence of the individual canonicalization passes.
Because there definitely is a canonicalization that will pull out the %2 = arith.negf %cst : f64 and fold it into the %cst = arith.constant 1.230000e-01 : f64 statement, which would result in the elimination of the arith.negf instruction and would also eliminate the segfault cause.
There has to be a way to address this. Either by making the canonicalization more robust (e.g., by moving the body of the original pow operation to the outside and then performing the manipulation) or by marking the canonicalization only as valid whenever the body of the operation only consists of the UnitaryOperation (and the yield).
The first solution seems to be more desirable to me.
This is kind of like inlining plus some additional manipulation.
I am pretty sure https://mlir.llvm.org/doxygen/classmlir_1_1RewriterBase.html#a4307b18bc25cec55349236d556cc89bd should do the trick here.
The canonicalization is basically inlining the block and adding a arith.mulf (or whatever the instruction is called) on the rotation angle argument before replacing the rotation angle argument with the new rotation angle.
|
Ok, I discovered another problem, consider this example:
My idea then: the then and then The only thing im not to sure about, whether the Concrete implementation: instead of emiting 2 operations, the canonicalization creates an inner operation and a preamble.
I dont know if creating a whole interface for that is premature, i believe only the power modifier emits 2-operation-canonicalizations so the whole thing could stay within power modifier. edit: maybe, when keeping it general it would make sense having this as an interface, so that other operations could be moved out as well (like arithmetic operations which e.g. |
2375c9d to
993a3eb
Compare
faeba24 to
05c9335
Compare
|
While implementing the QC<->QCO conversion tests, I noticed some friction: |
91700af to
b1759c7
Compare
|
Ok, commits cleaned up, linting and coverage succeeds - i think this is ready for a first look @DRovara @burgholzer |
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (23)
💤 Files with no reviewable changes (9)
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds a PowOp modifier across QC and QCO: TableGen op declarations, builder APIs, bidirectional conversions, canonicalization/folding and unitary-matrix support, math helpers, and broad unit-test/program-builder coverage for pow semantics. ChangesPowOp modifier
Sequence Diagram(s)sequenceDiagram
participant User
participant QCBuilder as QC ProgramBuilder
participant QCPowOp as qc::PowOp
participant Converter as QC↔QCO Converter
participant Canon as Canonicalizer
User->>QCBuilder: pow(exponent, body)
QCBuilder->>QCPowOp: create with exponent and body callback
QCPowOp->>QCPowOp: verify structure (exponent constant, single unitary op)
alt convert to QCO
Converter->>Converter: resolve mapped qubits
Converter->>Converter: create qco.pow, move body region
Converter->>Converter: add entry block qubit args
end
QCPowOp->>Canon: enqueue for canonicalization
Canon->>Canon: apply patterns (inline pow(1), erase pow(0))
Canon->>Canon: merge nested pow, move ctrl outside
Canon->>Canon: fold into gate (scale params, decompose)
alt folding succeeds
Canon->>User: replace with simplified gates/parameters
else folding inapplicable
Canon->>User: leave PowOp unchanged
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches✨ Simplify code
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 markdownlint-cli2 (0.22.1)CHANGELOG.mdmarkdownlint-cli2 v0.22.1 (markdownlint v0.40.0) Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (2)
mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp (1)
68-76:⚠️ Potential issue | 🔴 CriticalThe rewrite set is broader than the body invariant it actually preserves.
verify()admits helper ops before the body unitary, butNegPowToInvPowandMoveCtrlOutsideclone only the unitary,MergeNestedPowonly retargets the moved op’s qubit operands, and the identity-producing branches replacepowwith plain pass-through values. With body-localarith.*or captured values, that can dangle defs after replacement; with a nested parent modifier, it can leave the parent region with no unitary at all. Either tightenverify()back to the single-unitary form, or preserve/hoist the full prologue and rewrite enclosing modifiers in the same pattern.Also applies to: 158-169, 183-199, 218-221, 244-259, 284-289, 482-499, 596-655
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 68 - 76, The rewrite patterns (NegPowToInvPow, MoveCtrlOutside, MergeNestedPow, and functions like tryReplaceWithNamedPhaseGate operating on PowOp) assume a single-unitary body but verify() currently allows helper ops before the unitary; this leads to dangling defs or removal of the unitary while leaving prologue helpers behind. Fix by either tightening verify() on PowOp to require exactly one unitary op in the body (no prologue helpers), or update each rewrite (NegPowToInvPow, MoveCtrlOutside, MergeNestedPow and tryReplaceWithNamedPhaseGate) to preserve/hoist the full prologue: when cloning or retargeting the unitary, also clone or move any preceding helper ops and update their uses, ensure replacements replace the entire region (including helpers) or remap all results/operands so no defs are left dangling, and update operand/region remapping logic to maintain the same external SSA edges for PowOp.mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp (1)
60-68:⚠️ Potential issue | 🔴 CriticalThese rewrites assume a stricter body invariant than
verify()enforces.
verify()currently permits arbitrary non-unitary ops before the body unitary, but the structural rewrites only preservegetBodyUnitary(), and some folds erasePowOpoutright. With body-localarith.*defs, that can dangle operands after replacement; with a nested parent modifier, it can leave the parent body with no unitary at all. Either tighten the body back tounitary + yield, or hoist/inline the full prologue and rewrite the enclosing modifier atomically.Based on learnings,
InvOp::verifyintentionally restricts the body region to exactly two operations (one unitary + yield).Also applies to: 149-158, 170-173, 188-190, 204-207, 230-235, 431-450, 498-517
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@mlir/include/mlir/Dialect/QC/IR/QCOps.td`:
- Around line 1039-1047: The target/control accessors for the pow op are
inconsistent: getNumTargets() and getTarget(size_t) use overall qubit semantics
while getTargets() delegates to getBodyUnitary(), causing mismatched behavior;
update the pow op's accessors to mirror the InvOp pattern by delegating
target/control-related calls to getBodyUnitary() instead of using
getNumQubits()/getQubit(), i.e., make getNumTargets() call
getBodyUnitary().getNumTargets(), make getTarget(size_t i) call
getBodyUnitary().getTarget(i), and ensure
getNumControls()/getControls()/getControl() follow the same body-unitary
delegation so all UnitaryInterface accessors are consistent with
getBodyUnitary().
In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp`:
- Around line 71-75: The current replacement only clones
innerPow.getBodyUnitary().getOperation(), losing any non-terminator preamble ops
(constants/arithmetic) that feed the unitary; update the replacement passed to
rewriter.replaceOpWithNewOp<PowOp> / InvOp::create to iterate over
innerPow.getBody() and clone every non-terminator operation (preserving original
order) into the new region instead of cloning only innerPow.getBodyUnitary(), so
values used by the unitary remain available after MovePowOutside transforms.
In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 173-201: The NegPowToInvPow rewrite flips sign of the exponent by
replacing pow(p){...} with pow(-p){inv{...}}, which yields conjugated
eigenphases for operators that have eigenvalue -1 and thus produces incorrect
results; modify NegPowToInvPow (the matchAndRewrite for PowOp using
getExponentValue and creating an InvOp) to only apply when the operand unitary's
spectrum is guaranteed not to contain −1 (e.g., obtain the unitary via
getUnitaryMatrix() or a new op method and check no eigenvalue is within an
epsilon of -1), and otherwise skip the rewrite (return failure); alternatively
remove the NegPowToInvPow pattern entirely if no safe spectral test is
available.
In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp`:
- Around line 91-109: Add tests that exercise fractional powers of RXX to hit
the eigendecomposition/branch logic: create additional test cases in
QCOMatrixTest (e.g., new TEST_F methods or extend PowRxxOpMatrix) that build a
module with powRxx at fractional exponents like 0.5 and -0.5 (use the same
QCOProgramBuilder entry used for powRxx but with the power changed), retrieve
the PowOp via func::FuncOp and PowOp/getUnitaryMatrix(), compute the expected
matrix via dd::opToTwoQubitGateMatrix(qc::OpType::RXX, {theta * power}) into an
Eigen::Matrix4cd, and assert matrix->isApprox(eigenDefinition) for both 0.5 and
-0.5 to validate principal-branch/eigendecomposition behavior.
In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp`:
- Around line 893-894: The test case name string in the QCOTestCase entry is
incorrect: change QCOTestCase{"TdgThenS", MQT_NAMED_BUILDER(tdgThenT),
MQT_NAMED_BUILDER(emptyQCO)} so the human-readable name matches the builder by
renaming "TdgThenS" to "TdgThenT" (to align with the tdgThenT builder and
emptyQCO), ensuring test failure messages are not misleading.
In `@mlir/unittests/programs/qc_programs.cpp`:
- Around line 311-315: The global-phase sign is inverted in the reference
decompositions: in powThirdXRef change the gphase argument from -1.0/3.0 *
std::numbers::pi / 2.0 to +1.0/3.0 * std::numbers::pi / 2.0 so the reference
circuit matches the U/R* convention (i.e., X = gphase(+π/2)·rx(π)); apply the
same flip of sign for the gphase calls in the other reference functions
mentioned (the blocks at 357-361, 733-737, 789-793) so they use positive phase
corrections consistent with GPhaseOp(inputPhase - outPhase).
---
Duplicate comments:
In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 68-76: The rewrite patterns (NegPowToInvPow, MoveCtrlOutside,
MergeNestedPow, and functions like tryReplaceWithNamedPhaseGate operating on
PowOp) assume a single-unitary body but verify() currently allows helper ops
before the unitary; this leads to dangling defs or removal of the unitary while
leaving prologue helpers behind. Fix by either tightening verify() on PowOp to
require exactly one unitary op in the body (no prologue helpers), or update each
rewrite (NegPowToInvPow, MoveCtrlOutside, MergeNestedPow and
tryReplaceWithNamedPhaseGate) to preserve/hoist the full prologue: when cloning
or retargeting the unitary, also clone or move any preceding helper ops and
update their uses, ensure replacements replace the entire region (including
helpers) or remap all results/operands so no defs are left dangling, and update
operand/region remapping logic to maintain the same external SSA edges for
PowOp.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: e7ba28cd-bad0-4888-8e3a-03526f96e57b
📒 Files selected for processing (22)
mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.hmlir/include/mlir/Dialect/QC/IR/QCOps.tdmlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.hmlir/include/mlir/Dialect/QCO/IR/QCOOps.tdmlir/include/mlir/Dialect/Utils/Utils.hmlir/lib/Conversion/QCOToQC/QCOToQC.cppmlir/lib/Conversion/QCToQCO/QCToQCO.cppmlir/lib/Dialect/QC/Builder/QCProgramBuilder.cppmlir/lib/Dialect/QC/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QC/IR/Modifiers/PowOp.cppmlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cppmlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cppmlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cppmlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cppmlir/unittests/Dialect/QC/IR/test_qc_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cppmlir/unittests/programs/qc_programs.cppmlir/unittests/programs/qc_programs.hmlir/unittests/programs/qco_programs.cppmlir/unittests/programs/qco_programs.h
The problem with just invoking the conversion itself is that the verification becomes very hard at that point since canonicalization has not been run. Constants might be in different places. Some canonicalizations on classical constructs may not have been applied, etc. |
Hm. This one is tricky.
Technical nitpick, but these operations do not operate on the same qubit since the gphase operation has no target qubits.
This is definitely valid as it is also one of the existing canonicalizations for the CtrlOp when that op contains a GphaseOp. Before commenting on the proposal below, a couple of observations: --> --> --> At this stage, the real problem appears, because the The easy way out here is to not permit such PowOp rewrites in CtrlOp modifiers.
I believe I like the general direction of this, but I would not want to create that complexity as part of this PR. Let's rather get a working solution here for the majority of the cases and live with the fact that some constructs cannot be resolved at the moment, even though it would be technically possible. |
|
@coderabbitai continue |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp (2)
694-724: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winMake the matrix-availability predicate match
getUnitaryMatrix().Line 695 returns true for
pow(2.0) { x }becausexhas a known matrix, but lines 723-724 still returnstd::nulloptfor every non-0/1 exponent. GatehasCompileTimeKnownUnitaryMatrix()on the exponent cases actually implemented, or implement the general power computation.🛠️ Minimal predicate fix
bool PowOp::hasCompileTimeKnownUnitaryMatrix() { - return all_of(getBody()->getOps<UnitaryOpInterface>(), - [](UnitaryOpInterface op) { - return op.hasCompileTimeKnownUnitaryMatrix(); - }); + const double p = getExponentValue(); + if (std::abs(p) >= TOLERANCE && std::abs(p - 1.0) >= TOLERANCE) { + return false; + } + auto bodyUnitary = utils::getSoleBodyUnitary<UnitaryOpInterface>(*getBody()); + return bodyUnitary && bodyUnitary.hasCompileTimeKnownUnitaryMatrix(); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 694 - 724, The hasCompileTimeKnownUnitaryMatrix() method does not check the exponent value, causing it to return true even when getUnitaryMatrix() returns nullopt for unsupported exponents. Fix this inconsistency by adding exponent validation to hasCompileTimeKnownUnitaryMatrix(): use getExponentValue() to retrieve the exponent and check that it matches one of the supported cases (approximately equal to 0 or 1 using TOLERANCE comparisons), ensuring the predicate only returns true when getUnitaryMatrix() can actually produce a result.
621-686: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winReject non-unitary quantum side effects in
PowOp::verify.Line 657 only rejects extra unitary ops, so a
powbody can still containalloc/measure/reset/qtensor mutation before the sole unitary and be treated as a valid power modifier. Mirror theInvOpguard here, and reject duplicate input qubits while tightening the verifier.🛡️ Suggested verifier tightening
LogicalResult PowOp::verify() { FloatAttr attr; if (!matchPattern(getExponent(), m_Constant(&attr))) { return emitOpError("exponent must be a constant"); } auto& block = *getBody(); + if (llvm::any_of(block, [](Operation& nested) { + return isa<AllocOp, SinkOp, MeasureOp, ResetOp, qtensor::ExtractOp, + qtensor::InsertOp>(nested); + })) { + return emitOpError("body must not contain non-unitary quantum operations " + "or modify a quantum register"); + } + if (block.getOperations().size() < 2) { return emitOpError("body region must have at least two operations"); } const auto numTargets = getNumTargets(); + SmallPtrSet<Value, 4> uniqueQubitsIn; + for (Value target : getQubitsIn()) { + if (!uniqueQubitsIn.insert(target).second) { + return emitOpError("duplicate qubit found"); + } + } + if (block.getArguments().size() != numTargets) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 621 - 686, The PowOp::verify() method currently only rejects duplicate unitary operations but allows non-unitary quantum side effects like alloc, measure, reset, and qtensor mutations to exist in the body region before the unitary. Strengthen the verification loop that starts with the ++iter iterator (which currently checks for extra UnitaryOpInterface operations) to also reject any non-unitary quantum side effects in the body, mirroring the approach used in InvOp. Additionally, add validation to reject duplicate input qubits by ensuring each block argument used by the body unitary is unique and matches the corresponding target argument without duplication.mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp (1)
101-112: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
InvPowToNegPowcan drop required preamble defs and create dangling uses.Line 108 inlines only
innerPow’s body. If that body uses values defined earlier ininvOp’s body, those producer ops are erased withinvOp, leaving invalid IR.🔧 Proposed fix
const double exponent = innerPow.getExponentValue(); // Remap InvOp's block arguments to the outer qubits they alias, // since the inlined ops may reference them and InvOp is about to be erased. for (auto [blockArg, outerQubit] : llvm::zip_equal(invOp.getBody()->getArguments(), invOp.getQubits())) { rewriter.replaceAllUsesWith(blockArg, outerQubit); } + + // Preserve non-unitary preamble ops that may define values captured by the + // nested pow body. + for (auto& bodyOp : llvm::make_early_inc_range(*invOp.getBody())) { + if (&bodyOp != innerPow.getOperation() && !isa<YieldOp>(bodyOp)) { + rewriter.moveOpBefore(&bodyOp, invOp); + } + } rewriter.replaceOpWithNewOp<PowOp>(invOp, -exponent, [&] { auto* powBody = rewriter.getInsertionBlock(); rewriter.inlineBlockBefore(innerPow.getBody(), powBody, powBody->begin()); rewriter.eraseOp(&powBody->back()); // erase the inlined YieldOp });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp` around lines 101 - 112, The issue is that when inlining only innerPow.getBody() into the new PowOp, any preamble operations that define values used by innerPow's body will be lost when invOp is erased. Fix this by identifying and inlining any preamble definitions from invOp's body (operations that come before the innerPow operation and produce values used by innerPow) into the new PowOp before inlining innerPow.getBody(). This ensures that dependent definitions are preserved and not dangling references are created.mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp (1)
555-580: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winTighten
PowOp::verifyto reject non-unitary quantum ops in the body.Line 557 validates constant exponent, but the verifier still allows side-effecting non-unitary quantum ops in the preamble. That violates modifier legality and can make canonicalizations unsafe when they restructure body contents.
🔧 Proposed fix
LogicalResult PowOp::verify() { FloatAttr attr; if (!matchPattern(getExponent(), m_Constant(&attr))) { return emitOpError("exponent must be a constant"); } auto& block = *getBody(); + if (llvm::any_of(block, [](Operation& op) { + return isa<AllocOp, DeallocOp, MeasureOp, ResetOp>(op); + })) { + return emitOpError("body must not contain non-unitary quantum operations"); + } + if (block.getOperations().size() < 2) { return emitOpError("body region must have at least two operations"); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp` around lines 555 - 580, The verification logic in PowOp::verify currently only rejects unitary operations in the preamble (the loop starting with `for (auto it = ++iter; it != block.rend(); ++it)`), but it does not reject non-unitary quantum operations that would violate modifier legality. Enhance the loop that iterates through preamble operations to add an additional check that rejects non-unitary quantum ops alongside the existing rejection of unitary ops, ensuring only operations that are neither unitary quantum ops nor side-effecting quantum ops are permitted in the body before the required unitary operation.mlir/lib/Conversion/QCOToQC/QCOToQC.cpp (1)
725-748: 🩺 Stability & Availability | 🔴 CriticalUse
moveRegionwith type converter for block argument conversion inqco.powpattern.
cloneRegionBeforepreserves theqco.powentry block argument types (!qco.qubit), but line 742 replaces them withadaptor.getQubitsIn()which contains!qc.qubitvalues (post-type-conversion from the OpAdaptor). This creates a type mismatch. TheConvertQCOCtrlOpandConvertQCOInvOppatterns already usemoveRegionwithgetTypeConverter()to properly convert block argument types before RAUW. Apply the same approach here to ensure type safety.Proposed fix
- // Clone body region from QCO to QC - auto& dstRegion = qcOp.getRegion(); - rewriter.cloneRegionBefore(op.getRegion(), dstRegion, dstRegion.end()); - - auto& entryBlock = dstRegion.front(); - const auto numArgs = entryBlock.getNumArguments(); + auto& srcEntryBlock = op.getRegion().front(); + const auto numArgs = srcEntryBlock.getNumArguments(); if (adaptor.getQubitsIn().size() != numArgs) { return op.emitOpError() << "qco.pow: entry block args (" << numArgs << ") must match number of target operands (" << adaptor.getQubitsIn().size() << ")"; } + + // Move body region from QCO to QC and convert block args before RAUW. + auto& dstRegion = qcOp.getRegion(); + if (failed(moveRegion(op.getRegion(), dstRegion, rewriter, + getTypeConverter()))) { + return failure(); + } + auto& entryBlock = dstRegion.front(); // Remove all block arguments in the cloned region rewriter.modifyOpInPlace(qcOp, [&] {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp` around lines 725 - 748, The `cloneRegionBefore` call in the qco.pow pattern conversion preserves the original `!qco.qubit` block argument types, but then replaces them with `!qc.qubit` values from the adaptor, causing a type mismatch. Instead of using `cloneRegionBefore` followed by manual argument replacement, use `moveRegion` with `getTypeConverter()` to properly convert the block argument types during the region move, similar to how `ConvertQCOCtrlOp` and `ConvertQCOInvOp` patterns handle this conversion. This will ensure the block arguments are converted from `!qco.qubit` to `!qc.qubit` types automatically and safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h`:
- Around line 552-553: The example documentation for the `qc.ctrl` operation in
the QCProgramBuilder.h file is missing the `targets` keyword in the syntax.
Update the example that currently shows `qc.ctrl(%q0) (%a0 = %q1)` to include
the `targets` keyword so it reads `qc.ctrl(%q0) targets(%a0 = %q1)` to align
with the current API documentation and prevent confusion for users copying the
example.
In `@mlir/include/mlir/Dialect/Utils/Utils.h`:
- Around line 251-257: The function getBodyUnitary calls std::next to advance an
iterator by i positions before checking bounds, which results in undefined
behavior since std::next performs no bounds validation. To fix this, manually
iterate through the filtered unitaries range while counting elements, checking
if the count reaches i and if iteration completes without reaching the target
index. Return the unitary operation when the count matches i, or call
llvm::reportFatalUsageError if iteration completes without finding the i-th
unitary.
In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 127-138: The inlineRegion function currently inlines regions and
then attempts to replace block arguments with type-converted adaptor values,
violating MLIR's same-type RAUW invariant when the adaptor values have different
types than the original block arguments. Modify inlineRegion to use the type
converter to convert the block argument types before performing the
replaceAllUsesWith operation, following the pattern used by moveRegion from
ConversionUtils.h. Change the function signature to return LogicalResult instead
of void to properly communicate conversion failures. Update all call sites of
inlineRegion (in the ForOp pattern around line 822, WhileOp pattern around lines
868-870, and IfOp pattern around lines 913-918) to handle the returned
LogicalResult.
In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp`:
- Around line 403-430: The InvOp::verify() method dereferences
block.getTerminator() without first validating that the body is non-empty and
that the terminator is specifically a YieldOp. Before line 426 where
getTerminator() is called, add a verification check to ensure the terminator
exists and is a YieldOp instance, returning an appropriate emitOpError if either
condition fails. This will prevent null dereferences and reject malformed IR
that contains non-yield terminators.
- Around line 343-355: The EraseEmptyInv struct's matchAndRewrite method removes
InvOp operations without checking if they are nested as the sole body unitary
within another modifier, which violates modifier body constraints. Before the
current check on op.getNumBodyUnitaries() in matchAndRewrite, add logic to
detect if the InvOp is nested inside a parent modifier body and handle it
accordingly: for single-target nested cases, replace with an IdOp instead of
removing it; for multi-target nested cases, return failure to prevent the
rewrite. This approach should mirror the handling used in the pow(0) pattern to
maintain parent modifier validity.
- Around line 448-517: The hasCompileTimeKnownUnitaryMatrix() function returns
true for cases where getUnitaryMatrix() returns std::nullopt, causing
inconsistency. Modify the hasCompileTimeKnownUnitaryMatrix() predicate to
include the same conditional checks that getUnitaryMatrix() implements: it
should verify that getNumBodyUnitaries() returns zero, or that there is a sole
body unitary available, or that getNumTargets() equals one (for the composition
case). This ensures the predicate only returns true when getUnitaryMatrix() will
successfully return a matrix rather than std::nullopt.
---
Outside diff comments:
In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 725-748: The `cloneRegionBefore` call in the qco.pow pattern
conversion preserves the original `!qco.qubit` block argument types, but then
replaces them with `!qc.qubit` values from the adaptor, causing a type mismatch.
Instead of using `cloneRegionBefore` followed by manual argument replacement,
use `moveRegion` with `getTypeConverter()` to properly convert the block
argument types during the region move, similar to how `ConvertQCOCtrlOp` and
`ConvertQCOInvOp` patterns handle this conversion. This will ensure the block
arguments are converted from `!qco.qubit` to `!qc.qubit` types automatically and
safely.
In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp`:
- Around line 101-112: The issue is that when inlining only innerPow.getBody()
into the new PowOp, any preamble operations that define values used by
innerPow's body will be lost when invOp is erased. Fix this by identifying and
inlining any preamble definitions from invOp's body (operations that come before
the innerPow operation and produce values used by innerPow) into the new PowOp
before inlining innerPow.getBody(). This ensures that dependent definitions are
preserved and not dangling references are created.
In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp`:
- Around line 555-580: The verification logic in PowOp::verify currently only
rejects unitary operations in the preamble (the loop starting with `for (auto it
= ++iter; it != block.rend(); ++it)`), but it does not reject non-unitary
quantum operations that would violate modifier legality. Enhance the loop that
iterates through preamble operations to add an additional check that rejects
non-unitary quantum ops alongside the existing rejection of unitary ops,
ensuring only operations that are neither unitary quantum ops nor side-effecting
quantum ops are permitted in the body before the required unitary operation.
In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 694-724: The hasCompileTimeKnownUnitaryMatrix() method does not
check the exponent value, causing it to return true even when getUnitaryMatrix()
returns nullopt for unsupported exponents. Fix this inconsistency by adding
exponent validation to hasCompileTimeKnownUnitaryMatrix(): use
getExponentValue() to retrieve the exponent and check that it matches one of the
supported cases (approximately equal to 0 or 1 using TOLERANCE comparisons),
ensuring the predicate only returns true when getUnitaryMatrix() can actually
produce a result.
- Around line 621-686: The PowOp::verify() method currently only rejects
duplicate unitary operations but allows non-unitary quantum side effects like
alloc, measure, reset, and qtensor mutations to exist in the body region before
the unitary. Strengthen the verification loop that starts with the ++iter
iterator (which currently checks for extra UnitaryOpInterface operations) to
also reject any non-unitary quantum side effects in the body, mirroring the
approach used in InvOp. Additionally, add validation to reject duplicate input
qubits by ensuring each block argument used by the body unitary is unique and
matches the corresponding target argument without duplication.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 10c356a3-8858-4c5a-8f0f-1d7dd0b4f96c
📒 Files selected for processing (23)
CHANGELOG.mdmlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.hmlir/include/mlir/Dialect/QC/IR/QCOps.tdmlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.hmlir/include/mlir/Dialect/QCO/IR/QCOOps.tdmlir/include/mlir/Dialect/Utils/Utils.hmlir/lib/Conversion/QCOToQC/QCOToQC.cppmlir/lib/Conversion/QCToQCO/QCToQCO.cppmlir/lib/Dialect/QC/Builder/QCProgramBuilder.cppmlir/lib/Dialect/QC/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QC/IR/Modifiers/PowOp.cppmlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cppmlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cppmlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cppmlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cppmlir/unittests/Dialect/QC/IR/test_qc_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cppmlir/unittests/programs/qc_programs.cppmlir/unittests/programs/qc_programs.hmlir/unittests/programs/qco_programs.cppmlir/unittests/programs/qco_programs.h
💤 Files with no reviewable changes (9)
- mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
- mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
- mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
- mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
- mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
- mlir/unittests/programs/qc_programs.h
- mlir/unittests/programs/qco_programs.h
- mlir/unittests/programs/qco_programs.cpp
- mlir/unittests/programs/qc_programs.cpp
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp (2)
694-724: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winMake the matrix-availability predicate match
getUnitaryMatrix().Line 695 returns true for
pow(2.0) { x }becausexhas a known matrix, but lines 723-724 still returnstd::nulloptfor every non-0/1 exponent. GatehasCompileTimeKnownUnitaryMatrix()on the exponent cases actually implemented, or implement the general power computation.🛠️ Minimal predicate fix
bool PowOp::hasCompileTimeKnownUnitaryMatrix() { - return all_of(getBody()->getOps<UnitaryOpInterface>(), - [](UnitaryOpInterface op) { - return op.hasCompileTimeKnownUnitaryMatrix(); - }); + const double p = getExponentValue(); + if (std::abs(p) >= TOLERANCE && std::abs(p - 1.0) >= TOLERANCE) { + return false; + } + auto bodyUnitary = utils::getSoleBodyUnitary<UnitaryOpInterface>(*getBody()); + return bodyUnitary && bodyUnitary.hasCompileTimeKnownUnitaryMatrix(); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 694 - 724, The hasCompileTimeKnownUnitaryMatrix() method does not check the exponent value, causing it to return true even when getUnitaryMatrix() returns nullopt for unsupported exponents. Fix this inconsistency by adding exponent validation to hasCompileTimeKnownUnitaryMatrix(): use getExponentValue() to retrieve the exponent and check that it matches one of the supported cases (approximately equal to 0 or 1 using TOLERANCE comparisons), ensuring the predicate only returns true when getUnitaryMatrix() can actually produce a result.
621-686: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winReject non-unitary quantum side effects in
PowOp::verify.Line 657 only rejects extra unitary ops, so a
powbody can still containalloc/measure/reset/qtensor mutation before the sole unitary and be treated as a valid power modifier. Mirror theInvOpguard here, and reject duplicate input qubits while tightening the verifier.🛡️ Suggested verifier tightening
LogicalResult PowOp::verify() { FloatAttr attr; if (!matchPattern(getExponent(), m_Constant(&attr))) { return emitOpError("exponent must be a constant"); } auto& block = *getBody(); + if (llvm::any_of(block, [](Operation& nested) { + return isa<AllocOp, SinkOp, MeasureOp, ResetOp, qtensor::ExtractOp, + qtensor::InsertOp>(nested); + })) { + return emitOpError("body must not contain non-unitary quantum operations " + "or modify a quantum register"); + } + if (block.getOperations().size() < 2) { return emitOpError("body region must have at least two operations"); } const auto numTargets = getNumTargets(); + SmallPtrSet<Value, 4> uniqueQubitsIn; + for (Value target : getQubitsIn()) { + if (!uniqueQubitsIn.insert(target).second) { + return emitOpError("duplicate qubit found"); + } + } + if (block.getArguments().size() != numTargets) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp` around lines 621 - 686, The PowOp::verify() method currently only rejects duplicate unitary operations but allows non-unitary quantum side effects like alloc, measure, reset, and qtensor mutations to exist in the body region before the unitary. Strengthen the verification loop that starts with the ++iter iterator (which currently checks for extra UnitaryOpInterface operations) to also reject any non-unitary quantum side effects in the body, mirroring the approach used in InvOp. Additionally, add validation to reject duplicate input qubits by ensuring each block argument used by the body unitary is unique and matches the corresponding target argument without duplication.mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp (1)
101-112: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
InvPowToNegPowcan drop required preamble defs and create dangling uses.Line 108 inlines only
innerPow’s body. If that body uses values defined earlier ininvOp’s body, those producer ops are erased withinvOp, leaving invalid IR.🔧 Proposed fix
const double exponent = innerPow.getExponentValue(); // Remap InvOp's block arguments to the outer qubits they alias, // since the inlined ops may reference them and InvOp is about to be erased. for (auto [blockArg, outerQubit] : llvm::zip_equal(invOp.getBody()->getArguments(), invOp.getQubits())) { rewriter.replaceAllUsesWith(blockArg, outerQubit); } + + // Preserve non-unitary preamble ops that may define values captured by the + // nested pow body. + for (auto& bodyOp : llvm::make_early_inc_range(*invOp.getBody())) { + if (&bodyOp != innerPow.getOperation() && !isa<YieldOp>(bodyOp)) { + rewriter.moveOpBefore(&bodyOp, invOp); + } + } rewriter.replaceOpWithNewOp<PowOp>(invOp, -exponent, [&] { auto* powBody = rewriter.getInsertionBlock(); rewriter.inlineBlockBefore(innerPow.getBody(), powBody, powBody->begin()); rewriter.eraseOp(&powBody->back()); // erase the inlined YieldOp });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp` around lines 101 - 112, The issue is that when inlining only innerPow.getBody() into the new PowOp, any preamble operations that define values used by innerPow's body will be lost when invOp is erased. Fix this by identifying and inlining any preamble definitions from invOp's body (operations that come before the innerPow operation and produce values used by innerPow) into the new PowOp before inlining innerPow.getBody(). This ensures that dependent definitions are preserved and not dangling references are created.mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp (1)
555-580: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winTighten
PowOp::verifyto reject non-unitary quantum ops in the body.Line 557 validates constant exponent, but the verifier still allows side-effecting non-unitary quantum ops in the preamble. That violates modifier legality and can make canonicalizations unsafe when they restructure body contents.
🔧 Proposed fix
LogicalResult PowOp::verify() { FloatAttr attr; if (!matchPattern(getExponent(), m_Constant(&attr))) { return emitOpError("exponent must be a constant"); } auto& block = *getBody(); + if (llvm::any_of(block, [](Operation& op) { + return isa<AllocOp, DeallocOp, MeasureOp, ResetOp>(op); + })) { + return emitOpError("body must not contain non-unitary quantum operations"); + } + if (block.getOperations().size() < 2) { return emitOpError("body region must have at least two operations"); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp` around lines 555 - 580, The verification logic in PowOp::verify currently only rejects unitary operations in the preamble (the loop starting with `for (auto it = ++iter; it != block.rend(); ++it)`), but it does not reject non-unitary quantum operations that would violate modifier legality. Enhance the loop that iterates through preamble operations to add an additional check that rejects non-unitary quantum ops alongside the existing rejection of unitary ops, ensuring only operations that are neither unitary quantum ops nor side-effecting quantum ops are permitted in the body before the required unitary operation.mlir/lib/Conversion/QCOToQC/QCOToQC.cpp (1)
725-748: 🩺 Stability & Availability | 🔴 CriticalUse
moveRegionwith type converter for block argument conversion inqco.powpattern.
cloneRegionBeforepreserves theqco.powentry block argument types (!qco.qubit), but line 742 replaces them withadaptor.getQubitsIn()which contains!qc.qubitvalues (post-type-conversion from the OpAdaptor). This creates a type mismatch. TheConvertQCOCtrlOpandConvertQCOInvOppatterns already usemoveRegionwithgetTypeConverter()to properly convert block argument types before RAUW. Apply the same approach here to ensure type safety.Proposed fix
- // Clone body region from QCO to QC - auto& dstRegion = qcOp.getRegion(); - rewriter.cloneRegionBefore(op.getRegion(), dstRegion, dstRegion.end()); - - auto& entryBlock = dstRegion.front(); - const auto numArgs = entryBlock.getNumArguments(); + auto& srcEntryBlock = op.getRegion().front(); + const auto numArgs = srcEntryBlock.getNumArguments(); if (adaptor.getQubitsIn().size() != numArgs) { return op.emitOpError() << "qco.pow: entry block args (" << numArgs << ") must match number of target operands (" << adaptor.getQubitsIn().size() << ")"; } + + // Move body region from QCO to QC and convert block args before RAUW. + auto& dstRegion = qcOp.getRegion(); + if (failed(moveRegion(op.getRegion(), dstRegion, rewriter, + getTypeConverter()))) { + return failure(); + } + auto& entryBlock = dstRegion.front(); // Remove all block arguments in the cloned region rewriter.modifyOpInPlace(qcOp, [&] {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp` around lines 725 - 748, The `cloneRegionBefore` call in the qco.pow pattern conversion preserves the original `!qco.qubit` block argument types, but then replaces them with `!qc.qubit` values from the adaptor, causing a type mismatch. Instead of using `cloneRegionBefore` followed by manual argument replacement, use `moveRegion` with `getTypeConverter()` to properly convert the block argument types during the region move, similar to how `ConvertQCOCtrlOp` and `ConvertQCOInvOp` patterns handle this conversion. This will ensure the block arguments are converted from `!qco.qubit` to `!qc.qubit` types automatically and safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h`:
- Around line 552-553: The example documentation for the `qc.ctrl` operation in
the QCProgramBuilder.h file is missing the `targets` keyword in the syntax.
Update the example that currently shows `qc.ctrl(%q0) (%a0 = %q1)` to include
the `targets` keyword so it reads `qc.ctrl(%q0) targets(%a0 = %q1)` to align
with the current API documentation and prevent confusion for users copying the
example.
In `@mlir/include/mlir/Dialect/Utils/Utils.h`:
- Around line 251-257: The function getBodyUnitary calls std::next to advance an
iterator by i positions before checking bounds, which results in undefined
behavior since std::next performs no bounds validation. To fix this, manually
iterate through the filtered unitaries range while counting elements, checking
if the count reaches i and if iteration completes without reaching the target
index. Return the unitary operation when the count matches i, or call
llvm::reportFatalUsageError if iteration completes without finding the i-th
unitary.
In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 127-138: The inlineRegion function currently inlines regions and
then attempts to replace block arguments with type-converted adaptor values,
violating MLIR's same-type RAUW invariant when the adaptor values have different
types than the original block arguments. Modify inlineRegion to use the type
converter to convert the block argument types before performing the
replaceAllUsesWith operation, following the pattern used by moveRegion from
ConversionUtils.h. Change the function signature to return LogicalResult instead
of void to properly communicate conversion failures. Update all call sites of
inlineRegion (in the ForOp pattern around line 822, WhileOp pattern around lines
868-870, and IfOp pattern around lines 913-918) to handle the returned
LogicalResult.
In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp`:
- Around line 403-430: The InvOp::verify() method dereferences
block.getTerminator() without first validating that the body is non-empty and
that the terminator is specifically a YieldOp. Before line 426 where
getTerminator() is called, add a verification check to ensure the terminator
exists and is a YieldOp instance, returning an appropriate emitOpError if either
condition fails. This will prevent null dereferences and reject malformed IR
that contains non-yield terminators.
- Around line 343-355: The EraseEmptyInv struct's matchAndRewrite method removes
InvOp operations without checking if they are nested as the sole body unitary
within another modifier, which violates modifier body constraints. Before the
current check on op.getNumBodyUnitaries() in matchAndRewrite, add logic to
detect if the InvOp is nested inside a parent modifier body and handle it
accordingly: for single-target nested cases, replace with an IdOp instead of
removing it; for multi-target nested cases, return failure to prevent the
rewrite. This approach should mirror the handling used in the pow(0) pattern to
maintain parent modifier validity.
- Around line 448-517: The hasCompileTimeKnownUnitaryMatrix() function returns
true for cases where getUnitaryMatrix() returns std::nullopt, causing
inconsistency. Modify the hasCompileTimeKnownUnitaryMatrix() predicate to
include the same conditional checks that getUnitaryMatrix() implements: it
should verify that getNumBodyUnitaries() returns zero, or that there is a sole
body unitary available, or that getNumTargets() equals one (for the composition
case). This ensures the predicate only returns true when getUnitaryMatrix() will
successfully return a matrix rather than std::nullopt.
---
Outside diff comments:
In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 725-748: The `cloneRegionBefore` call in the qco.pow pattern
conversion preserves the original `!qco.qubit` block argument types, but then
replaces them with `!qc.qubit` values from the adaptor, causing a type mismatch.
Instead of using `cloneRegionBefore` followed by manual argument replacement,
use `moveRegion` with `getTypeConverter()` to properly convert the block
argument types during the region move, similar to how `ConvertQCOCtrlOp` and
`ConvertQCOInvOp` patterns handle this conversion. This will ensure the block
arguments are converted from `!qco.qubit` to `!qc.qubit` types automatically and
safely.
In `@mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp`:
- Around line 101-112: The issue is that when inlining only innerPow.getBody()
into the new PowOp, any preamble operations that define values used by
innerPow's body will be lost when invOp is erased. Fix this by identifying and
inlining any preamble definitions from invOp's body (operations that come before
the innerPow operation and produce values used by innerPow) into the new PowOp
before inlining innerPow.getBody(). This ensures that dependent definitions are
preserved and not dangling references are created.
In `@mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp`:
- Around line 555-580: The verification logic in PowOp::verify currently only
rejects unitary operations in the preamble (the loop starting with `for (auto it
= ++iter; it != block.rend(); ++it)`), but it does not reject non-unitary
quantum operations that would violate modifier legality. Enhance the loop that
iterates through preamble operations to add an additional check that rejects
non-unitary quantum ops alongside the existing rejection of unitary ops,
ensuring only operations that are neither unitary quantum ops nor side-effecting
quantum ops are permitted in the body before the required unitary operation.
In `@mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp`:
- Around line 694-724: The hasCompileTimeKnownUnitaryMatrix() method does not
check the exponent value, causing it to return true even when getUnitaryMatrix()
returns nullopt for unsupported exponents. Fix this inconsistency by adding
exponent validation to hasCompileTimeKnownUnitaryMatrix(): use
getExponentValue() to retrieve the exponent and check that it matches one of the
supported cases (approximately equal to 0 or 1 using TOLERANCE comparisons),
ensuring the predicate only returns true when getUnitaryMatrix() can actually
produce a result.
- Around line 621-686: The PowOp::verify() method currently only rejects
duplicate unitary operations but allows non-unitary quantum side effects like
alloc, measure, reset, and qtensor mutations to exist in the body region before
the unitary. Strengthen the verification loop that starts with the ++iter
iterator (which currently checks for extra UnitaryOpInterface operations) to
also reject any non-unitary quantum side effects in the body, mirroring the
approach used in InvOp. Additionally, add validation to reject duplicate input
qubits by ensuring each block argument used by the body unitary is unique and
matches the corresponding target argument without duplication.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 10c356a3-8858-4c5a-8f0f-1d7dd0b4f96c
📒 Files selected for processing (23)
CHANGELOG.mdmlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.hmlir/include/mlir/Dialect/QC/IR/QCOps.tdmlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.hmlir/include/mlir/Dialect/QCO/IR/QCOOps.tdmlir/include/mlir/Dialect/Utils/Utils.hmlir/lib/Conversion/QCOToQC/QCOToQC.cppmlir/lib/Conversion/QCToQCO/QCToQCO.cppmlir/lib/Dialect/QC/Builder/QCProgramBuilder.cppmlir/lib/Dialect/QC/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QC/IR/Modifiers/PowOp.cppmlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cppmlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cppmlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cppmlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cppmlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cppmlir/unittests/Dialect/QC/IR/test_qc_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cppmlir/unittests/programs/qc_programs.cppmlir/unittests/programs/qc_programs.hmlir/unittests/programs/qco_programs.cppmlir/unittests/programs/qco_programs.h
💤 Files with no reviewable changes (9)
- mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
- mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
- mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
- mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
- mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
- mlir/unittests/programs/qc_programs.h
- mlir/unittests/programs/qco_programs.h
- mlir/unittests/programs/qco_programs.cpp
- mlir/unittests/programs/qc_programs.cpp
🛑 Comments failed to post (6)
mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h (1)
552-553: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix inconsistent
ctrlMLIR example syntax.This example is missing the
targetskeyword (qc.ctrl(%q0) (%a0 = %q1)), while the updated API/docs elsewhere usetargets(...). Please align it to prevent copy/paste confusion.Suggested doc fix
- * qc.ctrl(%q0) (%a0 = %q1) { \ + * qc.ctrl(%q0) targets(%a0 = %q1) { \📝 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.* qc.ctrl(%q0) targets(%a0 = %q1) { \ * qc.OP_NAME(%PARAM1, %PARAM2) %a0 : !qc.qubit \🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h` around lines 552 - 553, The example documentation for the `qc.ctrl` operation in the QCProgramBuilder.h file is missing the `targets` keyword in the syntax. Update the example that currently shows `qc.ctrl(%q0) (%a0 = %q1)` to include the `targets` keyword so it reads `qc.ctrl(%q0) targets(%a0 = %q1)` to align with the current API documentation and prevent confusion for users copying the example.mlir/include/mlir/Dialect/Utils/Utils.h (1)
251-257: 🩺 Stability & Availability | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail FILE="$(fd -p 'Utils.h' mlir/include | head -n1)" echo "Inspecting: $FILE" nl -ba "$FILE" | sed -n '245,262p' echo echo "Searching for similar unchecked std::next patterns:" rg -n --type=cpp 'std::next\([^,]+,\s*static_cast<std::ptrdiff_t>\(i\)\)' mlir/include/mlir/DialectRepository: munich-quantum-toolkit/core
Length of output: 264
🏁 Script executed:
#!/bin/bash set -euo pipefail # Find the correct file echo "=== Finding Utils.h in mlir/include/mlir/Dialect/Utils ===" find mlir/include/mlir/Dialect/Utils -name "Utils.h" 2>/dev/null || echo "File not found with find" echo echo "=== Listing files in mlir/include/mlir/Dialect/Utils ===" ls -la mlir/include/mlir/Dialect/Utils/ 2>/dev/null || echo "Directory not found" echo echo "=== Using git ls-files to find Utils.h ===" git ls-files | grep -E "mlir/include.*Utils\.h$" | head -20Repository: munich-quantum-toolkit/core
Length of output: 762
🏁 Script executed:
#!/bin/bash set -euo pipefail # Once we find the correct file, read lines 245-262 FILE="mlir/include/mlir/Dialect/Utils/Utils.h" if [ -f "$FILE" ]; then echo "=== Content of $FILE (lines 245-262) ===" cat -n "$FILE" | sed -n '245,262p' echo echo "=== Full context around getBodyUnitary function ===" cat -n "$FILE" | sed -n '240,270p' else echo "File $FILE not found" fiRepository: munich-quantum-toolkit/core
Length of output: 2538
🏁 Script executed:
#!/bin/bash set -euo pipefail # Search for std::next usage with iterator patterns echo "=== Searching for std::next patterns in MLIR ===" rg -n 'std::next' mlir/include/mlir/Dialect/Utils/ --type=cpp --type=hpp 2>/dev/null || echo "No matches found" echo echo "=== Checking for llvm::make_filter_range usage ===" rg -n 'make_filter_range' mlir/include/ --type=cpp --type=hpp -A 3 -B 1 | head -50Repository: munich-quantum-toolkit/core
Length of output: 311
🏁 Script executed:
#!/bin/bash set -euo pipefail FILE="mlir/include/mlir/Dialect/Utils/Utils.h" echo "=== Checking header guard ===" head -5 "$FILE" echo echo "=== Checking for `#pragma` once ===" grep -n "pragma once" "$FILE" || echo "No `#pragma` once found" echo echo "=== Verifying surrounding code context ===" cat -n "$FILE" | sed -n '1,20p'Repository: munich-quantum-toolkit/core
Length of output: 1095
🏁 Script executed:
#!/bin/bash set -euo pipefail # Search for similar std::next patterns that might have the same issue echo "=== Searching for std::next with distance in entire mlir/include ===" rg -n 'std::next\s*\(' mlir/include --type cpp -A 1 -B 1 | head -80 echo echo "=== Checking LLVM FilterRange in LLVM headers ===" find mlir -name "*.h" -o -name "*.hpp" | xargs grep -l "make_filter_range" 2>/dev/null | head -5Repository: munich-quantum-toolkit/core
Length of output: 679
🌐 Web query:
std::next iterator undefined behavior C++ standard forward iterator bounds checking💡 Result:
In C++, std::next is defined in terms of std::advance [1][2]. The C++ standard does not require std::next to perform bounds checking [3][4]. Consequently, if you use std::next to advance an iterator beyond the valid range (e.g., past the end of a container), it results in undefined behavior [3][4]. The standard library's iterator operations assume that the caller ensures the resulting iterator remains within the bounds of a valid range (specifically, that every iterator between the start and the destination, inclusive, is non-singular and valid) [5][4]. Because there is no internal bounds checking, std::next will simply perform the underlying increment operations, leading to undefined behavior if those operations violate iterator requirements [3][4]. While the C++ standard does not provide bounds checking for std::next, some specific implementations or compilation modes may provide optional "checked" iterators that trigger runtime assertions or errors in debug builds [6][7]. However, these are not guaranteed by the standard and typically do not exist in release builds to maintain performance [7]. If you need to safely advance an iterator while respecting container bounds, you should either: 1. Check the distance manually (e.g., using std::distance or container size) before calling std::next [3]. 2. Use C++20's std::ranges::next, which provides overloads that accept a sentinel (such as end) to safely bound the advancement [8][9]. This allows you to advance an iterator without exceeding the specified limit [8][10].
Citations:
- 1: https://cppreference.dev/w/cpp/iterator/next
- 2: https://dev.cppreference.com/cpp/iterator/next
- 3: https://stackoverflow.com/questions/38305559/stdnext-with-n-stddistanceit-c-end
- 4: https://stackoverflow.com/questions/5916239/stdadvance-behavior-when-advancing-beyond-end-of-container
- 5: https://eel.is/c++draft/iterator.requirements.general
- 6: https://learn.microsoft.com/en-us/cpp/standard-library/checked-iterators?view=msvc-170
- 7: https://stackoverflow.com/questions/8864429/bound-check-for-iterator-in-release-mode-c
- 8: https://cppreference.dev/w/cpp/iterator/ranges/next
- 9: https://eel.is/c++draft/iterator.primitives
- 10: https://en.cppreference.com/cpp/iterator/ranges/next
Avoid undefined iterator advancement in
getBodyUnitary.At line 254,
std::next(unitaries.begin(), i)can advance pastend()before the bounds check executes, resulting in undefined behavior. Per the C++ standard,std::nextperforms no bounds checking—advancing an iterator beyond its valid range is undefined behavior regardless of subsequent condition checks.🔧 Proposed fix
template <typename UnitaryInterface> [[nodiscard]] UnitaryInterface getBodyUnitary(Block& block, size_t i) { auto unitaries = llvm::make_filter_range( block, [](Operation& op) { return isa<UnitaryInterface>(op); }); - auto it = std::next(unitaries.begin(), static_cast<std::ptrdiff_t>(i)); + auto it = unitaries.begin(); + for (size_t n = 0; n < i && it != unitaries.end(); ++n) { + ++it; + } if (it == unitaries.end()) { llvm::reportFatalUsageError("Unitary index out of bounds"); } return cast<UnitaryInterface>(*it); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/include/mlir/Dialect/Utils/Utils.h` around lines 251 - 257, The function getBodyUnitary calls std::next to advance an iterator by i positions before checking bounds, which results in undefined behavior since std::next performs no bounds validation. To fix this, manually iterate through the filtered unitaries range while counting elements, checking if the count reaches i and if iteration completes without reaching the target index. Return the unitary operation when the count matches i, or call llvm::reportFatalUsageError if iteration completes without finding the i-th unitary.mlir/lib/Conversion/QCOToQC/QCOToQC.cpp (1)
127-138: 🩺 Stability & Availability | 🔴 Critical
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash # First, verify the file exists and read the function at the specified lines if [ -f "mlir/lib/Conversion/QCOToQC/QCOToQC.cpp" ]; then echo "=== File found ===" wc -l mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== Function inlineRegion (lines 127-138) ===" sed -n '127,145p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp else echo "File not found" fiRepository: munich-quantum-toolkit/core
Length of output: 1082
🏁 Script executed:
#!/bin/bash # Search for the moveRegion function mentioned in the diff echo "=== Searching for moveRegion ===" fd -t f "\.cpp$|\.h$" mlir/ | xargs rg "moveRegion" -B 2 -A 5 | head -50Repository: munich-quantum-toolkit/core
Length of output: 3926
🏁 Script executed:
#!/bin/bash # Find the other call sites mentioned (821-823, 867-871, 912-919) echo "=== Checking call sites in the file ===" sed -n '821,823p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" sed -n '867,871p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" sed -n '912,919p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 1082
🏁 Script executed:
#!/bin/bash # Search for all calls to inlineRegion in the file echo "=== All calls to inlineRegion ===" rg "inlineRegion\s*\(" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -B 2 -A 2Repository: munich-quantum-toolkit/core
Length of output: 1373
🏁 Script executed:
#!/bin/bash # Search for moveRegion in QCOToQC.cpp specifically echo "=== Searching for moveRegion in QCOToQC.cpp ===" rg "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "Exit code: $?" echo "" # Check the full context around inlineRegion calls to understand type conversion echo "=== Checking context around line 821 ===" sed -n '810,830p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" # Check class context - see if this is a pattern rewriter echo "=== Searching for class context and pattern definition ===" sed -n '1,50p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 2527
🏁 Script executed:
#!/bin/bash # Look for type conversion or adaptor usage in the region operations echo "=== Searching for adaptor usage around inlineRegion calls ===" rg -B 5 "inlineRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | head -80Repository: munich-quantum-toolkit/core
Length of output: 1985
🏁 Script executed:
#!/bin/bash # Check the QCOToJeff moveRegion implementation to understand the pattern echo "=== Full moveRegion implementation in QCOToJeff.cpp ===" rg "static LogicalResult moveRegion" mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp -A 30Repository: munich-quantum-toolkit/core
Length of output: 1421
🏁 Script executed:
#!/bin/bash # Check if there's any type conversion or adaptor in QCOToQC patterns echo "=== Searching for TypeConverter usage in QCOToQC.cpp ===" rg "TypeConverter\|getTypeConverter" mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 222
🏁 Script executed:
#!/bin/bash # Find where moveRegion is called in QCOToQC.cpp echo "=== Full context of moveRegion calls in QCOToQC.cpp ===" rg "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -B 10 -A 3Repository: munich-quantum-toolkit/core
Length of output: 1306
🏁 Script executed:
#!/bin/bash # Check if moveRegion is defined in QCOToQC.cpp echo "=== Search for moveRegion definition ===" rg "static.*moveRegion\|LogicalResult.*moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -A 15Repository: munich-quantum-toolkit/core
Length of output: 206
🏁 Script executed:
#!/bin/bash # Look at the complete ForOp pattern to understand type conversion concern echo "=== Complete ForOp pattern class ===" sed -n '809,850p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 1554
🏁 Script executed:
#!/bin/bash # Check if there's any type conversion happening with adaptor values echo "=== Adaptor type context in patterns ===" rg "struct.*Adaptor\|class.*Adaptor\|OpAdaptor" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -A 5 | head -50Repository: munich-quantum-toolkit/core
Length of output: 206
🏁 Script executed:
#!/bin/bash # Check the return type and error handling in current inlineRegion to see if it's supposed to fail echo "=== Checking if void return is problematic ===" sed -n '120,160p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 1959
🏁 Script executed:
#!/bin/bash # Find all function definitions in QCOToQC.cpp echo "=== All function definitions ===" ast-grep outline mlir/lib/Conversion/QCOToQC/QCOToQC.cpp --view expanded | grep -A 2 "moveRegion\|inlineRegion"Repository: munich-quantum-toolkit/core
Length of output: 198
🏁 Script executed:
#!/bin/bash # Search more broadly for moveRegion in the file echo "=== All occurrences of moveRegion with context ===" grep -n "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 364
🏁 Script executed:
#!/bin/bash # Check line counts around the moveRegion usages - are they before or after inlineRegion? echo "=== Lines in QCOToQC.cpp ===" wc -l mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== Lines with moveRegion ===" grep -n "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== Lines with inlineRegion ===" grep -n "inlineRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 979
🏁 Script executed:
#!/bin/bash # Check if moveRegion is a helper lambda or a separate function echo "=== Context around first moveRegion call (around line 1001) ===" sed -n '995,1005p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 840
🏁 Script executed:
#!/bin/bash # Look for any function that might be moveRegion before the first call echo "=== Search in the area before first moveRegion call ===" sed -n '100,200p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | grep -A 5 -B 5 "moveRegion\|inlineRegion"Repository: munich-quantum-toolkit/core
Length of output: 1097
🏁 Script executed:
#!/bin/bash # Look at the moveRegion calls with full context echo "=== moveRegion call at line 649 ===" sed -n '640,660p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== moveRegion call at line 687 ===" sed -n '678,698p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 1537
🏁 Script executed:
#!/bin/bash # Find the moveRegion function definition - it must be defined somewhere echo "=== Search for moveRegion definition in entire file ===" sed -n '1,700p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | grep -B 5 -A 30 "moveRegion"Repository: munich-quantum-toolkit/core
Length of output: 1756
🏁 Script executed:
#!/bin/bash # Check if it's imported from a header echo "=== Check headers for moveRegion ===" head -50 mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | grep -i "include" echo "" echo "=== Search for moveRegion in header files ===" find mlir/lib/Conversion -name "*.h" -exec grep -l "moveRegion" {} \;Repository: munich-quantum-toolkit/core
Length of output: 1282
🏁 Script executed:
#!/bin/bash # Let's look at the full section from line 640-700 to see the complete pattern echo "=== Complete pattern around moveRegion calls ===" sed -n '630,700p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 2146
🏁 Script executed:
#!/bin/bash # Search for moveRegion definition anywhere in the codebase echo "=== Search for moveRegion definition across all files ===" rg "^\s*auto moveRegion\s*=|^\s*static.*LogicalResult moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -A 20Repository: munich-quantum-toolkit/core
Length of output: 223
🏁 Script executed:
#!/bin/bash # Check if moveRegion might be a lambda defined in the pass structure echo "=== Check the full pass structure ===" sed -n '1040,1098p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 2742
🏁 Script executed:
#!/antml:bash # Search backwards from first moveRegion call to find definition echo "=== Search in lines 1-648 for moveRegion definition ===" sed -n '1,648p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | tail -100 | grep -B 20 -A 5 "moveRegion\|^static"Repository: munich-quantum-toolkit/core
Length of output: 221
🏁 Script executed:
#!/bin/bash # Let's check the include file that defines the pass echo "=== Check QCOToQC.h ===" find mlir -name "QCOToQC.h" -exec cat {} \;Repository: munich-quantum-toolkit/core
Length of output: 660
🏁 Script executed:
#!/bin/bash # Search more carefully - moveRegion might be defined as a member function of the pass echo "=== Full context of the pass class definition ===" sed -n '950,1098p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | head -150Repository: munich-quantum-toolkit/core
Length of output: 6008
🏁 Script executed:
#!/bin/bash # Search the entire repository for moveRegion definition echo "=== All moveRegion definitions in the repo ===" rg "static.*LogicalResult.*moveRegion\|auto\s+moveRegion\s*=" --type cpp --type h -B 2 -A 15Repository: munich-quantum-toolkit/core
Length of output: 212
🏁 Script executed:
#!/antml:bash # Check around the ConvertQCOCtrlOp and ConvertQCOInvOp to understand what moveRegion they're calling echo "=== Full ConvertQCOCtrlOp struct ===" sed -n '628,666p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 1303
🏁 Script executed:
#!/bin/bash # Check if this conversion is actually compiling - moveRegion must be defined somewhere echo "=== Check if file builds - look for compilation artifacts ===" find . -name "CMakeLists.txt" -path "*/QCOToQC*" -exec cat {} \;Repository: munich-quantum-toolkit/core
Length of output: 1922
🏁 Script executed:
#!/bin/bash # Search all files in Conversion directory for moveRegion echo "=== All references to moveRegion in Conversion directory ===" rg "moveRegion" mlir/lib/Conversion/ --type cpp --type h | head -30Repository: munich-quantum-toolkit/core
Length of output: 1595
🏁 Script executed:
#!/bin/bash # This is key - moveRegion is called in QCOToQC.cpp but not defined. Let's verify this is a real issue echo "=== Check if QCOToQC.cpp compiles or if moveRegion is missing ===" rg "moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -B 5 -A 2 | head -60 echo "" echo "=== Check for static moveRegion in QCOToQC.cpp ===" rg "static.*moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "Exit code: $?" echo "" echo "=== Check all helper functions defined in QCOToQC.cpp before the patterns ===" sed -n '100,650p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | grep -B 2 -A 10 "^static\|^auto"Repository: munich-quantum-toolkit/core
Length of output: 2299
🏁 Script executed:
#!/bin/bash # Check QCToQCO.cpp to see if it has the same issue echo "=== Check QCToQCO.cpp for moveRegion definition ===" rg "static.*moveRegion\|LogicalResult moveRegion" mlir/lib/Conversion/QCToQCO/QCToQCO.cpp -A 15Repository: munich-quantum-toolkit/core
Length of output: 217
🏁 Script executed:
#!/bin/bash # Let's check the ConversionUtils header to see if moveRegion is defined there echo "=== Check ConversionUtils.h for moveRegion ===" find mlir -name "ConversionUtils.h" -exec cat {} \; | grep -B 2 -A 15 "moveRegion"Repository: munich-quantum-toolkit/core
Length of output: 851
🏁 Script executed:
#!/bin/bash # Check if there's a common header that defines moveRegion echo "=== Check all includes in QCOToQC.cpp that might have moveRegion ===" head -40 mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== Look for moveRegion in those header files ===" find mlir/Conversion -name "*.h" | xargs grep -l "moveRegion" 2>/dev/nullRepository: munich-quantum-toolkit/core
Length of output: 1589
🏁 Script executed:
#!/bin/bash # Get the full ConversionUtils.h definition to understand moveRegion echo "=== Full ConversionUtils.h content ===" find mlir -name "ConversionUtils.h" -type f -exec cat {} \;Repository: munich-quantum-toolkit/core
Length of output: 1910
🏁 Script executed:
#!/bin/bash # Now verify the concern - check if SCF patterns using inlineRegion with adaptor values echo "=== Check ForOp pattern with adaptor.getInitArgs() ===" sed -n '809,835p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== Check WhileOp pattern with adaptor values ===" sed -n '854,876p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== Check IfOp pattern with adaptor values ===" sed -n '900,925p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 3098
🏁 Script executed:
#!/bin/bash # Verify that CtrlOp and InvOp already use moveRegion successfully echo "=== CtrlOp using moveRegion ===" sed -n '641,664p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "=== InvOp using moveRegion ===" sed -n '679,702p' mlir/lib/Conversion/QCOToQC/QCOToQC.cppRepository: munich-quantum-toolkit/core
Length of output: 1535
🏁 Script executed:
#!/bin/bash # Verify that adaptor values are indeed type-converted # Check how OpAdaptor works with type conversion in MLIR patterns echo "=== Understanding the issue: adaptor provides converted types ===" echo "In SCF patterns:" echo "- op.getInitArgs() returns QCO-typed values (original operation)" echo "- adaptor.getInitArgs() returns QC-typed values (type-converted)" echo "" echo "When inlineRegion is called with adaptor values:" echo "- Block args in inlined region still have QCO types" echo "- Then they're replaced with QC-typed adaptor values" echo "- This violates MLIR's same-type RAUW invariant" echo "" echo "=== Confirm the pattern is used correctly elsewhere ===" rg "inlineRegion\|moveRegion" mlir/lib/Conversion/QCOToQC/QCOToQC.cpp -B 3 | grep -A 3 "inlineRegion\|moveRegion" | head -100Repository: munich-quantum-toolkit/core
Length of output: 638
🏁 Script executed:
#!/bin/bash # Check if there are any compile errors or test failures related to this echo "=== Check test file to understand expected behavior ===" find mlir -path "*/test*" -name "*qco_to_qc*" -type f | head -5Repository: munich-quantum-toolkit/core
Length of output: 274
🏁 Script executed:
#!/bin/bash # Verify return type consistency - inlineRegion is void but moveRegion returns LogicalResult echo "=== Function signatures comparison ===" echo "inlineRegion signature:" sed -n '127,129p' mlir/lib/Conversion/QCOToQC/QCOToQC.cpp echo "" echo "moveRegion signature (from ConversionUtils.h):" echo "inline LogicalResult moveRegion(...)" echo "" echo "This means the proposed fix needs to:" echo "1. Change inlineRegion to return LogicalResult" echo "2. Update all callers to handle the return value"Repository: munich-quantum-toolkit/core
Length of output: 657
Update SCF region inline patterns to convert block argument types before RAUW.
inlineRegion(lines 127-138) inlines regions with original block-argument types, then replaces those args using type-converted adaptor values. This violates MLIR's same-type RAUW invariant when adaptor values have different types (e.g., QC-typed) than the block arguments (e.g., QCO-typed).The SCF conversion patterns (ForOp at 822, WhileOp at 868-870, IfOp at 913-918) pass type-converted adaptor values to
inlineRegion, triggering this issue. However, the QCO control-flow patterns (CtrlOp, InvOp) correctly usemoveRegionfromConversionUtils.hto convert block signatures during the inline operation, avoiding the invariant violation.Change
inlineRegionto use the type converter to convert block argument signatures before argument replacement, similar to themoveRegionpattern already in use. Update the function signature to returnLogicalResultand modify all call sites to handle the result.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp` around lines 127 - 138, The inlineRegion function currently inlines regions and then attempts to replace block arguments with type-converted adaptor values, violating MLIR's same-type RAUW invariant when the adaptor values have different types than the original block arguments. Modify inlineRegion to use the type converter to convert the block argument types before performing the replaceAllUsesWith operation, following the pattern used by moveRegion from ConversionUtils.h. Change the function signature to return LogicalResult instead of void to properly communicate conversion failures. Update all call sites of inlineRegion (in the ForOp pattern around line 822, WhileOp pattern around lines 868-870, and IfOp pattern around lines 913-918) to handle the returned LogicalResult.mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp (3)
343-355: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve parent modifier validity when erasing empty
inv.Line 354 removes the
InvOpeven when it is the sole body unitary of another modifier, leaving the parent body with onlyqco.yield. Use anIdOpfor the single-target nested case and bail for multi-target nested identity, matching thepow(0)handling.🛡️ Suggested nested-modifier guard
- rewriter.replaceOp(op, op.getOperands()); + if (isa<CtrlOp, InvOp, PowOp>(op->getParentOp())) { + if (op.getNumTargets() != 1) { + return failure(); + } + rewriter.replaceOpWithNewOp<IdOp>(op, op.getInputQubit(0)); + } else { + rewriter.replaceOp(op, op.getOperands()); + } return success(); } };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp` around lines 343 - 355, The EraseEmptyInv struct's matchAndRewrite method removes InvOp operations without checking if they are nested as the sole body unitary within another modifier, which violates modifier body constraints. Before the current check on op.getNumBodyUnitaries() in matchAndRewrite, add logic to detect if the InvOp is nested inside a parent modifier body and handle it accordingly: for single-target nested cases, replace with an IdOp instead of removing it; for multi-target nested cases, return failure to prevent the rewrite. This approach should mirror the handling used in the pow(0) pattern to maintain parent modifier validity.
403-430: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Require a
YieldOpbefore reading terminator operands.Line 426 dereferences
block.getTerminator()without proving the body is non-empty and yield-terminated. Malformed IR should get a verifier diagnostic instead of a null dereference or a non-yield “terminator” being accepted.🛠️ Suggested verifier guard
- auto* blockTerminator = block.getTerminator(); - if (const auto numYieldOperands = blockTerminator->getNumOperands(); + if (block.empty() || !isa<YieldOp>(block.back())) { + return emitOpError( + "last operation in body region must be a yield operation"); + } + auto yieldOp = cast<YieldOp>(block.back()); + if (const auto numYieldOperands = yieldOp->getNumOperands(); numYieldOperands != numTargets) { return emitOpError("yield operation must yield ") << numTargets << " values, but found " << numYieldOperands;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp` around lines 403 - 430, The InvOp::verify() method dereferences block.getTerminator() without first validating that the body is non-empty and that the terminator is specifically a YieldOp. Before line 426 where getTerminator() is called, add a verification check to ensure the terminator exists and is a YieldOp instance, returning an appropriate emitOpError if either condition fails. This will prevent null dereferences and reject malformed IR that contains non-yield terminators.
448-517: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Constrain matrix availability to the cases
getUnitaryMatrix()implements.Line 449 returns true for multi-target bodies with multiple known unitaries, but lines 514-515 return
std::nulloptfor those cases. Include the same zero/sole-unitary/single-target-composition conditions in the predicate.🛠️ Minimal predicate alignment
bool InvOp::hasCompileTimeKnownUnitaryMatrix() { + if (getNumBodyUnitaries() == 0) { + return true; + } + if (utils::getSoleBodyUnitary<UnitaryOpInterface>(*getBody())) { + return all_of(getBody()->getOps<UnitaryOpInterface>(), + [](UnitaryOpInterface op) { + return op.hasCompileTimeKnownUnitaryMatrix(); + }); + } + if (getNumTargets() != 1) { + return false; + } return all_of(getBody()->getOps<UnitaryOpInterface>(), [](UnitaryOpInterface op) { return op.hasCompileTimeKnownUnitaryMatrix(); }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp` around lines 448 - 517, The hasCompileTimeKnownUnitaryMatrix() function returns true for cases where getUnitaryMatrix() returns std::nullopt, causing inconsistency. Modify the hasCompileTimeKnownUnitaryMatrix() predicate to include the same conditional checks that getUnitaryMatrix() implements: it should verify that getNumBodyUnitaries() returns zero, or that there is a sole body unitary available, or that getNumTargets() equals one (for the composition case). This ensures the predicate only returns true when getUnitaryMatrix() will successfully return a matrix rather than std::nullopt.
Signed-off-by: J4MMlE <113241498+J4MMlE@users.noreply.github.com>
|
Tests pass, linter is happy, all comments addressed, refactored to match the new modifier constraints ... I guess this is getting close to being mergeable. Please have another look @burgholzer @denialhaag! |
denialhaag
left a comment
There was a problem hiding this comment.
Thanks a lot for rebasing this and pushing it forward, @J4MMlE! 🙂
I just pushed a commit that cleans up the docstrings of CtrlOp, InvOp, and PowOp. While reviewing your PR, I noticed that all of them had become out-of-date and out-of-sync, and I felt it didn't make sense to write 20 comments here. Let me know if the changes look good to you!
Apart from that, you can find some comments on the canonicalization patterns below. I believe they can still be improved a bit to work under all circumstances.
| using llvm::reportFatalUsageError; | ||
| using llvm::to_vector; |
There was a problem hiding this comment.
We usually don't do this, but we explicitly write the llvm::. 🤔
| static LogicalResult tryReplaceWithNamedPhaseGate(double angle, PowOp op, | ||
| PatternRewriter& rewriter) { |
There was a problem hiding this comment.
Maybe the function name should reflect that it is only relevant to the P gate. 🤔
| } | ||
| }; | ||
|
|
||
| /// pow(p) where p < 0 => pow(-p) { inv { g } } |
There was a problem hiding this comment.
| /// pow(p) where p < 0 => pow(-p) { inv { g } } | |
| /// pow(p) { g } with p < 0 => pow(-p) { inv { g } } |
| } | ||
| }; | ||
|
|
||
| /// pow(p) { ctrl(q, g) } => ctrl(q, pow(p, g)) |
There was a problem hiding this comment.
Something like this feels a little more consistent with your usual notation. 🤔
| /// pow(p) { ctrl(q, g) } => ctrl(q, pow(p, g)) | |
| /// pow(p) { ctrl(q) {g} } => ctrl(q) { pow(p) { g } } |
| const double merged = op.getExponentValue() * innerPow.getExponentValue(); | ||
| auto mergedConst = arith::ConstantFloatOp::create( | ||
| rewriter, op.getLoc(), rewriter.getF64Type(), APFloat(merged)); |
There was a problem hiding this comment.
This should use your scaleByExponent helper so that the pattern also works if the exponents are not constants.
There was a problem hiding this comment.
See my comments in QCO/IR/Modifiers/InvOp.cpp (some of them are further below). Most (if not all) should also apply here.
Generally speaking, all patterns can (and should) look more or less identical in QC and QCO now that we use block arguments in both dialects. The QC ones will be slightly simpler because no values need to be yielded.
Furthermore, it might also make sense to extract some functions (e.g., tryReplaceWithNamedPhaseGate and tryReplaceWithNamedPhaseGate) into a common helper.
| double PowOp::getExponentValue() { | ||
| FloatAttr attr; | ||
| if (!matchPattern(getExponent(), m_Constant(&attr))) { | ||
| reportFatalUsageError("PowOp exponent must be a constant"); | ||
| } | ||
| return attr.getValueAsDouble(); | ||
| } |
| * Downstream patterns (e.g., `NegPowToInvPow`) can then rewrite | ||
| * `pow(-p){U} => pow(p){inv(U)}` when the exponent is an integer. | ||
| */ | ||
| struct InvPowToNegPow final : OpRewritePattern<InvOp> { |
There was a problem hiding this comment.
Two comments here:
- Right now, this only works if the exponent is a constant. We could generalize this by using
arithoperations here. - Shouldn't this also use
utils::getValueFromBlockArgument(see your implementation inQC/IR/Modifiers/InvOp.cpp)?
| * Downstream patterns (e.g., `NegPowToInvPow`) can then rewrite | ||
| * `pow(-p){U} => pow(p){inv(U)}` when the exponent is an integer. | ||
| */ | ||
| struct InvPowToNegPow final : OpRewritePattern<InvOp> { |
There was a problem hiding this comment.
Right now, this only works if the exponent is a constant. We could generalize this by using arith operations here.
| rewriter.moveOpBefore(innerPow, op); | ||
| rewriter.modifyOpInPlace(innerPow, [&]() { | ||
| innerPow.getExponentMutable().assign(mergedConst.getResult()); | ||
| innerPow.getQubitsInMutable().assign(op.getInputQubits()); | ||
| }); | ||
| rewriter.replaceOp(op, innerPow->getResults()); |
There was a problem hiding this comment.
This could break if the input qubits of the two power modifiers do not have the same order. You can use utils::getValueFromBlockArgument to resolve this. Generally speaking, you can probably largely mirror the implementation of MergeNestedCtrl:
core/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp
Lines 45 to 100 in 95e7e05
Description
Adds a power modifier (
pow) to both MLIR-dialectsQCandQCO.The power modifier raises an operation
Uby an integer/floating numberkpow(k) { U } == U^kin other words the corresponding unitary matrix gets raised by factorkvia eigendecomposition:U = V D V† => U^p = V D^p V†.When
kis an integerU^kcould also be unrolled (Uis then appliedk-times to the input qubit - but this modifier does NOT implement any unrolling - this is left for (possibly) later stages in the pipeline).QC
QCO
Canonicalizations
The modifier contains the following canonicalizations in both dialects:
pow(a) { pow(b) { U } } → pow(a*b) { U }pow(1) { U } → Upow(0) { U } → remove (becomes identity, then eliminated)pow(-r) { U } → pow(r) { inv { U } }pow { ctrl { U } } → ctrl { pow { U } }inv:inv { pow { U } } -> pow { inf { U } }Gate specific canonicalizations
pow(r) { G(θ, ...) } → G(r·θ, ...)G in {GPhase, RX, RY, RZ, P, RXX, RYY, RZX, RZZ, R(θ,φ), XX+YY(θ,β), XX-YY(θ,β)}P-Gates:Z=P(π),S=P(π/2),Sdg=P(-π/2),T=P(π/4),Tdg=P(-π/4):pow(r) { G }→ named gate for normalizedr*base_angle, elseP(r*base_angle)All gates are implicitly converted and scaled as a P gate i.e.
G^r = P(r*base_angle). Then we check ifr*base_angleis again a base angle of a named gate: matchr*base_anglewith:0 → erase,±π → Z,π/2 → S,-π/2 → Sdg,π/4 → T,-π/4 → TdgX/Y:pow(r) { X/Y } → gphase(-r·π/2); RX/RY(r·π)Xonly:r=½ → SX,r=-½ → SXdgSX/SXdg— symmetric pair:|r| = 2 → Xpow(r) { SX } → gphase(-r·π/4); RX( r·π/2)pow(r) { SXdg } → gphase(+r·π/4); RX(-r·π/2)pow(n) { H / ECR / SWAP } → erasefor even exponentelse
→ H / ECR / SWAPfor odd exponentiSWAPpow(r) { iSWAP } → XX+YY(-r·π, 0)pow(r) { Id / Barrier } → Id / BarrierNotes
jeffdialectInvOpandPowOpis duplicated (extracting operations, reordering, etc. is basically the same) - maybe its a good idea to unify by creating utility functions, which all modifiers can use - this might be only worth doing, if more modifiers would be implemented (which is unlikely).pow(r) { inv { U }}topow(-r) { U }we leave it as is and instead convert the other way around (see Negative Power Canonicalization)Related Issues
Fixes #1129
Fixes #1131
Checklist
I have added migration instructions to the upgrade guide (if needed).If PR contains AI-assisted content:
Assisted-by: [Model Name] via [Tool Name]footer.