Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
eee1742
✨ Add an `unroll-modifiers` pass for multi-operation modifiers
denialhaag Aug 6, 2026
0cf9047
Address the Rabbit's comments
denialhaag Aug 6, 2026
7707cba
Reject StaticOps in modifier bodies
denialhaag Aug 6, 2026
61cb3d0
Clean up a bit
denialhaag Aug 7, 2026
9ff2e9d
Log modifiers that cannot be unrolled
denialhaag Aug 7, 2026
050511d
Fix linter errors
denialhaag Aug 7, 2026
491e802
Share the unrolled ctrl and inv programs
denialhaag Aug 7, 2026
a52566b
Fix typo
denialhaag Aug 7, 2026
9cb5201
Update changelog
denialhaag Aug 7, 2026
c505ade
Merge branch 'main' into unroll-modifiers
denialhaag Aug 9, 2026
d043d9b
✅ Reject qubit captures in QCO modifiers
burgholzer Aug 10, 2026
65d12fc
🎨 Follow fixed-width integer style
burgholzer Aug 10, 2026
7170cea
📝 Document QCO modifier capture rules
burgholzer Aug 10, 2026
ac7b14e
Clean up namespace qualifiers
denialhaag Aug 10, 2026
31794d4
Merge remote-tracking branch 'origin/main' into unroll-modifiers
denialhaag Aug 10, 2026
3f9ce38
Unroll pow modifiers acting on disjoint qubits
denialhaag Aug 10, 2026
6a2a6f8
Implement RegionBranchOpInterface for QC and QCO modifiers
denialhaag Aug 10, 2026
b0cba26
Fix dangling values when inlining modifier bodies
denialhaag Aug 10, 2026
8559d15
Revert "Implement RegionBranchOpInterface for QC and QCO modifiers"
denialhaag Aug 10, 2026
216524b
Add canonicalization patterns for removing unused qubit block arguments
denialhaag Aug 10, 2026
da18dbc
Merge remote-tracking branch 'origin/main' into unroll-modifiers
denialhaag Aug 10, 2026
b76d5ac
Fix linter errors
denialhaag Aug 10, 2026
c3665b5
Add interface targets for the shared MLIR test headers
denialhaag Aug 10, 2026
67be78c
Merge remote-tracking branch 'origin/main' into unroll-modifiers
denialhaag Aug 10, 2026
18192a8
Merge origin/main into PR #2015 remediation branch
burgholzer Aug 10, 2026
8f34843
Reduce unrelated test namespace churn
burgholzer Aug 10, 2026
1f27829
Construct narrowed modifiers during unrolling
burgholzer Aug 10, 2026
2901113
Reject nested non-unitary QCO modifier bodies
burgholzer Aug 10, 2026
cc2169a
Preserve QCO wire order when folding powers
burgholzer Aug 10, 2026
be8d32a
Avoid leaking QIR control state past no-op bodies
burgholzer Aug 10, 2026
889041b
Assert unroll-modifier structure before cleanup
burgholzer Aug 10, 2026
c5e5815
Format modifier remediation changes
burgholzer Aug 10, 2026
01e4c8e
Clean up modifier remediation includes
burgholzer Aug 10, 2026
3701773
Merge current main into PR #2015 remediation branch
burgholzer Aug 10, 2026
93c8cda
Preserve QCO wire order across all power folds
burgholzer Aug 10, 2026
6344d70
Merge remote-tracking branch 'origin/main' into unroll-modifiers
denialhaag Aug 11, 2026
5d77e5d
Clean up a bit
denialhaag Aug 11, 2026
69e60fe
Fix linter errors
denialhaag Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ struct LoweringState {
DenseSet<Operation*> returnedStaticResults;

/// Modifier information
size_t inCtrlOp = 0;
bool inCtrlOp = false;
SmallVector<Value> controls;

/// Allocator and StringSaver for stable StringRefs
Expand Down
19 changes: 19 additions & 0 deletions mlir/include/mlir/Dialect/Utils/Transforms/Passes.td
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,23 @@ def NormalizeGlobalPhases : Pass<"normalize-global-phases", "mlir::ModuleOp"> {
}];
}

