Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 48 additions & 0 deletions .agent/plans/payload-control-flow-legalization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Compiler-only control-flow legalization

Status: implemented.

## Scope and release boundary

Core #2162 follows #2219 without QDMI runtime or adapter ancestry and targets
Core 4.0. Core #2365 and QDMI #523 track the separate Core 4.1/QDMI 1.4
adaptation and do not gate this prototype. The rebase preserves the capability
snapshot; human review must still settle the provider-neutral vocabulary.

Legalize structural control flow against the selected target environment. Retain
supported constructs, lower unsupported static loops and switches where
possible, and fail closed when residual control flow cannot be represented.
Scalar computation, measurement provenance, allocation, functions and final QIR
profile verification remain separate work.

## Implementation

Keep two passes in one source: bounded static-loop unrolling before cleanup,
then dialect conversion for residual branches and loops. Reuse MLIR symbol DCE,
SCCP, native static trip counts, loop unrolling and conversion legality. Require
structured QCO/SCF input; producers normalize CFG branches before compilation.
Preserve literal-bound proofs, the 65,536 cloned-operation limit, and signed
arithmetic safety checks for full unrolling. Reject invalid linear captures;
carry quantum values explicitly through regions.

The canonical pipeline receives one selected TargetEnvironment and shares its
prepared target with all passes. It does not reintroduce removed cleanup passes
or unknown-target fallbacks. Both legalization passes consume the existing
cached TargetEnvironmentAnalysis. Capability names and constraints remain
provisional.

Single-case switches require only multiway branching. Cleanup after mapping
remains; the redundant cleanup immediately after control legalization is
removed.

## Validation

The optimized native build passed all 3,217 configured tests, with one existing
optional-device skip. The compiler suite passed all 191 tests, including early
CFG rejection, runtime assertions, single-case quantum and classical switches,
full-width trip counts, unroll bounds, and linear-state constraints. MLIR
documentation, repository lint, and whole changed-file C++ lint passed.

