Skip to content
Merged
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
46 changes: 46 additions & 0 deletions .agent/plans/payload-control-flow-legalization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Compiler-only control-flow legalization

Status: implemented. Latest local validation: 2026-09-09.

## Scope

Legalize structured QCO/SCF control flow for the selected payload. Producers
normalize CFG branches before target compilation. Scalar operations, measurement
provenance, allocation, functions, and final payload-profile verification remain
separate checks.

The implementation is in
`mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp`; compiler tests
are in `mlir/unittests/Compiler/test_compiler_pipeline.cpp`. Public contracts
are in `docs/mlir/target_compilation.md` and the QCO `Passes.td`.

## Decisions

- Keep SCCP and QCO cleanup between unrolling and residual legality checks:
unrolling exposes constant bounds and branches.
- Reuse MLIR trip counts, zero/one-trip promotion, and full unrolling. Require
literal bounds, signed-arithmetic safety, and a scaled step that fits the IV
type. Interpret unsigned bounds with zero extension. Limit the pass to 65,536
cloned body operations. A temporary constant lets LLVM unroll terminator-only
state updates under the same budget; cleanup removes it.
- Build switch fallbacks iteratively. Preflight the payload's branch-depth limit
and a compiler limit of 256 total control-flow levels, including moved case
bodies. Retained native multiway switches do not use this expansion limit.
- Require explicit quantum iteration arguments and QCO branch state transport.
Exactly one SSA use does not exclude captures in repeated regions. Keep
negative fixtures valid under allocation verification so they test this rule.
- Reuse the cached `TargetEnvironment`. Capability IDs remain a compiler
snapshot; the QDMI adapter and final payload-profile checks stay separate.

## Validation

After rebasing on main `2bd6a88e1`, the LLVM/MLIR 23.1.0 release build passed
all 201 compiler tests. The full native suite passed 3,388 tests with one
optional `QueryJobId` skip. MLIR documentation generation, repository lint, and
whole changed-file C++ lint passed. Focused regressions cover IV values,
cumulative cloning, switch depth, case/default selection, quantum-state
forwarding, and invalid captures.

The loop-boundary fixes pass all 203 compiler tests, including frontend state
permutations, terminator-only induction values, and unsigned bounds above the
signed range of their type.
12 changes: 7 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ releases may include breaking changes.
direct lowering and dense-array helpers for supported compiler inputs
([#1915], [#1973], [#2077], [#2078], [#2079], [#2334]) ([**@simon1hofmann**],
[**@burgholzer**])
- ✨ Add immutable MLIR compiler targets, selected payload specifications, QDMI
device integration, ordered operation applicability, directional native
synthesis, and target compilation through C++, Python, and `mqt-cc` ([#2285],
[#2219], [#2049], [#1999], [#1993], [#1687]) ([**@MatthiasReumann**],
[**@simon1hofmann**], [**@burgholzer**])
- ✨ Add immutable MLIR compiler targets, selected payload specifications,
payload-aware control-flow legalization, QDMI device integration, ordered
operation applicability, directional native synthesis, and target compilation
through C++, Python, and `mqt-cc` ([#2285], [#2219], [#2162], [#2049],
[#1999], [#1993], [#1687]) ([**@MatthiasReumann**], [**@simon1hofmann**],
[**@burgholzer**])

#### Import and export

Expand Down Expand Up @@ -992,6 +993,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
52 changes: 52 additions & 0 deletions docs/mlir/target_compilation.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,58 @@ 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
uses `unroll-loops-for-payload` before cleanup and `legalize-control-flow` after
cleanup, so unrolling can expose constant branches before legality checks. The
latter pass 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 a linear chain of nested forward branches when that
form fits the selected contract. Before expansion, the compiler checks the
selected forward-branching nesting limit and a compiler safety limit of 256
total control-flow levels, including enclosing control flow. This compiler limit
is not a QDMI requirement and does not apply to switches retained under multiway
branching.

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. Both control-flow passes validate this loop input restriction
before transforming loops or lowering switches. It is separate from QCO's
exactly-one-SSA-use check.

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. The proof requires literal loop bounds and a literal step;
it does not infer a trip count from symbolic bounds. MLIR computes static trip
counts; full unrolling additionally requires bounds and scaled steps that fit
its signed arithmetic. The scaled step must also fit the loop induction-variable
type. 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
14 changes: 14 additions & 0 deletions mlir/include/mlir/Compiler/TargetEnvironment.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ struct PayloadFormat {

/// One typed constraint on a payload capability.
struct ProgramConstraint {
/// IDs of control-flow constraints understood by the compiler.
static constexpr llvm::StringLiteral MAX_NESTING_DEPTH =
"max-control-flow-nesting-depth";
static constexpr llvm::StringLiteral MAX_ITERATION_COUNT =
"max-iteration-count";
static constexpr llvm::StringLiteral MAX_CASE_COUNT = "max-case-count";

std::string id;
uint64_t value = 0;

Expand All @@ -58,6 +65,13 @@ struct ProgramConstraint {

/// One extensible payload execution capability.
struct ProgramCapability {
/// IDs of structural control-flow capabilities understood by the compiler.
static constexpr llvm::StringLiteral FORWARD_BRANCHING = "forward-branching";
static constexpr llvm::StringLiteral COUNTED_ITERATION = "counted-iteration";
static constexpr llvm::StringLiteral CONDITIONAL_LOOP = "conditional-loop";
static constexpr llvm::StringLiteral MULTIWAY_BRANCHING =
"multiway-branching";

std::string id;
uint64_t value = 0;
std::vector<ProgramConstraint> constraints;
Expand Down
38 changes: 38 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,44 @@ def QuantumLoopUnroll
"set it to -1, and it will fully unroll.">];
}

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

def UnrollLoopsForPayload : Pass<"unroll-loops-for-payload", "mlir::ModuleOp"> {
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.
Run constant propagation and QCO cleanup after this pass, then
`legalize-control-flow` to check the remaining branches and loops.
After folding static branches, the pass checks that all SCF loops carry
linear QCO state through iteration arguments instead of capturing it.
}];
}

def LegalizeControlFlow : Pass<"legalize-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.
Switch expansion checks the selected forward-branching depth limit before
rewriting and limits the resulting total control-flow nesting depth to 256.
This compiler safety limit does not restrict retained native switches.
Run this pass after `unroll-loops-for-payload`, constant propagation, and
QCO cleanup so newly constant control flow can fold before legality checks.
Before conversion, the pass checks that all SCF loops carry linear QCO
state through iteration arguments instead of capturing it.
}];
}

//===----------------------------------------------------------------------===//
// 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::createUnrollLoopsForPayload());
pm.addPass(createSCCPPass());
populateQCOCleanupPipeline(pm);
pm.addPass(qco::createLegalizeControlFlow());
pm.addPass(qco::createDecomposeMultiControlled(target));
populateDefaultQCOOptimizationPipeline(pm);
/// ponytail: CX/CZ-cost fusion can increase square-root iSWAP counts;
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