def UnrollModifiers : Pass<"unroll-modifiers", "mlir::ModuleOp"> {
let dependentDialects = ["mlir::qc::QCDialect", "mlir::qco::QCODialect"];
let summary = "Unroll multi-operation modifiers into single-operation ones";
let description = [{
Splits `ctrl` and `inv` modifiers whose body holds more than one unitary
operation into a sequence of modifiers that each hold a single operation.
The targets of the new modifiers are narrowed to the qubits of the
respective operation. For `inv`, the order of the operations is reversed.
Classical operations of the body are moved in front of the new modifiers.

If a modifier cannot be unrolled, the modifier is left untouched. Such
modifiers are skipped silently; the pass never fails.

In addition, `pow` modifiers are left untouched because `pow(r) { a; b }`
is generally not equivalent to `pow(r) { a }; pow(r) { b }`. Modifiers
nested in a `pow` body are still unrolled.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This feels a bit too narrow. The cases where this simplification holds are well known. IIRC, this holds for (positive?) integer powers. That should be easy to verify though with an LLM.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Unless I'm missing something fundamental right now, pow(r) { a; b } = pow(r) { a }; pow(r) { b } requires at least that a and b commute, no? This should already be a simple enough counterexample: $(XY)^2 = XYXY \neq XXYY = X^2Y^2$, where $X$ and $Y$ are the respective Pauli matrices.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I guess we can always expand pow(r) { a; b } if a and b are acting on different qubits, as they definitely commute then. 🤔 Should be easy enough to check.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ah. I was definitely looking at this too quickly. You are right, and I would not overcomplicate the current implementation with Commutation Analysis.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I encountered a related problem while extending MergeSingleQubitRotationGates to fixed gates in #1639.

For the OpenQASM program

gate hs q {
  h q;
  s q;
}

pow(2) @ hs q;

the merge pass turns the h; s body into a u operation plus a separate gphase. This is unitary-equivalent outside a modifier, but inside pow it changes the body from

pow(2) { h; s }

to roughly

pow(2) { gphase(φ); u(...) }

That currently breaks the standard QC-to-QIR pipeline because the resulting multi-operation pow cannot be legalized.

For an integer exponent, the phase could theoretically be handled with

pow(n) { gphase(φ); U } = gphase(nφ); pow(n) { U }

but that would require an explicitly Pow-aware global-phase rewrite, as it is not the same as distributing the power over arbitrary body operations.

Is that something that should be taken care of in this PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

3f9ce38 adds a pattern that unrolls a PowOp if all body operations are disjoint.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This probably supersedes #2041, could you check if that is similar to what is covered by your pattern?

@denialhaag denialhaag Aug 10, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The implementation here only splits pow(2) { gphase(..); u(...) } into pow(2) { gphase(..) }; pow(2) { u(...) }, but it doesn't then inline pow(2) { u(...) }. If I'm not missing something right now, I still see the value in #2041. 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Okay great, then I will keep that open 👍

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 AI text below 🤖

Implemented the bounded version at 93c8cdabd: unroll-modifiers splits only body operations whose qubit sets are disjoint. Overlapping operations and non-integral powers remain untouched; no commutation analysis or general pow @ u synthesis was added. Structural tests assert both the split and preservation cases before cleanup.

}];
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

#endif // MLIR_DIALECT_UTILS_TRANSFORMS_PASSES_TD
8 changes: 7 additions & 1 deletion mlir/lib/Compiler/Programs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "mlir/Dialect/QCO/Transforms/Passes.h"
#include "mlir/Dialect/QTensor/IR/QTensorDialect.h"
#include "mlir/Dialect/Utils/Transforms/GlobalPhaseNormalization.h"
#include "mlir/Dialect/Utils/Transforms/Passes.h"
#include "mlir/Support/Passes.h"

#include <capnp/common.h>
Expand Down Expand Up @@ -333,6 +334,7 @@ std::optional<QIRProgram> QCProgram::intoQIR(const QIRProfile profile) && {
if (failed(runPasses(
mod(),
[profile](OpPassManager& pm) {
pm.addPass(mqt::createUnrollModifiers());
if (profile == QIRProfile::Adaptive) {
pm.addPass(createQCToQIRAdaptive());
} else {
Expand Down Expand Up @@ -467,7 +469,11 @@ std::optional<QCProgram> QCOProgram::intoQC() && {

std::optional<JeffProgram> QCOProgram::intoJeff() && {
if (failed(runPasses(
mod(), [](OpPassManager& pm) { pm.addPass(createQCOToJeff()); },
mod(),
[](OpPassManager& pm) {
pm.addPass(mqt::createUnrollModifiers());
pm.addPass(createQCOToJeff());
},
"failed to convert QCO to jeff"))) {
return std::nullopt;
}
Expand Down
26 changes: 14 additions & 12 deletions mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,8 @@ convertUnitaryToCallOp(QCOpType& op, QCOpAdaptorType& adaptor,
LoweringState& state, StringRef fnName,
size_t numTargets, size_t numParams) {
// Query state for modifier information
const auto inCtrlOp = state.inCtrlOp;
const SmallVector<Value> controls =
inCtrlOp != 0 ? state.controls : SmallVector<Value>{};
state.inCtrlOp ? state.controls : SmallVector<Value>{};
const size_t numCtrls = controls.size();

// Define argument types
Expand Down Expand Up @@ -134,11 +133,9 @@ convertUnitaryToCallOp(QCOpType& op, QCOpAdaptorType& adaptor,
operands.append(adaptor.getOperands().begin(), adaptor.getOperands().end());

// Clean up modifier information
if (inCtrlOp != 0) {
state.inCtrlOp--;
if (state.inCtrlOp == 0) {
state.controls.clear();
}
if (state.inCtrlOp) {
state.inCtrlOp = false;
state.controls.clear();
}

// Replace operation with CallOp
Expand Down Expand Up @@ -242,8 +239,7 @@ struct ConvertQCUnitaryOpQIR : StatefulOpConversionPattern<OpType> {
matchAndRewrite(OpType op, OpType::Adaptor adaptor,
ConversionPatternRewriter& rewriter) const override {
auto& state = this->getState();
const auto inCtrlOp = state.inCtrlOp;
const size_t numCtrls = inCtrlOp != 0 ? state.controls.size() : 0;
const size_t numCtrls = state.inCtrlOp ? state.controls.size() : 0;
const auto fnName = GetFnName(numCtrls);
return convertUnitaryToCallOp(op, adaptor, rewriter, this->getContext(),
state, fnName, NumTargets, NumParams);
Expand Down Expand Up @@ -325,7 +321,7 @@ struct ConvertQCGPhaseOp final : StatefulOpConversionPattern<GPhaseOp> {
matchAndRewrite(GPhaseOp op, OpAdaptor adaptor,
ConversionPatternRewriter& rewriter) const override {
auto& state = getState();
if (state.inCtrlOp != 0) {
if (state.inCtrlOp) {
return op.emitError("Controlled GPhaseOps cannot be converted to QIR");
}
return convertUnitaryToCallOp(op, adaptor, rewriter, getContext(), state,
Expand Down Expand Up @@ -360,13 +356,19 @@ struct ConvertQCCtrlOp final : StatefulOpConversionPattern<CtrlOp> {
ConversionPatternRewriter& rewriter) const override {
auto& state = getState();

if (state.inCtrlOp != 0) {
if (state.inCtrlOp) {
return rewriter.notifyMatchFailure(op,
"Nested CtrlOps are not supported");
}

if (op.getNumBodyUnitaries() > 1) {
return rewriter.notifyMatchFailure(
op, "CtrlOps with multiple body unitaries are not supported. Run the "
"unroll-modifiers pass before the conversion");
}

// Update modifier information
state.inCtrlOp = op.getNumBodyUnitaries();
state.inCtrlOp = true;
state.controls = llvm::to_vector(adaptor.getControls());

// Inline block and remove operation
Expand Down
4 changes: 2 additions & 2 deletions mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ namespace mlir::qc::detail {
LogicalResult verifyModifierBody(Operation* modifierOp, Block& body) {
const auto hasNonUnitaryOperation =
body.walk([](Operation* operation) {
return isa<AllocOp, DeallocOp, MeasureOp, ResetOp, memref::LoadOp,
memref::StoreOp>(operation)
return isa<AllocOp, DeallocOp, StaticOp, MeasureOp, ResetOp,
memref::LoadOp, memref::StoreOp>(operation)
? WalkResult::interrupt()
: WalkResult::advance();
})
Expand Down
4 changes: 2 additions & 2 deletions mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,8 @@ void CtrlOp::build(OpBuilder& odsBuilder, OperationState& odsState,
LogicalResult CtrlOp::verify() {
auto& block = *getBody();
if (llvm::any_of(block, [](Operation& op) {
return isa<AllocOp, SinkOp, MeasureOp, ResetOp, qtensor::ExtractOp,
qtensor::InsertOp>(op);
return isa<AllocOp, SinkOp, StaticOp, MeasureOp, ResetOp,
qtensor::ExtractOp, qtensor::InsertOp>(op);
})) {
return emitOpError("body must not contain non-unitary quantum operations "
"or modify a quantum register");
Expand Down
4 changes: 2 additions & 2 deletions mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -442,8 +442,8 @@ void InvOp::build(OpBuilder& odsBuilder, OperationState& odsState, Value qubit,
LogicalResult InvOp::verify() {
auto& block = *getBody();
if (llvm::any_of(block, [](Operation& op) {
return isa<AllocOp, SinkOp, MeasureOp, ResetOp, qtensor::ExtractOp,
qtensor::InsertOp>(op);
return isa<AllocOp, SinkOp, StaticOp, MeasureOp, ResetOp,
qtensor::ExtractOp, qtensor::InsertOp>(op);
})) {
return emitOpError("body must not contain non-unitary quantum operations "
"or modify a quantum register");
Expand Down
4 changes: 2 additions & 2 deletions mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -670,8 +670,8 @@ void PowOp::build(OpBuilder& odsBuilder, OperationState& odsState, Value qubit,
LogicalResult PowOp::verify() {
auto& block = *getBody();
if (llvm::any_of(block, [](Operation& op) {
return isa<AllocOp, SinkOp, MeasureOp, ResetOp, qtensor::ExtractOp,
qtensor::InsertOp>(op);
return isa<AllocOp, SinkOp, StaticOp, MeasureOp, ResetOp,
qtensor::ExtractOp, qtensor::InsertOp>(op);
})) {
return emitOpError("body must not contain non-unitary quantum operations "
"or modify a quantum register");
Expand Down
1 change: 1 addition & 0 deletions mlir/lib/Dialect/Utils/Transforms/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
add_mlir_library(
MLIRMQTTransforms
NormalizeGlobalPhases.cpp
UnrollModifiers.cpp
ADDITIONAL_HEADER_DIRS
${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/Utils/Transforms
LINK_LIBS
Expand Down
Loading
Loading