Simon Hofmann's human co-authorship and the existing review history are
preserved. The child commits are restacked on the shared-environment
implementation in #2219.
9 changes: 5 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ releases may include breaking changes.
[#1807], [#1808], [#1815], [#1824], [#1869], [#1872], [#1914], [#1925],
[#1927], [#1935], [#1936], [#1938], [#1975], [#1976], [#2006], [#2014],
[#2015], [#2017], [#2026], [#2028], [#2054], [#2058], [#2125], [#2136],
[#2149], [#2150], [#2158], [#2194], [#2210], [#2211], [#2215], [#2218],
[#2220], [#2323], [#2336], [#2344]) ([**@burgholzer**], [**@denialhaag**],
[**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**],
[**@MatthiasReumann**], [**@simon1hofmann**], [**@J4MMlE**])
[#2149], [#2150], [#2158], [#2162], [#2194], [#2210], [#2211], [#2215],
[#2218], [#2220], [#2323], [#2336], [#2344]) ([**@burgholzer**],
[**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**],
[**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**], [**@J4MMlE**])
- ✨ Add a library for typed structured quantum benchmarks with versioned
instance specifications, analytic references, deterministic manifests, and
C++, Python, and command-line interfaces ([#2135], [#2299], [#2315], [#2324],
Expand Down Expand Up @@ -990,6 +990,7 @@ for previous changelogs._
[#2175]: https://github.com/munich-quantum-toolkit/core/pull/2175
[#2169]: https://github.com/munich-quantum-toolkit/core/pull/2169
[#2168]: https://github.com/munich-quantum-toolkit/core/pull/2168
[#2162]: https://github.com/munich-quantum-toolkit/core/pull/2162
[#2158]: https://github.com/munich-quantum-toolkit/core/pull/2158
[#2157]: https://github.com/munich-quantum-toolkit/core/pull/2157
[#2156]: https://github.com/munich-quantum-toolkit/core/pull/2156
Expand Down
40 changes: 40 additions & 0 deletions docs/mlir/target_compilation.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,46 @@ Payload versions accept one to three numeric components. A
suffixes, and version ranges are rejected. The same rules apply when reading the
typed `#mqt.payload_spec` attribute.

### Payload control flow

Target compilation requires structured QCO/SCF input. Producers of raw CFG
branches must normalize them before target compilation; runtime assertions are
allowed. The pipeline removes unused symbols, propagates constants, unrolls
unsupported static loops, and then runs the standard QCO cleanup pipeline. It
applies these structural capabilities to the remaining control flow:

| Capability | Residual operations |
| -------------------- | --------------------------------------------------- |
| `forward-branching` | `qco.if` and classical `scf.if` |
| `counted-iteration` | `scf.for` |
| `conditional-loop` | `scf.while` |
| `multiway-branching` | `qco.index_switch` and classical `scf.index_switch` |

A finite `scf.for` that exceeds the selected counted-iteration contract is fully
unrolled when this clones at most 65,536 body operations. Cleanup runs again
because unrolling can make nested bounds and conditions constant. An unsupported
index switch is lowered to nested forward branches when that form fits the
selected contract. Generic SCF branches cannot capture or return QCO qubits or
quantum tensors; use the corresponding QCO branch operation for linear quantum
state. SCF loops must carry linear quantum state through their iteration
arguments instead of capturing it.

The supported constraints are `max-control-flow-nesting-depth` on all four
capabilities, `max-iteration-count` on both iteration capabilities, and
`max-case-count` on multiway branching, counting explicit cases without the
default region. One explicit case plus a default is a supported index switch and
does not require forward branching. Limits are inclusive. The compiler must
prove a constrained loop's trip count. It currently proves constant `scf.for`
bounds and rejects a constrained `scf.while` because no general termination
bound is available. MLIR computes static trip counts; full unrolling
additionally requires bounds and scaled steps that fit its signed arithmetic. A
zero, unknown, or misapplied constraint makes that capability group unusable.
Missing or incomplete optional metadata never implies support.

This stage checks structural control flow only. Later lowering stages remain
responsible for scalar types and operations, measurement provenance, function
features, allocation, and final payload-profile conformance.

The target can also be constructed directly. Connectivity and native-operation
support are required:

Expand Down
2 changes: 2 additions & 0 deletions mlir/include/mlir/Compiler/TargetCompilation.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class OpPassManager;
/// synthesizes native operations, performs a final local cleanup, and verifies
/// target conformance. The context that runs this low-level pipeline must
/// register inliner extensions for its callable dialects.
/// Input must use structured QCO/SCF control flow. Normalize CFG branches
/// before calling this pipeline. Runtime assertions are allowed.
/// The supplied environment is authoritative: the pipeline attaches it to the
/// module and shares its prepared target with every target-dependent pass.
/// The environment must remain unchanged during pipeline execution.
Expand Down
30 changes: 30 additions & 0 deletions mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,36 @@ def QuantumLoopUnroll
"set it to -1, and it will fully unroll.">];
}

//===----------------------------------------------------------------------===//
// Payload legalization passes
//===----------------------------------------------------------------------===//

def UnrollUnsupportedPayloadLoops
: Pass<"unroll-unsupported-payload-loops", "mlir::ModuleOp"> {
Comment on lines +117 to +118

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.

The naming of this is a bit odd. Can we make this a little more natural? same holds for the other pass. Maybe the passes could even be combined. I like the "legalize-control-flow" part of this. Maybe a combination of both makes sense.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I just want to add to that there is also the QuantumLoopUnroll pass defined right above that. I think these two could potentially be one.

let dependentDialects = ["mlir::arith::ArithDialect",
"mlir::scf::SCFDialect"];
let summary = "Unroll static loops unsupported by the selected payload";
let description = [{
Reads the selected payload from `mqt.target_env` and fully unrolls static
`scf.for` operations that its counted-iteration capability does not cover.
The pass limits the total number of cloned body operations to 65,536.
Dynamic loops remain for payload control-flow legalization.
}];
}

def LegalizePayloadControlFlow
: Pass<"legalize-payload-control-flow", "mlir::ModuleOp"> {
let dependentDialects = ["mlir::qco::QCODialect", "mlir::scf::SCFDialect",
"mlir::arith::ArithDialect"];
let summary = "Legalize control flow for the selected payload";
let description = [{
Reads the selected payload from `mqt.target_env`. The pass retains covered
QCO and SCF branches and loops, lowers unsupported index switches to nested
forward branches when possible, and rejects residual structural control
flow that the payload does not support.
}];
}

//===----------------------------------------------------------------------===//
// Transpilation Passes
//===----------------------------------------------------------------------===//
Expand Down
30 changes: 25 additions & 5 deletions mlir/lib/Compiler/TargetCompilation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
#include "mlir/Dialect/QCO/Transforms/Passes.h"
#include "mlir/Support/Passes.h"

#include <mlir/IR/Visitors.h>
#include <mlir/Pass/Pass.h>
#include <mlir/Pass/PassManager.h>
#include <mlir/Support/WalkResult.h>
#include <mlir/Transforms/Passes.h>

#include <memory>
Expand All @@ -26,18 +28,31 @@
namespace mlir {
namespace {

class InitializeTargetEnvironmentPass
: public PassWrapper<InitializeTargetEnvironmentPass,
class PrepareTargetCompilationPass
: public PassWrapper<PrepareTargetCompilationPass,
OperationPass<ModuleOp>> {
public:
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(InitializeTargetEnvironmentPass)
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PrepareTargetCompilationPass)

explicit InitializeTargetEnvironmentPass(TargetEnvironment environment)
explicit PrepareTargetCompilationPass(TargetEnvironment environment)
: environment_(std::move(environment)) {}

protected:
void runOnOperation() override {
getAnalysis<TargetEnvironmentAnalysis>().initialize(environment_);
auto result = getOperation().walk([](Operation* operation) {
if (operation->getNumSuccessors() == 0) {
return WalkResult::advance();
}
operation->emitError(
"target compilation requires structured QCO/SCF input; normalize "
"CFG branches before compilation");
return WalkResult::interrupt();
});
if (result.wasInterrupted()) {
signalPassFailure();
return;
}
markAnalysesPreserved<TargetEnvironmentAnalysis>();
}

Expand All @@ -49,10 +64,15 @@ class InitializeTargetEnvironmentPass

void populateTargetCompilationPipeline(OpPassManager& pm,
const TargetEnvironment& environment) {
pm.addPass(std::make_unique<InitializeTargetEnvironmentPass>(environment));
pm.addPass(std::make_unique<PrepareTargetCompilationPass>(environment));
const auto& target = environment.target();
pm.addPass(createInlinerPass());
pm.addPass(createSymbolDCEPass());
pm.addPass(createSCCPPass());
pm.addPass(qco::createUnrollUnsupportedPayloadLoops());
pm.addPass(createSCCPPass());
populateQCOCleanupPipeline(pm);
pm.addPass(qco::createLegalizePayloadControlFlow());
pm.addPass(qco::createDecomposeMultiControlled(target));
populateDefaultQCOOptimizationPipeline(pm);
pm.addPass(qco::createFuseTwoQubitGates());
Expand Down
1 change: 1 addition & 0 deletions mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ add_mlir_library(
MLIRMQTDialect
MLIRMQTTransforms
MLIRMQTUtils
MLIRSCFDialect
MLIRSCFUtils
MLIRTransformUtils
DEPENDS
Expand Down
Loading
Loading