Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bec1fdf
✨ Implement `decompose-multi-controlled` pass for decomposing multi-c…
simon1hofmann Jun 17, 2026
d48be9c
✨ Extend `decompose-multi-controlled` pass to support decomposition o…
simon1hofmann Jun 24, 2026
f0de507
📝 Update copyright year in QCO dialect files to 2025 and remove outda…
simon1hofmann Jun 24, 2026
08a9dfa
📝 Refine comments in McxSynthesis.cpp for clarity and update test des…
simon1hofmann Jun 24, 2026
d47641d
🚨 Fix linter warnings
simon1hofmann Jun 24, 2026
e0b53a4
📝 Update CHANGELOG.md to include pull request link for `decompose-mul…
simon1hofmann Jun 24, 2026
0ef3f71
☂️ Increase coverage
simon1hofmann Jun 24, 2026
dff6eeb
🚨 Fix linter warnings
simon1hofmann Jun 24, 2026
9ec9e6f
🐇 Address Rabbit's comments
Jun 24, 2026
cf57d47
✨ Add project options for MLIRQCOTransforms in CMakeLists.txt
Jun 24, 2026
0292b3e
🎨 pre-commit fixes
pre-commit-ci[bot] Jun 24, 2026
b2f30eb
☂️ Increase coverage
Jun 24, 2026
55176d1
🐇 Address Rabbit's comments
Jun 24, 2026
968d06c
Merge branch 'main' into decompose-multi-controlled-gates
simon1hofmann Jun 25, 2026
1351927
Merge remote-tracking branch 'origin/main' into decompose-multi-contr…
simon1hofmann Jun 30, 2026
0b297c5
Merge branch 'main' into decompose-multi-controlled-gates
simon1hofmann Jul 5, 2026
f782391
Merge branch 'main' into decompose-multi-controlled-gates
simon1hofmann Jul 6, 2026
0905bd3
Merge branch 'main' into decompose-multi-controlled-gates
simon1hofmann Jul 8, 2026
77b384b
✨ Enhance documentation for multi-controlled gate decomposition optio…
simon1hofmann Jul 8, 2026
735f8fd
✨ Introduce RCCX operation for three-qubit relative-phase CCX gate, i…
simon1hofmann Jul 8, 2026
78edde7
✨ Implement multi-controlled gate decomposition enhancements, introdu…
simon1hofmann Jul 8, 2026
48c29d7
Merge remote-tracking branch 'origin/main' into decompose-multi-contr…
simon1hofmann Jul 9, 2026
86f52b5
🔧 Refactor control operation lambda functions in MultiControlled.cpp
simon1hofmann Jul 9, 2026
dd067f2
✨ Introduce `addGadget` method to encapsulate gadget creation logic i…
simon1hofmann Jul 9, 2026
3a75a99
Merge remote-tracking branch 'origin/main' into decompose-multi-contr…
simon1hofmann Jul 9, 2026
495f55b
🔀 Merge main and adapt tests
simon1hofmann Jul 13, 2026
95da2b2
🔧 Update decomposition tests to use EXPECT_EQ for matrix comparison
simon1hofmann Jul 13, 2026
6539e1a
🚨 Fix linter warnings
simon1hofmann Jul 13, 2026
daa0f4a
🚨 Fix linter warnings
simon1hofmann Jul 13, 2026
559c5d7
🚨 Fix linter warnings
simon1hofmann Jul 13, 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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ releases may include breaking changes.
([#1887]) ([**@flowerthrower**], [**@burgholzer**])
- ✨ Add QIR Output Schemas support to the QIR runtime ([#1877])
([**@rturrado**])
- ✨ Add a `decompose-multi-controlled` pass for decomposing multi-controlled X
and Z gates into one- and two-qubit gates ([#1810]) ([**@simon1hofmann**])
- ✨ Add support for IQM's `move` gate in the QDMI Qiskit backend converter
([#1844], [#1848]) ([**@burgholzer**], [**@marcelwa**])
- 🚸 Add `const` version of the `CompoundOperation`'s `getOps()` function
([#1826]) ([**@ystade])
([#1826]) ([**@ystade**])
- 🐳 Add dev container configuration for consistent local development
environment ([#1786]) ([**@denialhaag**])
- ✨ Add two-qubit Weyl (KAK) decomposition and native-gateset synthesis support
Expand Down Expand Up @@ -648,6 +650,7 @@ changelogs._
[#1823]: https://github.com/munich-quantum-toolkit/core/pull/1823
[#1817]: https://github.com/munich-quantum-toolkit/core/pull/1817
[#1814]: https://github.com/munich-quantum-toolkit/core/pull/1814
[#1810]: https://github.com/munich-quantum-toolkit/core/pull/1810
[#1809]: https://github.com/munich-quantum-toolkit/core/pull/1809
[#1808]: https://github.com/munich-quantum-toolkit/core/pull/1808
[#1807]: https://github.com/munich-quantum-toolkit/core/pull/1807
Expand Down
15 changes: 15 additions & 0 deletions mlir/include/mlir/Compiler/CompilerPipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <mlir/Pass/PassManager.h>
#include <mlir/Support/LogicalResult.h>

#include <cstdint>
#include <string>

namespace mlir {
Expand Down Expand Up @@ -49,6 +50,20 @@ struct QuantumCompilerConfig {

/// Enable Hadamard lifting
bool enableHadamardLifting = false;

/// Decompose multi-controlled X/Z/phase gates through the full pass chain:
/// @c decompose-multi-controlled (k &ge; 4), @c decompose-three-controlled
/// (k = 3), and @c decompose-two-controlled (k = 2 and `qco.rccx`). @ref
/// decomposeMultiControlledMinControls governs when HP24 retains two- and
/// three-control building blocks versus expanding them inline. When enabled,
/// @ref decomposeMultiControlledMinControls must be at least 2; the pipeline
/// rejects smaller values before running any passes.
bool enableDecomposeMultiControlled = false;

/// Minimum control count for @ref enableDecomposeMultiControlled (default 2).
/// Must be at least 2 when decomposition is enabled; values below 2 cause
/// @ref QuantumCompilerPipeline::runPipeline to fail.
std::uint64_t decomposeMultiControlledMinControls = 2;
};

/**
Expand Down
11 changes: 11 additions & 0 deletions mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,17 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder {

#undef DECLARE_TWO_TARGET_TWO_PARAMETER

/**
* @brief Apply a relative-phase CCX (`RCCX`) gate to three qubits.
*
* @param qubit0 First control qubit (must be valid/unconsumed)
* @param qubit1 Second control qubit (must be valid/unconsumed)
* @param qubit2 Target qubit (must be valid/unconsumed)
* @return Output qubits `(qubit0, qubit1, qubit2)`
*/
std::tuple<Value, Value, Value> rccx(Value qubit0, Value qubit1,
Value qubit2);

// BarrierOp

/**
Expand Down
37 changes: 37 additions & 0 deletions mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ def OneTargetThreeParameter : TargetAndParameterArityTrait<1, 3>;
def TwoTargetZeroParameter : TargetAndParameterArityTrait<2, 0>;
def TwoTargetOneParameter : TargetAndParameterArityTrait<2, 1>;
def TwoTargetTwoParameter : TargetAndParameterArityTrait<2, 2>;
def ThreeTargetZeroParameter : TargetAndParameterArityTrait<3, 0>;

//===----------------------------------------------------------------------===//
// Unitary Operations
Expand Down Expand Up @@ -784,6 +785,42 @@ def DCXOp : QCOOp<"dcx",
let hasCanonicalizer = 1;
}

def RCCXOp
: QCOOp<"rccx", traits = [UnitaryOpInterface, ThreeTargetZeroParameter]> {
let summary = "Apply a relative-phase CCX gate to three qubits";
let description = [{
Applies a relative-phase Toffoli (`RCCX`) to three qubits and returns the
transformed qubits. The gate acts as a controlled @f$X@f$ on @f$q_2@f$
when @f$q_0@f$ and @f$q_1@f$ are @f$|1\rangle@f$, but may introduce a
non-trivial phase on @f$|111\rangle@f$ relative to a standard `CCX`.
This matches the `rccx` gate in OpenQASM 3 `qelib1.inc` and is commonly
used as a building block in multi-controlled @f$X@f$ decompositions.

Example:
```mlir
%c0_out, %c1_out, %t_out = qco.rccx %c0_in, %c1_in, %t_in
: !qco.qubit, !qco.qubit, !qco.qubit
-> !qco.qubit, !qco.qubit, !qco.qubit
```
}];

let arguments =
(ins Arg<QubitType, "the first control qubit", [MemRead]>:$qubit0_in,
Arg<QubitType, "the second control qubit", [MemRead]>:$qubit1_in,
Arg<QubitType, "the target qubit", [MemRead]>:$qubit2_in);
let results = (outs QubitType:$qubit0_out, QubitType:$qubit1_out,
QubitType:$qubit2_out);
let assemblyFormat =
"$qubit0_in `,` $qubit1_in `,` $qubit2_in attr-dict `:` type($qubit0_in) "
"`,` type($qubit1_in) `,` type($qubit2_in) `->` type($qubit0_out) `,` "
"type($qubit1_out) `,` type($qubit2_out)";

let extraClassDeclaration = [{
[[nodiscard]] static StringRef getBaseSymbol() { return "rccx"; }
[[nodiscard]] static DynamicMatrix getUnitaryMatrix();
}];
}

def ECROp : QCOOp<"ecr",
traits = [UnitaryOpInterface, TwoTargetZeroParameter, Pure]> {
let summary = "Apply an ECR gate to two qubits";
Expand Down

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 might once more just be me, but is there really any value in having a dedicated header file for these functions? Couldn't these simply be part of the respective pass CPP file?
It feels like these are very particular function that do not have any public API usage or at least none that I could foresee.
Would you agree?

Edit: having gone through more of the PR, I think I just don't quite like the idea that so much of the pass logic is actively decoupled from MLIR. For the most part, we have the MLIR pass logic and that then builds some auxiliary data structures, which are passed to a collection of methods that doesn't even really care about MLIR.
I would have thought/hoped that we could integrate the respective functionality a little tighter with MLIR itself and avoid auxiliary boilerplate as much as possible.
However, I can also see that the current design also has its pro's; decoupling the infrastructure from the actual pass logic.
Maybe this is just me leaning towards the tightly integrated solution.
Tagging @munich-quantum-toolkit/mqt-cc here for visibility. Maybe someone has a comment or a different perspective

Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright (c) 2023 - 2026 Chair for Design Automation, TUM
* Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH
* All rights reserved.
*
* SPDX-License-Identifier: MIT
*
* Licensed under the MIT License
*/

#pragma once

#include "mlir/Dialect/QCO/Utils/Matrix.h"

#include <mlir/IR/Builders.h>
#include <mlir/IR/Location.h>
#include <mlir/IR/Value.h>
#include <mlir/Support/LLVM.h>

#include <cstdint>
#include <optional>

namespace mlir::qco::decomposition {

/// Target Pauli or phase gate inside a multi-controlled `qco.ctrl` body.
enum class ControlledTarget : std::uint8_t { X, Z, Phase };

/**
* @brief Synthesizes a two-controlled Pauli-X, Pauli-Z, or phase gate.
*
* @param builder Builder positioned at the desired insertion point.
* @param loc Location attached to the emitted operations.
* @param control0 First control qubit (MSB).
* @param control1 Second control qubit.
* @param target Target qubit (LSB).
* @param gate Target gate type (`CCX`, `CCZ`, or `CCPhase`).
* @param theta Phase angle in radians when @p gate is
* `ControlledTarget::Phase`.
* @return The updated SSA values `[control0, control1, target]`.
*/
[[nodiscard]] SmallVector<Value>
synthesizeTwoControlled(OpBuilder& builder, Location loc, Value control0,
Value control1, Value target, ControlledTarget gate,
std::optional<double> theta = std::nullopt);

/**
* @brief Synthesizes a compile-time-known two-controlled unitary.
*
* @details Dispatches to @ref synthesizeTwoControlled when the unitary matches
* two-controlled @f$X@f$ or two-controlled phase patterns.
*
* @param builder Builder positioned at the desired insertion point.
* @param loc Location attached to the emitted operations.
* @param control0 First control qubit (MSB).
* @param control1 Second control qubit.
* @param target Target qubit (LSB).
* @param unitary Compile-time-known @f$8 \times 8@f$ unitary matrix.
* @return The updated SSA values `[control0, control1, target]`.
*
* @pre @p unitary must be @f$8 \times 8@f$.
*/
[[nodiscard]] SmallVector<Value>
synthesizeTwoControlled(OpBuilder& builder, Location loc, Value control0,
Value control1, Value target,
const DynamicMatrix& unitary);

/**
* @brief Emits an elementary-gate decomposition of a relative-phase CCX
* (`RCCX`).
*
* @param builder Builder positioned at the desired insertion point.
* @param loc Location attached to the emitted operations.
* @param control0 First control qubit.
* @param control1 Second control qubit.
* @param target Target qubit.
* @return The updated SSA values `[control0, control1, target]`.
*/
[[nodiscard]] SmallVector<Value> synthesizeRCCX(OpBuilder& builder,
Location loc, Value control0,
Value control1, Value target);

/**
* @brief Synthesizes a three-controlled Pauli-X, Pauli-Z, or phase gate.
*
* @param builder Builder positioned at the desired insertion point.
* @param loc Location attached to the emitted operations.
* @param controls Exactly three control qubits.
* @param target Target qubit.
* @param gate Target gate type (`CCCX`, `CCCZ`, or `CCCPhase`).
* @param theta Phase angle in radians when @p gate is
* `ControlledTarget::Phase`.
* @return The updated SSA values, ordered as `[controls..., target]`.
*
* @pre @p controls must contain exactly three qubits.
*/
[[nodiscard]] SmallVector<Value>
synthesizeThreeControlled(OpBuilder& builder, Location loc, ValueRange controls,
Value target, ControlledTarget gate,
std::optional<double> theta = std::nullopt);

/**
* @brief Emits a decomposition of a multi-controlled Pauli-X, Pauli-Z, or
* phase gate.
*
* @details For exactly three controls, dispatches to @ref
* synthesizeThreeControlled (used by @c decompose-three-controlled). For four
* or more controls, applies the Huang-Palsberg (PLDI 2024) no-ancilla MCX core
* (with target Hadamard bookends for Pauli-X). Subcircuits with two or three
* controls are expanded via @ref synthesizeTwoControlled and @ref
* synthesizeThreeControlled when @p minControls is at most the subcircuit
* control count; otherwise the corresponding `qco.ctrl` building blocks are
* retained for subsequent passes.
*
* @note Adapted from ``synth_mcx_noaux_hp24`` and ``synth_mcx_n_dirty_i15`` in
* the IBM Qiskit framework.
* (C) Copyright IBM 2025
*
* This code is licensed under the Apache License, Version 2.0. You may
* obtain a copy of this license in the LICENSE.txt file in the root
* directory of this source tree or at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Any modifications or derivative works of this code must retain this
* copyright notice, and modified files need to carry a notice
* indicating that they have been altered from the originals.
*
* @param builder Builder positioned at the desired insertion point.
* @param loc Location attached to the emitted operations.
* @param controls Control qubits (at least three).
* @param target Target qubit.
* @param minControls Minimum control count for which subcircuits are expanded
* rather than retained as building-block `qco.ctrl` operations.
* @param gate Target gate type.
* @param theta Phase angle when @p gate is `ControlledTarget::Phase`.
* @return The updated SSA values, ordered as `[controls..., target]`.
*
* @pre @p controls must contain at least three qubits. The HP24 core is used
* only when @p controls has four or more elements.
*/
[[nodiscard]] SmallVector<Value>
synthesizeMultiControlled(OpBuilder& builder, Location loc, ValueRange controls,
Value target, std::uint64_t minControls,
ControlledTarget gate,
std::optional<double> theta = std::nullopt);

} // namespace mlir::qco::decomposition
71 changes: 71 additions & 0 deletions mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,75 @@ def HadamardLifting : Pass<"hadamard-lifting", "mlir::ModuleOp"> {
}];
}

//===----------------------------------------------------------------------===//
// Decomposition Passes
//===----------------------------------------------------------------------===//

def DecomposeMultiControlled
: Pass<"decompose-multi-controlled", "mlir::ModuleOp"> {
let dependentDialects = ["mlir::qco::QCODialect",
"::mlir::arith::ArithDialect"];
let summary =
"Decompose four-or-more-controlled gates via Huang-Palsberg synthesis";
let description = [{
Decomposes multi-controlled Pauli-X, Pauli-Z, and phase gates (`qco.ctrl`
with `qco.x`, `qco.z`, or `qco.p` body) with at least four controls via
@ref mlir::qco::decomposition::synthesizeMultiControlled.

Gates with exactly three controls are handled by
@c decompose-three-controlled. Subcircuits with two or three controls are
expanded inline when `min-controls` is at most the subcircuit control count;
otherwise the corresponding `qco.ctrl` building blocks are retained for
subsequent @c decompose-three-controlled and @c decompose-two-controlled
passes.

The pass matches `qco.ctrl` operations with at least `min-controls` and at
least four control qubits.
}];
let options = [Option<
"minControls", "min-controls", "uint64_t", "2",

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.

IIRC tablegen also allows to directly place constraints on options/parameters. Maybe it is worth looking into whether this would simplify some of the error handling code throughout this PR.

"Minimum control count for decomposing a top-level gate (must be at "
"least 4). Controlled gates with fewer than four controls are left to "
"@c decompose-three-controlled or @c decompose-two-controlled. Must be "
"at least 2.">];
}

def DecomposeThreeControlled
: Pass<"decompose-three-controlled", "mlir::ModuleOp"> {
let dependentDialects = ["mlir::qco::QCODialect",
"::mlir::arith::ArithDialect"];
let summary =
"Decompose three-control CCCX/CCCZ/CCCP gates via three-controlled "
"synthesis";
let description = [{
Decomposes three-control building-block gates (`CCCX`, `CCCZ`, and
`CCCPhase`) via @ref mlir::qco::decomposition::synthesizeThreeControlled.

Pauli-X and Pauli-Z gates are expanded to elementary one- and two-qubit
gates. Phase gates are lowered to a three-control `qco.ctrl` with a
`qco.p` body for subsequent passes.

The pass matches `qco.ctrl` operations with exactly three controls and a
single `qco.x`, `qco.z`, or `qco.p` body on one target. Gates with fewer
than three controls (e.g. `CX`, `CCX`) are left unchanged.
}];
}

def DecomposeTwoControlled
: Pass<"decompose-two-controlled", "mlir::ModuleOp"> {
let dependentDialects = ["mlir::qco::QCODialect",
"::mlir::arith::ArithDialect"];
let summary = "Decompose two-control CCX/CCZ/CCPhase and RCCX gates via "
"two-controlled synthesis";
let description = [{
Decomposes two-control building-block gates (`CCX`, `CCZ`, `CCPhase`) via
@ref mlir::qco::decomposition::synthesizeTwoControlled and standalone
`qco.rccx` gates via @ref mlir::qco::decomposition::synthesizeRCCX.

The pass matches `qco.ctrl` operations with exactly two controls and a
single `qco.x`, `qco.z`, or `qco.p` body on one target. Single-control
gates (e.g. `CX`, `CP`) are left unchanged.
}];
}

#endif // MLIR_DIALECT_QCO_TRANSFORMS_PASSES_TD
14 changes: 14 additions & 0 deletions mlir/lib/Compiler/CompilerPipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module,
"enabled and the record pointer to be non-null.\n";
return failure();
}
if (config_.enableDecomposeMultiControlled &&
config_.decomposeMultiControlledMinControls < 2) {
llvm::errs()
<< "decomposeMultiControlledMinControls must be at least 2 when "
"enableDecomposeMultiControlled is enabled.\n";
Comment thread
simon1hofmann marked this conversation as resolved.
return failure();
}

auto runStage = [&](auto&& populatePasses) -> LogicalResult {
PassManager pm(module.getContext());
Expand Down Expand Up @@ -147,6 +154,13 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module,
}
// Stage 5: Optimization passes
if (failed(runStage([&](PassManager& pm) {
if (config_.enableDecomposeMultiControlled) {
qco::DecomposeMultiControlledOptions options;
options.minControls = config_.decomposeMultiControlledMinControls;
pm.addPass(qco::createDecomposeMultiControlled(options));
pm.addPass(qco::createDecomposeThreeControlled());
pm.addPass(qco::createDecomposeTwoControlled());
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (!config_.disableMergeSingleQubitRotationGates) {
pm.addPass(qco::createMergeSingleQubitRotationGates());
}
Expand Down
11 changes: 11 additions & 0 deletions mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
#include <cstddef>
#include <cstdint>
#include <string>
#include <tuple>
#include <utility>
#include <variant>

Expand Down Expand Up @@ -795,6 +796,16 @@ DEFINE_TWO_TARGET_TWO_PARAMETER(XXMinusYYOp, xx_minus_yy, theta, beta)

#undef DEFINE_TWO_TARGET_TWO_PARAMETER

std::tuple<Value, Value, Value>
QCOProgramBuilder::rccx(Value qubit0, Value qubit1, Value qubit2) {
checkFinalized();
auto op = RCCXOp::create(*this, qubit0, qubit1, qubit2);
updateQubitTracking(qubit0, op.getQubit0Out());
updateQubitTracking(qubit1, op.getQubit1Out());
updateQubitTracking(qubit2, op.getQubit2Out());
return {op.getQubit0Out(), op.getQubit1Out(), op.getQubit2Out()};
}

// BarrierOp

ValueRange QCOProgramBuilder::barrier(ValueRange qubits) {
Expand Down
Loading
Loading