From 8a691b7a92546e95dfd04f6f876b4d27f2c6aea3 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 24 Aug 2026 13:46:34 +0000 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20Legalize=20control=20flow=20for?= =?UTF-8?q?=20selected=20payloads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use stock MLIR normalization, bounded static loop unrolling, and dialect conversion to match residual structured control flow to the selected payload specification. Co-authored-by: Simon Hofmann Assisted-by: GPT-5.6 Sol via Codex --- .../payload-control-flow-legalization.md | 44 ++ CHANGELOG.md | 9 +- docs/mlir/target_compilation.md | 38 ++ .../mlir/Dialect/QCO/Transforms/Passes.td | 30 + mlir/lib/Compiler/CMakeLists.txt | 1 + mlir/lib/Compiler/TargetCompilation.cpp | 8 + .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 1 + .../Transforms/LegalizePayloadControlFlow.cpp | 636 ++++++++++++++++++ mlir/lib/Support/Passes.cpp | 2 + .../Compiler/test_compiler_pipeline.cpp | 590 ++++++++++++++++ 10 files changed, 1355 insertions(+), 4 deletions(-) create mode 100644 .agent/plans/payload-control-flow-legalization.md create mode 100644 mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp diff --git a/.agent/plans/payload-control-flow-legalization.md b/.agent/plans/payload-control-flow-legalization.md new file mode 100644 index 0000000000..7f2902d870 --- /dev/null +++ b/.agent/plans/payload-control-flow-legalization.md @@ -0,0 +1,44 @@ +# Compiler-only control-flow legalization + +Status: independently rebased and locally validated; design remains gated. + +## Scope and design gate + +Core #2162 follows #2219 without QDMI runtime or adapter ancestry. This is a +non-blocking Core 4.1 candidate, gated by the capability design in Core #2365 +and QDMI #523. The rebase preserves the prototype; it does not settle the +provider-neutral capability 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, +CFG-to-SCF lifting, SCCP, loop unrolling and conversion legality. Preserve the +65,536 cloned-operation limit and widened trip-count guard against overflow. +Reject invalid linear captures; carry quantum values explicitly through regions. + +The canonical pipeline retains its validated CompilerTarget parameter for +placement and decomposition. 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. + +## Validation + +Build independently on #2219 and run the compiler and full native suites, +including constant-control folding, loop bounds and overflow, conversion of +switches with linear results, unsupported dynamic control and exact constraint +boundaries. Run repository lint, C++ lint and the MLIR documentation build. + +The release suite passes: 3,889 tests pass and one existing optional-device test +skips. MLIR documentation and repository lint pass. The current LLVM correctly +represents the full-width loop range; its test now verifies that the loop and +its exact trip count survive instead of requiring the LLVM 22 failure. + +Preserve Simon Hofmann's human co-authorship and existing review discussion. No +archive branches or automatic review requests. diff --git a/CHANGELOG.md b/CHANGELOG.md index 97ff540113..db7c698527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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], @@ -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 diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index e09d0930b1..b43a7dc725 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -56,6 +56,44 @@ 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 removes unused symbols, lifts reducible ControlFlow dialect +graphs to SCF, and propagates constants. It checks constant loop ranges with +widened arithmetic before generic canonicalization, 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. 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 compiler rejects a constant range when MLIR's native +trip-count result disagrees with widened 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: diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 85e88d0cb6..b5c9af5215 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -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"> { + 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 //===----------------------------------------------------------------------===// diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index d29747cd43..85583295e1 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -107,6 +107,7 @@ add_mlir_library( LINK_LIBS PUBLIC LLVMBitWriter + MLIRControlFlowToSCF MLIRPass MLIRTransforms MLIRTransformUtils diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index 50b108ed9d..bb2c53c7dd 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -16,6 +16,7 @@ #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Support/Passes.h" +#include #include #include #include @@ -52,6 +53,13 @@ void populateTargetCompilationPipeline(OpPassManager& pm, pm.addPass(std::make_unique(environment)); const auto& target = environment.target(); pm.addPass(createInlinerPass()); + pm.addPass(createSymbolDCEPass()); + pm.addPass(createLiftControlFlowToSCFPass()); + pm.addPass(createSCCPPass()); + pm.addPass(qco::createUnrollUnsupportedPayloadLoops()); + pm.addPass(createSCCPPass()); + populateQCOCleanupPipeline(pm); + pm.addPass(qco::createLegalizePayloadControlFlow()); populateQCOCleanupPipeline(pm); pm.addPass(qco::createDecomposeMultiControlled(target)); populateDefaultQCOOptimizationPipeline(pm); diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 621df3444e..aec6d7fc02 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -23,6 +23,7 @@ add_mlir_library( MLIRMQTDialect MLIRMQTTransforms MLIRMQTUtils + MLIRSCFDialect MLIRSCFUtils MLIRTransformUtils DEPENDS diff --git a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp new file mode 100644 index 0000000000..8fa1b3320c --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp @@ -0,0 +1,636 @@ +/* + * 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 + */ + +#include "mlir/Compiler/TargetEnvironment.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { + +#define GEN_PASS_DEF_LEGALIZEPAYLOADCONTROLFLOW +#define GEN_PASS_DEF_UNROLLUNSUPPORTEDPAYLOADLOOPS +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" + +namespace { + +constexpr llvm::StringLiteral FORWARD_BRANCHING = "forward-branching"; +constexpr llvm::StringLiteral COUNTED_ITERATION = "counted-iteration"; +constexpr llvm::StringLiteral CONDITIONAL_LOOP = "conditional-loop"; +constexpr llvm::StringLiteral MULTIWAY_BRANCHING = "multiway-branching"; + +constexpr llvm::StringLiteral MAX_NESTING_DEPTH = + "max-control-flow-nesting-depth"; +constexpr llvm::StringLiteral MAX_ITERATION_COUNT = "max-iteration-count"; +constexpr llvm::StringLiteral MAX_CASE_COUNT = "max-case-count"; + +constexpr uint64_t MAX_UNROLLED_OPERATIONS = 65536U; + +enum class ControlFeature : uint8_t { + ForwardBranching, + CountedIteration, + ConditionalLoop, + MultiwayBranching, + Count, +}; + +struct CapabilityGroup { + bool usable = false; + std::optional maxNestingDepth; + std::optional maxIterationCount; + std::optional maxCaseCount; +}; + +class PayloadControlSupport { +public: + [[nodiscard]] static std::optional + read(ModuleOp moduleOp, const TargetEnvironmentAnalysis& analysis) { + if (!analysis) { + moduleOp.emitError() + << "payload control-flow legalization requires a valid " + "mqt.target_env: " + << analysis.error(); + return std::nullopt; + } + + PayloadControlSupport support; + const auto& payload = analysis.environment().payloadSpecification(); + for (const ProgramCapability& capability : payload.capabilities()) { + const auto feature = + llvm::StringSwitch>(capability.id) + .Case(FORWARD_BRANCHING, ControlFeature::ForwardBranching) + .Case(COUNTED_ITERATION, ControlFeature::CountedIteration) + .Case(CONDITIONAL_LOOP, ControlFeature::ConditionalLoop) + .Case(MULTIWAY_BRANCHING, ControlFeature::MultiwayBranching) + .Default(std::nullopt); + if (!feature) { + continue; + } + + CapabilityGroup& group = support.get(*feature); + if (capability.value != 0U) { + continue; + } + + group = CapabilityGroup{.usable = true}; + for (const ProgramConstraint& constraint : capability.constraints) { + applyConstraint(group, *feature, constraint.id, constraint.value); + } + } + return support; + } + + [[nodiscard]] const CapabilityGroup& get(const ControlFeature feature) const { + return groups[static_cast(feature)]; + } + + [[nodiscard]] bool coversDepth(const ControlFeature feature, + Operation* operation) const { + const CapabilityGroup& group = get(feature); + return group.usable && (!group.maxNestingDepth || + controlDepth(operation) <= *group.maxNestingDepth); + } + + [[nodiscard]] bool + coversIteration(const ControlFeature feature, Operation* operation, + const std::optional& tripCount) const { + if (!coversDepth(feature, operation)) { + return false; + } + const CapabilityGroup& group = get(feature); + if (!group.maxIterationCount) { + return true; + } + return tripCount && tripCount->ule(*group.maxIterationCount); + } + + [[nodiscard]] bool coversMultiwayBranching(Operation* operation, + const uint64_t caseCount) const { + if (!coversDepth(ControlFeature::MultiwayBranching, operation)) { + return false; + } + const CapabilityGroup& group = get(ControlFeature::MultiwayBranching); + return !group.maxCaseCount || caseCount <= *group.maxCaseCount; + } + + [[nodiscard]] static uint64_t controlDepth(Operation* operation) { + uint64_t depth = 1U; + for (Operation* parent = operation->getParentOp(); parent != nullptr; + parent = parent->getParentOp()) { + if (isa(parent)) { + ++depth; + } + } + return depth; + } + +private: + [[nodiscard]] CapabilityGroup& get(const ControlFeature feature) { + return groups[static_cast(feature)]; + } + + static void applyConstraint(CapabilityGroup& group, + const ControlFeature feature, + const llvm::StringRef id, const uint64_t value) { + if (!group.usable) { + return; + } + if (value == 0U) { + group.usable = false; + return; + } + if (id == MAX_NESTING_DEPTH) { + group.maxNestingDepth = value; + return; + } + if (id == MAX_ITERATION_COUNT && + (feature == ControlFeature::CountedIteration || + feature == ControlFeature::ConditionalLoop)) { + group.maxIterationCount = value; + return; + } + if (id == MAX_CASE_COUNT && feature == ControlFeature::MultiwayBranching) { + group.maxCaseCount = value; + return; + } + group.usable = false; + } + + std::array(ControlFeature::Count)> + groups{}; +}; + +} // namespace + +[[nodiscard]] static bool hasLinearCapture(Operation* operation) { + llvm::SetVector captures; + getUsedValuesDefinedAbove(operation->getRegions(), captures); + return llvm::any_of( + captures, [](Value value) { return isLinearQubitType(value.getType()); }); +} + +[[nodiscard]] static bool hasLinearBranchState(Operation* operation) { + return llvm::any_of(operation->getResultTypes(), isLinearQubitType) || + hasLinearCapture(operation); +} + +/// Widen before subtracting so a large range cannot appear to have zero trips. +[[nodiscard]] static std::optional +getExactConstantTripCount(scf::ForOp loop) { + const auto constant = [](Value value) -> std::optional { + const auto result = getConstantAPIntValue(getAsOpFoldResult(value)); + return result ? std::optional(result->first) : std::nullopt; + }; + + const auto lowerBound = constant(loop.getLowerBound()); + const auto upperBound = constant(loop.getUpperBound()); + const auto step = constant(loop.getStep()); + if (!lowerBound || !upperBound || !step) { + return std::nullopt; + } + + const unsigned width = std::max({ + lowerBound->getBitWidth(), + upperBound->getBitWidth(), + step->getBitWidth(), + }) + + 1U; + const bool isUnsigned = loop.getUnsignedCmp(); + const auto extend = [&](const llvm::APInt& value) { + return isUnsigned ? value.zextOrTrunc(width) : value.sextOrTrunc(width); + }; + const llvm::APInt lower = extend(*lowerBound); + const llvm::APInt upper = extend(*upperBound); + const llvm::APInt stride = extend(*step); + const llvm::APInt one(width, 1U); + + if ((isUnsigned && (stride.isZero() || upper.ule(lower))) || + (!isUnsigned && (!stride.isStrictlyPositive() || upper.sle(lower)))) { + return llvm::APInt(width, 0U); + } + const llvm::APInt difference = upper - lower; + return ((difference - one).udiv(stride)) + one; +} + +[[nodiscard]] static bool haveEqualTripCounts(const llvm::APInt& lhs, + const llvm::APInt& rhs) { + const unsigned width = std::max(lhs.getBitWidth(), rhs.getBitWidth()); + return lhs.zextOrTrunc(width) == rhs.zextOrTrunc(width); +} + +static LogicalResult foldStaticBranches(ModuleOp moduleOp) { + /// Do not load generic SCF loop patterns before the exact trip-count check. + RewritePatternSet patterns(moduleOp.getContext()); + IfOp::getCanonicalizationPatterns(patterns, moduleOp.getContext()); + IndexSwitchOp::getCanonicalizationPatterns(patterns, moduleOp.getContext()); + scf::IfOp::getCanonicalizationPatterns(patterns, moduleOp.getContext()); + scf::IndexSwitchOp::getCanonicalizationPatterns(patterns, + moduleOp.getContext()); + return applyPatternsGreedily( + moduleOp, std::move(patterns), + GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit)); +} + +[[nodiscard]] static bool isLegal(IfOp operation, + const PayloadControlSupport& support) { + return support.coversDepth(ControlFeature::ForwardBranching, operation); +} + +[[nodiscard]] static bool isLegal(scf::IfOp operation, + const PayloadControlSupport& support) { + return !hasLinearBranchState(operation) && + support.coversDepth(ControlFeature::ForwardBranching, operation); +} + +[[nodiscard]] static bool isLegal(scf::ForOp operation, + const PayloadControlSupport& support) { + return !hasLinearCapture(operation) && + support.coversIteration(ControlFeature::CountedIteration, operation, + getExactConstantTripCount(operation)); +} + +[[nodiscard]] static bool isLegal(scf::WhileOp operation, + const PayloadControlSupport& support) { + return !hasLinearCapture(operation) && + support.coversIteration(ControlFeature::ConditionalLoop, operation, + operation.getStaticTripCount()); +} + +[[nodiscard]] static bool isLegal(IndexSwitchOp operation, + const PayloadControlSupport& support) { + const uint64_t cases = operation.getNumCases(); + return cases > 1U && support.coversMultiwayBranching(operation, cases); +} + +[[nodiscard]] static bool isLegal(scf::IndexSwitchOp operation, + const PayloadControlSupport& support) { + const uint64_t cases = operation.getNumCases(); + return !hasLinearBranchState(operation) && cases > 1U && + support.coversMultiwayBranching(operation, cases); +} + +[[nodiscard]] static bool canUseFullUnroll(scf::ForOp loop, + const uint64_t iterations) { + const auto constant = [&](Value value) -> std::optional { + const auto result = getConstantAPIntValue(getAsOpFoldResult(value)); + if (!result || (loop.getUnsignedCmp() && result->first.isNegative())) { + return std::nullopt; + } + return result->first.trySExtValue(); + }; + + const auto lowerBound = constant(loop.getLowerBound()); + const auto upperBound = constant(loop.getUpperBound()); + const auto step = constant(loop.getStep()); + if (!lowerBound || !upperBound || !step || *step <= 0) { + return false; + } + + int64_t scaledStep = 0; + int64_t unrolledUpperBound = 0; + return llvm::MulOverflow(*step, static_cast(iterations), + scaledStep) == 0 && + llvm::AddOverflow(*lowerBound, scaledStep, unrolledUpperBound) == 0; +} + +static void inlineDefaultRegion(Operation* operation, Block& block, + ValueRange blockArguments, + ConversionPatternRewriter& rewriter) { + Operation* terminator = block.getTerminator(); + rewriter.inlineBlockBefore(&block, operation, blockArguments); + SmallVector replacements(terminator->getOperands()); + rewriter.eraseOp(terminator); + rewriter.replaceOp(operation, replacements); +} + +namespace { + +class LowerQCOIndexSwitch final : public OpConversionPattern { +public: + LowerQCOIndexSwitch(MLIRContext* context, + const PayloadControlSupport& supportIn) + : OpConversionPattern(context), support(&supportIn) {} + + LogicalResult + matchAndRewrite(IndexSwitchOp operation, OpAdaptor adaptor, + ConversionPatternRewriter& rewriter) const override { + auto cases = operation.getCaseRegions(); + Region* const defaultRegion = &operation.getDefaultRegion(); + + if (cases.empty()) { + inlineDefaultRegion(operation, defaultRegion->front(), + adaptor.getTargets(), rewriter); + return success(); + } + if (!support->get(ControlFeature::ForwardBranching).usable) { + return rewriter.notifyMatchFailure( + operation, "selected payload cannot use forward branches"); + } + + const auto build = [&](auto&& self, const size_t index, + ValueRange targets) -> IfOp { + auto constant = arith::ConstantIndexOp::create( + rewriter, operation.getLoc(), operation.getCases()[index]); + auto condition = arith::CmpIOp::create( + rewriter, operation.getLoc(), arith::CmpIPredicate::eq, + adaptor.getArg(), constant.getResult()); + auto ifOp = IfOp::create(rewriter, operation.getLoc(), + operation.getClassicalResults().getTypes(), + operation.getLinearResults().getTypes(), + condition, targets); + rewriter.inlineRegionBefore(cases[index], ifOp.getThenRegion(), + ifOp.getThenRegion().end()); + if (index + 1U == cases.size()) { + rewriter.inlineRegionBefore(*defaultRegion, ifOp.getElseRegion(), + ifOp.getElseRegion().end()); + return ifOp; + } + + Block& elseBlock = ifOp.getElseRegion().emplaceBlock(); + elseBlock.addArguments(targets.getTypes(), + SmallVector(targets.size(), operation.getLoc())); + const OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPointToEnd(&elseBlock); + IfOp nested = self(self, index + 1U, elseBlock.getArguments()); + YieldOp::create(rewriter, operation.getLoc(), nested.getResults()); + return ifOp; + }; + + IfOp replacement = build(build, 0U, adaptor.getTargets()); + rewriter.replaceOp(operation, replacement.getResults()); + return success(); + } + +private: + const PayloadControlSupport* support; +}; + +class LowerSCFIndexSwitch final + : public OpConversionPattern { +public: + LowerSCFIndexSwitch(MLIRContext* context, + const PayloadControlSupport& supportIn) + : OpConversionPattern(context), support(&supportIn) {} + + LogicalResult + matchAndRewrite(scf::IndexSwitchOp operation, OpAdaptor adaptor, + ConversionPatternRewriter& rewriter) const override { + if (hasLinearBranchState(operation)) { + return rewriter.notifyMatchFailure( + operation, "SCF control flow cannot carry QCO linear values"); + } + + auto cases = operation.getCaseRegions(); + Region* const defaultRegion = &operation.getDefaultRegion(); + + if (cases.empty()) { + inlineDefaultRegion(operation, defaultRegion->front(), {}, rewriter); + return success(); + } + if (!support->get(ControlFeature::ForwardBranching).usable) { + return rewriter.notifyMatchFailure( + operation, "selected payload cannot use forward branches"); + } + + const auto build = [&](auto&& self, const size_t index) -> scf::IfOp { + auto constant = arith::ConstantIndexOp::create( + rewriter, operation.getLoc(), operation.getCases()[index]); + auto condition = arith::CmpIOp::create( + rewriter, operation.getLoc(), arith::CmpIPredicate::eq, + adaptor.getArg(), constant.getResult()); + auto ifOp = + scf::IfOp::create(rewriter, operation.getLoc(), + operation.getResultTypes(), condition, true); + rewriter.eraseBlock(&ifOp.getThenRegion().front()); + rewriter.eraseBlock(&ifOp.getElseRegion().front()); + rewriter.inlineRegionBefore(cases[index], ifOp.getThenRegion(), + ifOp.getThenRegion().end()); + if (index + 1U == cases.size()) { + rewriter.inlineRegionBefore(*defaultRegion, ifOp.getElseRegion(), + ifOp.getElseRegion().end()); + return ifOp; + } + + Block& elseBlock = ifOp.getElseRegion().emplaceBlock(); + const OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPointToEnd(&elseBlock); + scf::IfOp nested = self(self, index + 1U); + scf::YieldOp::create(rewriter, operation.getLoc(), nested.getResults()); + return ifOp; + }; + + scf::IfOp replacement = build(build, 0U); + rewriter.replaceOp(operation, replacement.getResults()); + return success(); + } + +private: + const PayloadControlSupport* support; +}; + +struct UnrollUnsupportedPayloadLoops final + : impl::UnrollUnsupportedPayloadLoopsBase { + using UnrollUnsupportedPayloadLoopsBase::UnrollUnsupportedPayloadLoopsBase; + +protected: + void runOnOperation() override { + const auto support = PayloadControlSupport::read( + getOperation(), getAnalysis()); + if (!support) { + signalPassFailure(); + return; + } + + if (failed(foldStaticBranches(getOperation()))) { + signalPassFailure(); + return; + } + + uint64_t clonedOperations = 0U; + IRRewriter rewriter(&getContext()); + while (true) { + SmallVector> loops; + const WalkResult result = + getOperation().walk([&](scf::ForOp loop) { + if (hasLinearCapture(loop)) { + loop.emitError( + "SCF loop captures QCO linear values; pass them as " + "iteration arguments"); + return WalkResult::interrupt(); + } + const auto tripCount = getExactConstantTripCount(loop); + if (tripCount) { + const auto mlirTripCount = loop.getStaticTripCount(); + if (!mlirTripCount || + !haveEqualTripCounts(*tripCount, *mlirTripCount)) { + loop.emitError( + "MLIR cannot safely normalize this static loop range"); + return WalkResult::interrupt(); + } + } + if (support->coversIteration(ControlFeature::CountedIteration, loop, + tripCount)) { + return WalkResult::advance(); + } + if (!tripCount) { + return WalkResult::skip(); + } + loops.emplace_back(loop, *tripCount); + return WalkResult::skip(); + }); + if (result.wasInterrupted()) { + signalPassFailure(); + return; + } + if (loops.empty()) { + return; + } + + for (auto& [loop, tripCount] : loops) { + if (tripCount.isZero()) { + rewriter.replaceOp(loop, loop.getInitArgs()); + continue; + } + if (tripCount.isOne()) { + if (failed(loop.promoteIfSingleIteration(rewriter))) { + loop.emitError("failed to promote a single-iteration loop"); + signalPassFailure(); + return; + } + continue; + } + if (llvm::hasSingleElement(loop.getBody()->getOperations())) { + ValueRange yielded = loop.getYieldedValues(); + if (llvm::all_of(yielded, [&](Value value) { + return loop.isDefinedOutsideOfLoop(value); + })) { + rewriter.replaceOp(loop, yielded); + continue; + } + loop.emitError("cannot fully unroll a terminator-only loop"); + signalPassFailure(); + return; + } + + uint64_t bodyOperations = 0U; + Operation* const terminator = loop.getBody()->getTerminator(); + loop.getRegion().walk([&](Operation* operation) { + bodyOperations += operation != terminator; + }); + const uint64_t remaining = MAX_UNROLLED_OPERATIONS - clonedOperations; + const uint64_t maximumTripCount = (remaining / bodyOperations) + 1U; + if (!tripCount.ule(maximumTripCount)) { + loop.emitError() << "full legalization would clone more than " + << MAX_UNROLLED_OPERATIONS + << " loop-body operations"; + signalPassFailure(); + return; + } + const uint64_t iterations = tripCount.getZExtValue(); + clonedOperations += bodyOperations * (iterations - 1U); + if (!canUseFullUnroll(loop, iterations)) { + loop.emitError( + "cannot safely apply MLIR full unrolling to these loop bounds"); + signalPassFailure(); + return; + } + if (failed(loopUnrollFull(loop))) { + loop.emitError("failed to fully unroll a static counted loop"); + signalPassFailure(); + return; + } + } + + if (failed(foldStaticBranches(getOperation()))) { + signalPassFailure(); + return; + } + } + } +}; + +struct LegalizePayloadControlFlow final + : impl::LegalizePayloadControlFlowBase { + using LegalizePayloadControlFlowBase::LegalizePayloadControlFlowBase; + +protected: + void runOnOperation() override { + const auto support = PayloadControlSupport::read( + getOperation(), getAnalysis()); + if (!support) { + signalPassFailure(); + return; + } + + ConversionTarget target(getContext()); + target.addDynamicallyLegalOp( + [&](IfOp operation) { return isLegal(operation, *support); }); + target.addDynamicallyLegalOp( + [&](scf::IfOp operation) { return isLegal(operation, *support); }); + target.addDynamicallyLegalOp( + [&](scf::ForOp operation) { return isLegal(operation, *support); }); + target.addDynamicallyLegalOp( + [&](scf::WhileOp operation) { return isLegal(operation, *support); }); + target.addDynamicallyLegalOp( + [&](IndexSwitchOp operation) { return isLegal(operation, *support); }); + target.addDynamicallyLegalOp( + [&](scf::IndexSwitchOp operation) { + return isLegal(operation, *support); + }); + target.markUnknownOpDynamicallyLegal([](Operation* operation) { + if (isa(operation)) { + return true; + } + return !isa(operation); + }); + + RewritePatternSet patterns(&getContext()); + patterns.add(&getContext(), + *support); + if (failed(applyPartialConversion(getOperation(), target, + std::move(patterns)))) { + signalPassFailure(); + } + } +}; + +} // namespace +} // namespace mlir::qco diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index 7bf7cbab8a..8a4a55c34c 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -54,6 +54,7 @@ void registerMQTCompilerPasses() { qco::registerDecomposeMultiControlled(); qco::registerFuseSingleQubitUnitaryRuns(); qco::registerHadamardLifting(); + qco::registerLegalizePayloadControlFlow(); qco::registerMeasurementLifting(); qco::registerMergeSingleQubitRotationGates(); qco::registerPauliTwirl2QGates(); @@ -63,6 +64,7 @@ void registerMQTCompilerPasses() { qco::registerReplaceClassicalControls(); qco::registerReuseQubits(); qco::registerTargetNativeSynthesis(); + qco::registerUnrollUnsupportedPayloadLoops(); qco::registerVerifyTargetConformance(); mqt::registerNormalizeGlobalPhases(); mqt::registerUnrollModifiers(); diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index d3d42aad27..5b5f995e22 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -61,6 +61,7 @@ #include #include #include +#include #include #include #include @@ -272,6 +273,67 @@ makeCZTarget(std::initializer_list singleQubitGates) { true)); } +[[nodiscard]] static PayloadSpecification +makeControlPayloadSpecification(std::vector capabilities, + const bool optionalCapabilitiesKnown = true) { + return llvm::cantFail(PayloadSpecification::create( + { + .id = "test.payload", + .version = "1.2.3", + .profile = "dynamic", + .encoding = PayloadEncoding::Binary, + }, + std::move(capabilities), optionalCapabilitiesKnown)); +} + +[[nodiscard]] static CompilerTarget makeUnrestrictedTarget() { + return llvm::cantFail( + CompilerTarget::create(1, CompilerTarget::Connectivity::allToAll(), + CompilerTarget::NativeOperations::unrestricted())); +} + +[[nodiscard]] static bool +compileForTargetWithDiagnostics(QCOProgram& program, + const PayloadSpecification& payload, + std::string& diagnostics) { + diagnostics.clear(); + ScopedDiagnosticHandler handler(program.module()->getContext(), + [&](Diagnostic& diagnostic) { + if (!diagnostics.empty()) { + diagnostics += '\n'; + } + diagnostics += diagnostic.str(); + return success(); + }); + return program.compileForTarget( + TargetEnvironment(makeUnrestrictedTarget(), payload)); +} + +constexpr llvm::StringLiteral QCO_INDEX_SWITCH_SOURCE = R"mlir( + module { + func.func @main(%selector: index) -> i64 attributes {mqt.entry_point} { + %q0 = qco.alloc : !qco.qubit + %state, %q1 = qco.index_switch %selector -> (i64, !qco.qubit) + case 0 args(%arg0 = %q0) { + %q2 = qco.x %arg0 : !qco.qubit -> !qco.qubit + %value = arith.constant 0 : i64 + qco.yield %value, %q2 : i64, !qco.qubit + } + case 1 args(%arg0 = %q0) { + %q2 = qco.h %arg0 : !qco.qubit -> !qco.qubit + %value = arith.constant 1 : i64 + qco.yield %value, %q2 : i64, !qco.qubit + } + default args(%arg0 = %q0) { + %value = arith.constant 2 : i64 + qco.yield %value, %arg0 : i64, !qco.qubit + } + qco.sink %q1 : !qco.qubit + return %state : i64 + } + } +)mlir"; + TEST_P(CompilerPipelineTest, EndToEndPipeline) { const auto& testCase = GetParam(); const auto name = " (" + testCase.name + ")"; @@ -1861,6 +1923,534 @@ TEST_F(CompilerPipelineTest, QCOProgramCompilesForTarget) { unsupportedQCO->module()->hasAttr(mlir::mqt::TargetEnvAttr::name)); } +TEST_F(CompilerPipelineTest, + PayloadControlRejectsUnsupportedResidualOperations) { + constexpr llvm::StringLiteral forwardBranch = R"mlir( + module { + func.func @main(%condition: i1) attributes {mqt.entry_point} { + %q0 = qco.alloc : !qco.qubit + %q1 = qco.if %condition args(%arg0 = %q0) -> (!qco.qubit) { + %q2 = qco.x %arg0 : !qco.qubit -> !qco.qubit + qco.yield %q2 : !qco.qubit + } else args(%arg0 = %q0) { + qco.yield %arg0 : !qco.qubit + } + qco.sink %q1 : !qco.qubit + return + } + } + )mlir"; + constexpr llvm::StringLiteral countedLoop = R"mlir( + module { + func.func @main(%upper: index) attributes {mqt.entry_point} { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %q0 = qco.alloc : !qco.qubit + %q1 = scf.for %index = %c0 to %upper step %c1 + iter_args(%arg0 = %q0) -> (!qco.qubit) { + %q2 = qco.x %arg0 : !qco.qubit -> !qco.qubit + scf.yield %q2 : !qco.qubit + } + qco.sink %q1 : !qco.qubit + return + } + } + )mlir"; + constexpr llvm::StringLiteral conditionalLoop = R"mlir( + module { + func.func @main(%condition: i1) attributes {mqt.entry_point} { + %q0 = qco.alloc : !qco.qubit + %q1 = scf.while (%arg0 = %q0) : (!qco.qubit) -> !qco.qubit { + scf.condition(%condition) %arg0 : !qco.qubit + } do { + ^bb0(%arg0: !qco.qubit): + %q2 = qco.x %arg0 : !qco.qubit -> !qco.qubit + scf.yield %q2 : !qco.qubit + } + qco.sink %q1 : !qco.qubit + return + } + } + )mlir"; + + const auto payload = makeControlPayloadSpecification({}, true); + const std::vector> cases{ + {forwardBranch, "qco.if"}, + {countedLoop, "scf.for"}, + {conditionalLoop, "scf.while"}, + {QCO_INDEX_SWITCH_SOURCE, "qco.index_switch"}, + }; + for (const auto& [source, operation] : cases) { + SCOPED_TRACE(operation.str()); + auto program = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(program); + std::string diagnostics; + EXPECT_FALSE( + compileForTargetWithDiagnostics(*program, payload, diagnostics)); + EXPECT_TRUE(StringRef(diagnostics).contains(operation)) << diagnostics; + } + + const std::vector invalid{ + {.id = "forward-branching", .value = 1}, + { + .id = "forward-branching", + .constraints = {{.id = "max-case-count", .value = 1}}, + }, + { + .id = "forward-branching", + .constraints = {{.id = "max-control-flow-nesting-depth", .value = 0}}, + }, + }; + for (const auto& capability : invalid) { + SCOPED_TRACE(capability.id); + auto program = QCOProgram::fromMLIRString(forwardBranch.str()); + ASSERT_TRUE(program); + std::string diagnostics; + EXPECT_FALSE(compileForTargetWithDiagnostics( + *program, makeControlPayloadSpecification({capability}), diagnostics)); + EXPECT_TRUE(StringRef(diagnostics).contains("qco.if")) << diagnostics; + } +} + +TEST_F(CompilerPipelineTest, + PayloadControlUsesInclusiveCountedIterationConstraint) { + constexpr llvm::StringLiteral source = R"mlir( + module { + func.func @main() attributes {mqt.entry_point} { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c3 = arith.constant 3 : index + %q0 = qco.alloc : !qco.qubit + %q1 = scf.for %index = %c0 to %c3 step %c1 + iter_args(%arg0 = %q0) -> (!qco.qubit) { + %q2 = qco.x %arg0 : !qco.qubit -> !qco.qubit + scf.yield %q2 : !qco.qubit + } + qco.sink %q1 : !qco.qubit + return + } + } + )mlir"; + const auto payload = [](const uint64_t maximum) { + return makeControlPayloadSpecification({ + { + .id = "counted-iteration", + .constraints = {{.id = "max-iteration-count", .value = maximum}}, + }, + }); + }; + + auto atBoundary = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(atBoundary); + ASSERT_TRUE(atBoundary->compileForTarget( + TargetEnvironment(makeUnrestrictedTarget(), payload(3)))); + EXPECT_TRUE(StringRef(atBoundary->str()).contains("scf.for")); + + auto aboveBoundary = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(aboveBoundary); + ASSERT_TRUE(aboveBoundary->compileForTarget( + TargetEnvironment(makeUnrestrictedTarget(), payload(2)))); + EXPECT_FALSE(StringRef(aboveBoundary->str()).contains("scf.for")); +} + +TEST_F(CompilerPipelineTest, + PayloadControlUnrollsNewlyStaticNestedLoopsToFixedPoint) { + constexpr llvm::StringLiteral source = R"mlir( + module { + func.func @main() attributes {mqt.entry_point} { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c3 = arith.constant 3 : index + %q0 = qco.alloc : !qco.qubit + %q1 = scf.for %outer = %c0 to %c3 step %c1 + iter_args(%outerQubit = %q0) -> (!qco.qubit) { + %q2 = scf.for %inner = %c0 to %outer step %c1 + iter_args(%innerQubit = %outerQubit) -> (!qco.qubit) { + %condition = arith.cmpi eq, %inner, %c0 : index + %q3 = qco.if %condition args(%arg0 = %innerQubit) -> (!qco.qubit) { + %then = qco.x %arg0 : !qco.qubit -> !qco.qubit + qco.yield %then : !qco.qubit + } else args(%arg0 = %innerQubit) { + %otherwise = qco.h %arg0 : !qco.qubit -> !qco.qubit + qco.yield %otherwise : !qco.qubit + } + scf.yield %q3 : !qco.qubit + } + scf.yield %q2 : !qco.qubit + } + qco.sink %q1 : !qco.qubit + return + } + } + )mlir"; + + auto program = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(program); + ASSERT_TRUE(program->compileForTarget(TargetEnvironment( + makeUnrestrictedTarget(), makeControlPayloadSpecification({}, true)))); + EXPECT_FALSE(StringRef(program->str()).contains("scf.for")); + EXPECT_FALSE(StringRef(program->str()).contains("qco.if")); +} + +TEST_F(CompilerPipelineTest, PayloadControlBoundsFullUnrolling) { + constexpr llvm::StringLiteral source = R"mlir( + module { + func.func @main() attributes {mqt.entry_point} { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %limit = arith.constant 65538 : index + %q0 = qco.alloc : !qco.qubit + %q1 = scf.for %index = %c0 to %limit step %c1 + iter_args(%arg0 = %q0) -> (!qco.qubit) { + %q2 = qco.x %arg0 : !qco.qubit -> !qco.qubit + scf.yield %q2 : !qco.qubit + } + qco.sink %q1 : !qco.qubit + return + } + } + )mlir"; + auto program = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(program); + std::string diagnostics; + EXPECT_FALSE(compileForTargetWithDiagnostics( + *program, makeControlPayloadSpecification({}, true), diagnostics)); + EXPECT_TRUE(StringRef(diagnostics).contains("65536 loop-body operations")) + << diagnostics; + + constexpr llvm::StringLiteral nonconstantBounds = R"mlir( + module { + func.func @main(%lower: index) attributes {mqt.entry_point} { + %c1 = arith.constant 1 : index + %c2 = arith.constant 2 : index + %upper = arith.addi %lower, %c2 overflow : index + %q0 = qco.alloc : !qco.qubit + %q1 = scf.for %index = %lower to %upper step %c1 + iter_args(%arg0 = %q0) -> (!qco.qubit) { + %q2 = qco.x %arg0 : !qco.qubit -> !qco.qubit + scf.yield %q2 : !qco.qubit + } + qco.sink %q1 : !qco.qubit + return + } + } + )mlir"; + auto unsafe = QCOProgram::fromMLIRString(nonconstantBounds.str()); + ASSERT_TRUE(unsafe); + EXPECT_FALSE(compileForTargetWithDiagnostics( + *unsafe, makeControlPayloadSpecification({}, true), diagnostics)); + EXPECT_TRUE(StringRef(diagnostics).contains("scf.for")) << diagnostics; + + constexpr llvm::StringLiteral overflowingRange = R"mlir( + module { + func.func @main() attributes {mqt.entry_point} { + %min = arith.constant -9223372036854775808 : index + %max = arith.constant 9223372036854775807 : index + %c1 = arith.constant 1 : index + %q0 = qco.alloc : !qco.qubit + %q1 = scf.for %index = %min to %max step %c1 + iter_args(%arg0 = %q0) -> (!qco.qubit) { + %q2 = qco.x %arg0 : !qco.qubit -> !qco.qubit + scf.yield %q2 : !qco.qubit + } + qco.sink %q1 : !qco.qubit + return + } + } + )mlir"; + auto overflowing = QCOProgram::fromMLIRString(overflowingRange.str()); + ASSERT_TRUE(overflowing); + diagnostics.clear(); + ASSERT_TRUE(compileForTargetWithDiagnostics( + *overflowing, + makeControlPayloadSpecification({{.id = "counted-iteration"}}), + diagnostics)); + size_t largeLoops = 0U; + overflowing->module().walk([&](scf::ForOp loop) { + const auto trips = loop.getStaticTripCount(); + ASSERT_TRUE(trips); + EXPECT_EQ(trips->getZExtValue(), std::numeric_limits::max()); + ++largeLoops; + }); + EXPECT_EQ(largeLoops, 1U); +} + +TEST_F(CompilerPipelineTest, + PayloadControlPreservesOrLowersQCOIndexSwitchAtBoundaries) { + const auto target = makeUnrestrictedTarget(); + const auto multiway = [](const uint64_t maximum) { + return ProgramCapability{ + .id = "multiway-branching", + .constraints = {{.id = "max-case-count", .value = maximum}}, + }; + }; + const auto forward = [](const uint64_t maximum) { + return ProgramCapability{ + .id = "forward-branching", + .constraints = + { + {.id = "max-control-flow-nesting-depth", .value = maximum}, + }, + }; + }; + + auto preserved = QCOProgram::fromMLIRString(QCO_INDEX_SWITCH_SOURCE.str()); + ASSERT_TRUE(preserved); + ASSERT_TRUE(preserved->compileForTarget(TargetEnvironment( + target, makeControlPayloadSpecification({multiway(2)})))); + EXPECT_TRUE(StringRef(preserved->str()).contains("qco.index_switch")); + + auto lowered = QCOProgram::fromMLIRString(QCO_INDEX_SWITCH_SOURCE.str()); + ASSERT_TRUE(lowered); + ASSERT_TRUE(lowered->compileForTarget(TargetEnvironment( + target, makeControlPayloadSpecification({multiway(1), forward(2)})))); + EXPECT_FALSE(StringRef(lowered->str()).contains("qco.index_switch")); + EXPECT_EQ(StringRef(lowered->str()).count("qco.if"), 2U); + + auto tooDeep = QCOProgram::fromMLIRString(QCO_INDEX_SWITCH_SOURCE.str()); + ASSERT_TRUE(tooDeep); + std::string diagnostics; + EXPECT_FALSE(compileForTargetWithDiagnostics( + *tooDeep, makeControlPayloadSpecification({multiway(1), forward(1)}), + diagnostics)); + EXPECT_TRUE(StringRef(diagnostics).contains("qco.index_switch")) + << diagnostics; +} + +TEST_F(CompilerPipelineTest, PayloadControlLowersClassicalSCFIndexSwitch) { + constexpr llvm::StringLiteral source = R"mlir( + module { + func.func @main(%selector: index) -> i64 attributes {mqt.entry_point} { + %result = scf.index_switch %selector -> i64 + default { + %nested = scf.index_switch %selector -> i64 + case 0 { + %zero = arith.constant 0 : i64 + scf.yield %zero : i64 + } + case 1 { + %one = arith.constant 1 : i64 + scf.yield %one : i64 + } + default { + %two = arith.constant 2 : i64 + scf.yield %two : i64 + } + scf.yield %nested : i64 + } + %q0 = qco.alloc : !qco.qubit + %q1 = qco.x %q0 : !qco.qubit -> !qco.qubit + qco.sink %q1 : !qco.qubit + return %result : i64 + } + } + )mlir"; + auto program = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(program); + attachTargetEnvironment( + program->module(), TargetEnvironment(makeUnrestrictedTarget(), + makeControlPayloadSpecification( + {{.id = "forward-branching"}}))); + ASSERT_TRUE(program->runPassPipeline("legalize-payload-control-flow")); + EXPECT_FALSE(StringRef(program->str()).contains("scf.index_switch")); + EXPECT_EQ(StringRef(program->str()).count("scf.if"), 2U); +} + +TEST_F(CompilerPipelineTest, + PayloadControlRejectsLinearStateInGenericSCFControl) { + constexpr llvm::StringLiteral ifResult = R"mlir( + module { + func.func @main(%condition: i1) + attributes {mqt.entry_point} { + %result = scf.if %condition -> !qco.qubit { + %left = qco.alloc : !qco.qubit + scf.yield %left : !qco.qubit + } else { + %right = qco.alloc : !qco.qubit + scf.yield %right : !qco.qubit + } + qco.sink %result : !qco.qubit + return + } + } + )mlir"; + constexpr llvm::StringLiteral switchResult = R"mlir( + module { + func.func @main(%selector: index, %left: tensor<1x!qco.qubit>, + %right: tensor<1x!qco.qubit>) + -> tensor<1x!qco.qubit> attributes {mqt.entry_point} { + %q = qco.alloc : !qco.qubit + qco.sink %q : !qco.qubit + %result = scf.index_switch %selector -> tensor<1x!qco.qubit> + case 0 { + scf.yield %left : tensor<1x!qco.qubit> + } + default { + scf.yield %right : tensor<1x!qco.qubit> + } + return %result : tensor<1x!qco.qubit> + } + } + )mlir"; + constexpr llvm::StringLiteral forCapture = R"mlir( + module { + func.func @main(%upper: index, %q: !qco.qubit) + attributes {mqt.entry_point} { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %index = %c0 to %upper step %c1 { + %next = qco.x %q : !qco.qubit -> !qco.qubit + qco.sink %next : !qco.qubit + } + return + } + } + )mlir"; + constexpr llvm::StringLiteral whileCapture = R"mlir( + module { + func.func @main(%condition: i1, %q: !qco.qubit) + attributes {mqt.entry_point} { + scf.while : () -> () { + scf.condition(%condition) + } do { + %next = qco.x %q : !qco.qubit -> !qco.qubit + qco.sink %next : !qco.qubit + scf.yield + } + return + } + } + )mlir"; + + const auto payload = makeControlPayloadSpecification({ + {.id = "forward-branching"}, + {.id = "counted-iteration"}, + {.id = "conditional-loop"}, + {.id = "multiway-branching"}, + }); + const std::vector> cases{ + {ifResult, "scf.if"}, + {switchResult, "scf.index_switch"}, + {forCapture, "iteration arguments"}, + {whileCapture, "scf.while"}, + }; + for (const auto& [source, expected] : cases) { + SCOPED_TRACE(expected.str()); + auto program = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(program); + std::string diagnostics; + EXPECT_FALSE( + compileForTargetWithDiagnostics(*program, payload, diagnostics)); + EXPECT_TRUE(StringRef(diagnostics).contains(expected)) << diagnostics; + } +} + +TEST_F(CompilerPipelineTest, + PayloadControlDiagnosesIllegalControlNestedInLegalBranch) { + constexpr llvm::StringLiteral source = R"mlir( + module { + func.func private @effect() + func.func @main(%condition: i1) attributes {mqt.entry_point} { + %q = qco.alloc : !qco.qubit + qco.sink %q : !qco.qubit + scf.if %condition { + scf.while : () -> () { + scf.condition(%condition) + } do { + func.call @effect() : () -> () + scf.yield + } + } + return + } + } + )mlir"; + auto program = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(program); + std::string diagnostics; + EXPECT_FALSE(compileForTargetWithDiagnostics( + *program, + makeControlPayloadSpecification( + {{.id = "forward-branching", + .constraints = {{.id = "max-control-flow-nesting-depth", + .value = 2}}}}), + diagnostics)); + EXPECT_TRUE(StringRef(diagnostics).contains("scf.while")) << diagnostics; +} + +TEST_F(CompilerPipelineTest, + PayloadControlLiftsReducibleCFGToSupportedForwardBranch) { + constexpr llvm::StringLiteral source = R"mlir( + module { + func.func @main(%condition: i1) -> i64 attributes {mqt.entry_point} { + cf.cond_br %condition, ^then, ^otherwise + ^then: + %one = arith.constant 1 : i64 + cf.br ^merge(%one : i64) + ^otherwise: + %two = arith.constant 2 : i64 + cf.br ^merge(%two : i64) + ^merge(%value: i64): + %q0 = qco.alloc : !qco.qubit + %q1 = qco.x %q0 : !qco.qubit -> !qco.qubit + qco.sink %q1 : !qco.qubit + return %value : i64 + } + } + )mlir"; + const auto payload = makeControlPayloadSpecification({ + { + .id = "forward-branching", + .constraints = + { + {.id = "max-control-flow-nesting-depth", .value = 1}, + }, + }, + }); + auto program = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(program); + ASSERT_TRUE(program->compileForTarget( + TargetEnvironment(makeUnrestrictedTarget(), payload))); + EXPECT_FALSE(StringRef(program->str()).contains("cf.")); +} + +TEST_F(CompilerPipelineTest, + PayloadControlRequiresProofForConstrainedConditionalLoop) { + constexpr llvm::StringLiteral source = R"mlir( + module { + func.func @main(%condition: i1) attributes {mqt.entry_point} { + %q0 = qco.alloc : !qco.qubit + %q1 = scf.while (%arg0 = %q0) : (!qco.qubit) -> !qco.qubit { + scf.condition(%condition) %arg0 : !qco.qubit + } do { + ^bb0(%arg0: !qco.qubit): + %q2 = qco.x %arg0 : !qco.qubit -> !qco.qubit + scf.yield %q2 : !qco.qubit + } + qco.sink %q1 : !qco.qubit + return + } + } + )mlir"; + auto unconstrained = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(unconstrained); + ASSERT_TRUE(unconstrained->compileForTarget(TargetEnvironment( + makeUnrestrictedTarget(), + makeControlPayloadSpecification({{.id = "conditional-loop"}})))); + EXPECT_TRUE(StringRef(unconstrained->str()).contains("scf.while")); + + auto constrained = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(constrained); + std::string diagnostics; + EXPECT_FALSE(compileForTargetWithDiagnostics( + *constrained, + makeControlPayloadSpecification( + {{.id = "conditional-loop", + .constraints = {{.id = "max-iteration-count", .value = 4}}}}), + diagnostics)); + EXPECT_TRUE(StringRef(diagnostics).contains("scf.while")) << diagnostics; +} + /// Test: target passes use the canonical environment in textual form. TEST_F(CompilerPipelineTest, TargetPassesRunFromTextualPipeline) { constexpr llvm::StringLiteral source = R"(OPENQASM 3.0; From b00e2e155391ce79db87e298dd8fb05de3bbe2e4 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 12:57:27 +0200 Subject: [PATCH 2/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Remove=20redundant=20m?= =?UTF-8?q?odifier=20legality=20exemptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QCO modifiers do not implement branch interfaces, so the existing fallback already accepts them. Assisted-by: GPT-6 via Codex --- mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp index 8fa1b3320c..4f552ca868 100644 --- a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp @@ -616,7 +616,7 @@ struct LegalizePayloadControlFlow final return isLegal(operation, *support); }); target.markUnknownOpDynamicallyLegal([](Operation* operation) { - if (isa(operation)) { + if (isa(operation)) { return true; } return !isa(operation); From 97aa10026a01b3d5248f6ba379d23bdfb2d30e20 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 12:57:27 +0200 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=93=9D=20Align=20control=20flow=20rel?= =?UTF-8?q?ease=20scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserve the capability snapshot while distinguishing Core 4.0 compiler review from the separate QDMI 1.4 adaptation. Assisted-by: GPT-6 via Codex --- .../payload-control-flow-legalization.md | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/.agent/plans/payload-control-flow-legalization.md b/.agent/plans/payload-control-flow-legalization.md index 7f2902d870..71fe965c3f 100644 --- a/.agent/plans/payload-control-flow-legalization.md +++ b/.agent/plans/payload-control-flow-legalization.md @@ -1,13 +1,14 @@ # Compiler-only control-flow legalization -Status: independently rebased and locally validated; design remains gated. +Status: independently rebased and locally validated capability snapshot; ready +for human contract review. -## Scope and design gate +## Scope and release boundary -Core #2162 follows #2219 without QDMI runtime or adapter ancestry. This is a -non-blocking Core 4.1 candidate, gated by the capability design in Core #2365 -and QDMI #523. The rebase preserves the prototype; it does not settle the -provider-neutral capability vocabulary. +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 @@ -35,10 +36,11 @@ including constant-control folding, loop bounds and overflow, conversion of switches with linear results, unsupported dynamic control and exact constraint boundaries. Run repository lint, C++ lint and the MLIR documentation build. -The release suite passes: 3,889 tests pass and one existing optional-device test -skips. MLIR documentation and repository lint pass. The current LLVM correctly -represents the full-width loop range; its test now verifies that the loop and -its exact trip count survive instead of requiring the LLVM 22 failure. +The prior capability-snapshot validation passed 3,889 release tests, with one +existing optional-device skip. MLIR documentation and repository lint passed. +The current LLVM correctly represents the full-width loop range; its test now +verifies that the loop and its exact trip count survive instead of requiring the +LLVM 22 failure. Preserve Simon Hofmann's human co-authorship and existing review discussion. No archive branches or automatic review requests. From 7f44fbbd2368b2aeae42acb2373bf1311ae9209c Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 8 Sep 2026 15:36:07 +0000 Subject: [PATCH 4/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20structured?= =?UTF-8?q?=20payload=20legalization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use native static trip counts while retaining literal-bound proofs and full unroll safety limits. Accept single-case switches with multiway support. Require structured input at pipeline entry and remove CFG lifting and the redundant cleanup after control legalization. Assisted-by: GPT-6 via Codex --- .../payload-control-flow-legalization.md | 46 +++++----- docs/mlir/target_compilation.md | 22 ++--- .../include/mlir/Compiler/TargetCompilation.h | 2 + mlir/lib/Compiler/CMakeLists.txt | 1 - mlir/lib/Compiler/TargetCompilation.cpp | 28 ++++-- .../Transforms/LegalizePayloadControlFlow.cpp | 61 ++----------- .../Compiler/test_compiler_pipeline.cpp | 90 +++++++++++++++++-- 7 files changed, 152 insertions(+), 98 deletions(-) diff --git a/.agent/plans/payload-control-flow-legalization.md b/.agent/plans/payload-control-flow-legalization.md index 71fe965c3f..12db6a9256 100644 --- a/.agent/plans/payload-control-flow-legalization.md +++ b/.agent/plans/payload-control-flow-legalization.md @@ -1,7 +1,6 @@ # Compiler-only control-flow legalization -Status: independently rebased and locally validated capability snapshot; ready -for human contract review. +Status: implemented. ## Scope and release boundary @@ -20,27 +19,30 @@ profile verification remain separate work. Keep two passes in one source: bounded static-loop unrolling before cleanup, then dialect conversion for residual branches and loops. Reuse MLIR symbol DCE, -CFG-to-SCF lifting, SCCP, loop unrolling and conversion legality. Preserve the -65,536 cloned-operation limit and widened trip-count guard against overflow. -Reject invalid linear captures; carry quantum values explicitly through regions. - -The canonical pipeline retains its validated CompilerTarget parameter for -placement and decomposition. 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. +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 -Build independently on #2219 and run the compiler and full native suites, -including constant-control folding, loop bounds and overflow, conversion of -switches with linear results, unsupported dynamic control and exact constraint -boundaries. Run repository lint, C++ lint and the MLIR documentation build. - -The prior capability-snapshot validation passed 3,889 release tests, with one -existing optional-device skip. MLIR documentation and repository lint passed. -The current LLVM correctly represents the full-width loop range; its test now -verifies that the loop and its exact trip count survive instead of requiring the -LLVM 22 failure. +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. -Preserve Simon Hofmann's human co-authorship and existing review discussion. No -archive branches or automatic review requests. +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. diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index b43a7dc725..6bb5777308 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -58,11 +58,11 @@ typed `#mqt.payload_spec` attribute. ### Payload control flow -Target compilation removes unused symbols, lifts reducible ControlFlow dialect -graphs to SCF, and propagates constants. It checks constant loop ranges with -widened arithmetic before generic canonicalization, unrolls unsupported static -loops, and then runs the standard QCO cleanup pipeline. It applies these -structural capabilities to the remaining 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 | | -------------------- | --------------------------------------------------- | @@ -82,13 +82,15 @@ 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. Limits are inclusive. The compiler must +`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 compiler rejects a constant range when MLIR's native -trip-count result disagrees with widened arithmetic. A zero, unknown, or -misapplied constraint makes that capability group unusable. Missing or -incomplete optional metadata never implies support. +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 diff --git a/mlir/include/mlir/Compiler/TargetCompilation.h b/mlir/include/mlir/Compiler/TargetCompilation.h index d80c01f8b3..346011a77a 100644 --- a/mlir/include/mlir/Compiler/TargetCompilation.h +++ b/mlir/include/mlir/Compiler/TargetCompilation.h @@ -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. diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 85583295e1..d29747cd43 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -107,7 +107,6 @@ add_mlir_library( LINK_LIBS PUBLIC LLVMBitWriter - MLIRControlFlowToSCF MLIRPass MLIRTransforms MLIRTransformUtils diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index bb2c53c7dd..88617ac4b6 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -16,9 +16,10 @@ #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Support/Passes.h" -#include +#include #include #include +#include #include #include @@ -27,18 +28,31 @@ namespace mlir { namespace { -class InitializeTargetEnvironmentPass - : public PassWrapper> { 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().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(); } @@ -50,17 +64,15 @@ class InitializeTargetEnvironmentPass void populateTargetCompilationPipeline(OpPassManager& pm, const TargetEnvironment& environment) { - pm.addPass(std::make_unique(environment)); + pm.addPass(std::make_unique(environment)); const auto& target = environment.target(); pm.addPass(createInlinerPass()); pm.addPass(createSymbolDCEPass()); - pm.addPass(createLiftControlFlowToSCFPass()); pm.addPass(createSCCPPass()); pm.addPass(qco::createUnrollUnsupportedPayloadLoops()); pm.addPass(createSCCPPass()); populateQCOCleanupPipeline(pm); pm.addPass(qco::createLegalizePayloadControlFlow()); - populateQCOCleanupPipeline(pm); pm.addPass(qco::createDecomposeMultiControlled(target)); populateDefaultQCOOptimizationPipeline(pm); pm.addPass(qco::createFuseTwoQubitGates()); diff --git a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp index 4f552ca868..fc5b5efef2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp @@ -35,11 +35,9 @@ #include #include -#include #include #include #include -#include #include #include @@ -212,52 +210,20 @@ class PayloadControlSupport { hasLinearCapture(operation); } -/// Widen before subtracting so a large range cannot appear to have zero trips. +/// Require literal bounds; do not infer constrained iteration from symbolic IR. [[nodiscard]] static std::optional getExactConstantTripCount(scf::ForOp loop) { - const auto constant = [](Value value) -> std::optional { - const auto result = getConstantAPIntValue(getAsOpFoldResult(value)); - return result ? std::optional(result->first) : std::nullopt; - }; - - const auto lowerBound = constant(loop.getLowerBound()); - const auto upperBound = constant(loop.getUpperBound()); - const auto step = constant(loop.getStep()); - if (!lowerBound || !upperBound || !step) { + if (!getConstantAPIntValue(getAsOpFoldResult(loop.getLowerBound())) || + !getConstantAPIntValue(getAsOpFoldResult(loop.getUpperBound())) || + !getConstantAPIntValue(getAsOpFoldResult(loop.getStep()))) { return std::nullopt; } - - const unsigned width = std::max({ - lowerBound->getBitWidth(), - upperBound->getBitWidth(), - step->getBitWidth(), - }) + - 1U; - const bool isUnsigned = loop.getUnsignedCmp(); - const auto extend = [&](const llvm::APInt& value) { - return isUnsigned ? value.zextOrTrunc(width) : value.sextOrTrunc(width); - }; - const llvm::APInt lower = extend(*lowerBound); - const llvm::APInt upper = extend(*upperBound); - const llvm::APInt stride = extend(*step); - const llvm::APInt one(width, 1U); - - if ((isUnsigned && (stride.isZero() || upper.ule(lower))) || - (!isUnsigned && (!stride.isStrictlyPositive() || upper.sle(lower)))) { - return llvm::APInt(width, 0U); - } - const llvm::APInt difference = upper - lower; - return ((difference - one).udiv(stride)) + one; -} - -[[nodiscard]] static bool haveEqualTripCounts(const llvm::APInt& lhs, - const llvm::APInt& rhs) { - const unsigned width = std::max(lhs.getBitWidth(), rhs.getBitWidth()); - return lhs.zextOrTrunc(width) == rhs.zextOrTrunc(width); + return loop.getStaticTripCount(); } static LogicalResult foldStaticBranches(ModuleOp moduleOp) { - /// Do not load generic SCF loop patterns before the exact trip-count check. + /// Fold branches without transforming loops before the capture and unroll + /// safety checks below. RewritePatternSet patterns(moduleOp.getContext()); IfOp::getCanonicalizationPatterns(patterns, moduleOp.getContext()); IndexSwitchOp::getCanonicalizationPatterns(patterns, moduleOp.getContext()); @@ -297,13 +263,13 @@ static LogicalResult foldStaticBranches(ModuleOp moduleOp) { [[nodiscard]] static bool isLegal(IndexSwitchOp operation, const PayloadControlSupport& support) { const uint64_t cases = operation.getNumCases(); - return cases > 1U && support.coversMultiwayBranching(operation, cases); + return cases > 0U && support.coversMultiwayBranching(operation, cases); } [[nodiscard]] static bool isLegal(scf::IndexSwitchOp operation, const PayloadControlSupport& support) { const uint64_t cases = operation.getNumCases(); - return !hasLinearBranchState(operation) && cases > 1U && + return !hasLinearBranchState(operation) && cases > 0U && support.coversMultiwayBranching(operation, cases); } @@ -497,15 +463,6 @@ struct UnrollUnsupportedPayloadLoops final return WalkResult::interrupt(); } const auto tripCount = getExactConstantTripCount(loop); - if (tripCount) { - const auto mlirTripCount = loop.getStaticTripCount(); - if (!mlirTripCount || - !haveEqualTripCounts(*tripCount, *mlirTripCount)) { - loop.emitError( - "MLIR cannot safely normalize this static loop range"); - return WalkResult::interrupt(); - } - } if (support->coversIteration(ControlFeature::CountedIteration, loop, tripCount)) { return WalkResult::advance(); diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 5b5f995e22..8c542c117d 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -2378,8 +2378,7 @@ TEST_F(CompilerPipelineTest, EXPECT_TRUE(StringRef(diagnostics).contains("scf.while")) << diagnostics; } -TEST_F(CompilerPipelineTest, - PayloadControlLiftsReducibleCFGToSupportedForwardBranch) { +TEST_F(CompilerPipelineTest, PayloadControlRejectsUnstructuredCFG) { constexpr llvm::StringLiteral source = R"mlir( module { func.func @main(%condition: i1) -> i64 attributes {mqt.entry_point} { @@ -2407,11 +2406,92 @@ TEST_F(CompilerPipelineTest, }, }, }); + std::string diagnostics; auto program = QCOProgram::fromMLIRString(source.str()); ASSERT_TRUE(program); - ASSERT_TRUE(program->compileForTarget( - TargetEnvironment(makeUnrestrictedTarget(), payload))); - EXPECT_FALSE(StringRef(program->str()).contains("cf.")); + EXPECT_FALSE(compileForTargetWithDiagnostics(*program, payload, diagnostics)); + EXPECT_TRUE(StringRef(diagnostics).contains("structured QCO/SCF input")) + << diagnostics; +} + +TEST_F(CompilerPipelineTest, PayloadControlRejectsConstantCFGBeforeCleanup) { + auto program = QCOProgram::fromMLIRString(R"mlir( + module { + func.func @main() attributes {mqt.entry_point} { + cf.br ^exit + ^exit: + return + } + } + )mlir"); + ASSERT_TRUE(program); + std::string diagnostics; + EXPECT_FALSE(compileForTargetWithDiagnostics( + *program, makeControlPayloadSpecification({}), diagnostics)); + EXPECT_TRUE(StringRef(diagnostics).contains("structured QCO/SCF input")) + << diagnostics; +} + +TEST_F(CompilerPipelineTest, PayloadControlAllowsRuntimeAssertions) { + auto program = QCOProgram::fromMLIRString(R"mlir( + module { + func.func @main(%condition: i1) attributes {mqt.entry_point} { + cf.assert %condition, "runtime precondition" + return + } + } + )mlir"); + ASSERT_TRUE(program); + ASSERT_TRUE(program->compileForTarget(TargetEnvironment( + makeUnrestrictedTarget(), makeControlPayloadSpecification({})))); + EXPECT_TRUE(StringRef(program->str()).contains("cf.assert")); +} + +TEST_F(CompilerPipelineTest, PayloadControlPreservesSingleCaseNativeSwitches) { + constexpr llvm::StringLiteral quantum = R"mlir( + module { + func.func @main(%selector: index) attributes {mqt.entry_point} { + %q = qco.alloc : !qco.qubit + %r = qco.index_switch %selector -> (!qco.qubit) + case 0 args(%a = %q) { + %x = qco.x %a : !qco.qubit -> !qco.qubit + qco.yield %x : !qco.qubit + } + default args(%a = %q) { + %h = qco.h %a : !qco.qubit -> !qco.qubit + qco.yield %h : !qco.qubit + } + qco.sink %r : !qco.qubit + return + } + } + )mlir"; + constexpr llvm::StringLiteral classical = R"mlir( + module { + func.func @main(%selector: index) -> i64 attributes {mqt.entry_point} { + %r = scf.index_switch %selector -> i64 + case 0 { + %one = arith.constant 1 : i64 + scf.yield %one : i64 + } + default { + %two = arith.constant 2 : i64 + scf.yield %two : i64 + } + return %r : i64 + } + } + )mlir"; + for (auto source : {quantum, classical}) { + auto program = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(program); + ASSERT_TRUE(program->compileForTarget(TargetEnvironment( + makeUnrestrictedTarget(), + makeControlPayloadSpecification( + {{.id = "multiway-branching", + .constraints = {{.id = "max-case-count", .value = 1}}}})))); + EXPECT_TRUE(StringRef(program->str()).contains("index_switch")); + } } TEST_F(CompilerPipelineTest, From 505b6021594871051cde96062b9af18ffda3ea80 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 9 Sep 2026 10:28:15 +0200 Subject: [PATCH 5/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Validate=20loop=20inpu?= =?UTF-8?q?ts=20before=20legalization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Check the supported SCF loop input form at both pass entry points, so loop legalization can rely on explicit iteration arguments. Keep branch checks after unrolling and leave the QCO linearity contract unchanged. Assisted-by: OpenAI Codex --- docs/mlir/target_compilation.md | 4 +- .../mlir/Dialect/QCO/Transforms/Passes.td | 4 ++ .../Transforms/LegalizePayloadControlFlow.cpp | 61 ++++++++++--------- .../Compiler/test_compiler_pipeline.cpp | 45 ++++++++++++++ 4 files changed, 84 insertions(+), 30 deletions(-) diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 6bb5777308..54bdf41ae4 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -78,7 +78,9 @@ 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. +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 diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index b5c9af5215..75d2e7b816 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -124,6 +124,8 @@ def UnrollUnsupportedPayloadLoops `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. + After folding static branches, the pass checks that all SCF loops carry + linear QCO state through iteration arguments instead of capturing it. }]; } @@ -137,6 +139,8 @@ def LegalizePayloadControlFlow 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. + Before conversion, the pass checks that all SCF loops carry linear QCO + state through iteration arguments instead of capturing it. }]; } diff --git a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp index fc5b5efef2..8e736a4244 100644 --- a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp @@ -210,6 +210,21 @@ class PayloadControlSupport { hasLinearCapture(operation); } +// Check the supported loop input form once before transforming control flow. +// Exactly one SSA use does not exclude captures in repeated regions. +static LogicalResult verifyLoopCarriedState(ModuleOp moduleOp) { + const auto result = moduleOp.walk([](Operation* operation) { + if (isa(operation) && + hasLinearCapture(operation)) { + operation->emitOpError( + "captures QCO linear values; pass them as iteration arguments"); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return result.wasInterrupted() ? failure() : success(); +} + /// Require literal bounds; do not infer constrained iteration from symbolic IR. [[nodiscard]] static std::optional getExactConstantTripCount(scf::ForOp loop) { @@ -248,15 +263,13 @@ static LogicalResult foldStaticBranches(ModuleOp moduleOp) { [[nodiscard]] static bool isLegal(scf::ForOp operation, const PayloadControlSupport& support) { - return !hasLinearCapture(operation) && - support.coversIteration(ControlFeature::CountedIteration, operation, + return support.coversIteration(ControlFeature::CountedIteration, operation, getExactConstantTripCount(operation)); } [[nodiscard]] static bool isLegal(scf::WhileOp operation, const PayloadControlSupport& support) { - return !hasLinearCapture(operation) && - support.coversIteration(ControlFeature::ConditionalLoop, operation, + return support.coversIteration(ControlFeature::ConditionalLoop, operation, operation.getStaticTripCount()); } @@ -445,7 +458,8 @@ struct UnrollUnsupportedPayloadLoops final return; } - if (failed(foldStaticBranches(getOperation()))) { + if (failed(foldStaticBranches(getOperation())) || + failed(verifyLoopCarriedState(getOperation()))) { signalPassFailure(); return; } @@ -454,29 +468,18 @@ struct UnrollUnsupportedPayloadLoops final IRRewriter rewriter(&getContext()); while (true) { SmallVector> loops; - const WalkResult result = - getOperation().walk([&](scf::ForOp loop) { - if (hasLinearCapture(loop)) { - loop.emitError( - "SCF loop captures QCO linear values; pass them as " - "iteration arguments"); - return WalkResult::interrupt(); - } - const auto tripCount = getExactConstantTripCount(loop); - if (support->coversIteration(ControlFeature::CountedIteration, loop, - tripCount)) { - return WalkResult::advance(); - } - if (!tripCount) { - return WalkResult::skip(); - } - loops.emplace_back(loop, *tripCount); - return WalkResult::skip(); - }); - if (result.wasInterrupted()) { - signalPassFailure(); - return; - } + getOperation().walk([&](scf::ForOp loop) { + const auto tripCount = getExactConstantTripCount(loop); + if (support->coversIteration(ControlFeature::CountedIteration, loop, + tripCount)) { + return WalkResult::advance(); + } + if (!tripCount) { + return WalkResult::skip(); + } + loops.emplace_back(loop, *tripCount); + return WalkResult::skip(); + }); if (loops.empty()) { return; } @@ -552,7 +555,7 @@ struct LegalizePayloadControlFlow final void runOnOperation() override { const auto support = PayloadControlSupport::read( getOperation(), getAnalysis()); - if (!support) { + if (!support || failed(verifyLoopCarriedState(getOperation()))) { signalPassFailure(); return; } diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 8c542c117d..e185e931d4 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -2321,6 +2321,22 @@ TEST_F(CompilerPipelineTest, } } )mlir"; + constexpr llvm::StringLiteral nestedForCapture = R"mlir( + module { + func.func @main(%upper: index) attributes {mqt.entry_point} { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + scf.for %outer = %c0 to %upper step %c1 { + %q = qco.alloc : !qco.qubit + scf.for %inner = %c0 to %c1 step %c1 { + %next = qco.x %q : !qco.qubit -> !qco.qubit + qco.sink %next : !qco.qubit + } + } + return + } + } + )mlir"; const auto payload = makeControlPayloadSpecification({ {.id = "forward-branching"}, @@ -2343,6 +2359,35 @@ TEST_F(CompilerPipelineTest, compileForTargetWithDiagnostics(*program, payload, diagnostics)); EXPECT_TRUE(StringRef(diagnostics).contains(expected)) << diagnostics; } + + for (StringRef source : { + forCapture, + whileCapture, + nestedForCapture, + }) { + SCOPED_TRACE(source.str()); + for (const auto* pass : {"unroll-unsupported-payload-loops", + "legalize-payload-control-flow"}) { + SCOPED_TRACE(pass); + auto program = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(program); + attachTargetEnvironment( + program->module(), + TargetEnvironment(makeUnrestrictedTarget(), + makeControlPayloadSpecification({}))); + const auto before = program->str(); + std::string diagnostics; + ScopedDiagnosticHandler handler(program->module()->getContext(), + [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_FALSE(program->runPassPipeline(pass)); + EXPECT_TRUE(StringRef(diagnostics).contains("iteration arguments")) + << diagnostics; + EXPECT_EQ(program->str(), before); + } + } } TEST_F(CompilerPipelineTest, From 6214a2bd5ac6758e7591ec719262dede3c1f724c Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 9 Sep 2026 10:37:53 +0200 Subject: [PATCH 6/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Clarify=20control-flow?= =?UTF-8?q?=20pass=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep payload-loop unrolling separate from residual control-flow checks so constant propagation and QCO cleanup can run between them. Leave the factor-based quantum-loop optimization unchanged. Assisted-by: OpenAI Codex --- docs/mlir/target_compilation.md | 4 +++- .../mlir/Dialect/QCO/Transforms/Passes.td | 11 ++++++----- mlir/lib/Compiler/TargetCompilation.cpp | 4 ++-- .../Transforms/LegalizePayloadControlFlow.cpp | 16 ++++++++-------- mlir/lib/Support/Passes.cpp | 4 ++-- .../Compiler/test_compiler_pipeline.cpp | 6 +++--- 6 files changed, 24 insertions(+), 21 deletions(-) diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 54bdf41ae4..051c40a734 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -62,7 +62,9 @@ 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: +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 | | -------------------- | --------------------------------------------------- | diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 75d2e7b816..92932a69a8 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -114,8 +114,7 @@ def QuantumLoopUnroll // Payload legalization passes //===----------------------------------------------------------------------===// -def UnrollUnsupportedPayloadLoops - : Pass<"unroll-unsupported-payload-loops", "mlir::ModuleOp"> { +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"; @@ -123,14 +122,14 @@ def UnrollUnsupportedPayloadLoops 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. + 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 LegalizePayloadControlFlow - : Pass<"legalize-payload-control-flow", "mlir::ModuleOp"> { +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"; @@ -139,6 +138,8 @@ def LegalizePayloadControlFlow 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. + 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. }]; diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index 88617ac4b6..db77587a2a 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -69,10 +69,10 @@ void populateTargetCompilationPipeline(OpPassManager& pm, pm.addPass(createInlinerPass()); pm.addPass(createSymbolDCEPass()); pm.addPass(createSCCPPass()); - pm.addPass(qco::createUnrollUnsupportedPayloadLoops()); + pm.addPass(qco::createUnrollLoopsForPayload()); pm.addPass(createSCCPPass()); populateQCOCleanupPipeline(pm); - pm.addPass(qco::createLegalizePayloadControlFlow()); + pm.addPass(qco::createLegalizeControlFlow()); pm.addPass(qco::createDecomposeMultiControlled(target)); populateDefaultQCOOptimizationPipeline(pm); pm.addPass(qco::createFuseTwoQubitGates()); diff --git a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp index 8e736a4244..af266b76e0 100644 --- a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp @@ -43,8 +43,8 @@ namespace mlir::qco { -#define GEN_PASS_DEF_LEGALIZEPAYLOADCONTROLFLOW -#define GEN_PASS_DEF_UNROLLUNSUPPORTEDPAYLOADLOOPS +#define GEN_PASS_DEF_LEGALIZECONTROLFLOW +#define GEN_PASS_DEF_UNROLLLOOPSFORPAYLOAD #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" namespace { @@ -445,9 +445,9 @@ class LowerSCFIndexSwitch final const PayloadControlSupport* support; }; -struct UnrollUnsupportedPayloadLoops final - : impl::UnrollUnsupportedPayloadLoopsBase { - using UnrollUnsupportedPayloadLoopsBase::UnrollUnsupportedPayloadLoopsBase; +struct UnrollLoopsForPayload final + : impl::UnrollLoopsForPayloadBase { + using UnrollLoopsForPayloadBase::UnrollLoopsForPayloadBase; protected: void runOnOperation() override { @@ -547,9 +547,9 @@ struct UnrollUnsupportedPayloadLoops final } }; -struct LegalizePayloadControlFlow final - : impl::LegalizePayloadControlFlowBase { - using LegalizePayloadControlFlowBase::LegalizePayloadControlFlowBase; +struct LegalizeControlFlow final + : impl::LegalizeControlFlowBase { + using LegalizeControlFlowBase::LegalizeControlFlowBase; protected: void runOnOperation() override { diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index 8a4a55c34c..f191f40cf8 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -54,7 +54,7 @@ void registerMQTCompilerPasses() { qco::registerDecomposeMultiControlled(); qco::registerFuseSingleQubitUnitaryRuns(); qco::registerHadamardLifting(); - qco::registerLegalizePayloadControlFlow(); + qco::registerLegalizeControlFlow(); qco::registerMeasurementLifting(); qco::registerMergeSingleQubitRotationGates(); qco::registerPauliTwirl2QGates(); @@ -64,7 +64,7 @@ void registerMQTCompilerPasses() { qco::registerReplaceClassicalControls(); qco::registerReuseQubits(); qco::registerTargetNativeSynthesis(); - qco::registerUnrollUnsupportedPayloadLoops(); + qco::registerUnrollLoopsForPayload(); qco::registerVerifyTargetConformance(); mqt::registerNormalizeGlobalPhases(); mqt::registerUnrollModifiers(); diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index e185e931d4..a43c834249 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -2251,7 +2251,7 @@ TEST_F(CompilerPipelineTest, PayloadControlLowersClassicalSCFIndexSwitch) { program->module(), TargetEnvironment(makeUnrestrictedTarget(), makeControlPayloadSpecification( {{.id = "forward-branching"}}))); - ASSERT_TRUE(program->runPassPipeline("legalize-payload-control-flow")); + ASSERT_TRUE(program->runPassPipeline("legalize-control-flow")); EXPECT_FALSE(StringRef(program->str()).contains("scf.index_switch")); EXPECT_EQ(StringRef(program->str()).count("scf.if"), 2U); } @@ -2366,8 +2366,8 @@ TEST_F(CompilerPipelineTest, nestedForCapture, }) { SCOPED_TRACE(source.str()); - for (const auto* pass : {"unroll-unsupported-payload-loops", - "legalize-payload-control-flow"}) { + for (const auto* pass : + {"unroll-loops-for-payload", "legalize-control-flow"}) { SCOPED_TRACE(pass); auto program = QCOProgram::fromMLIRString(source.str()); ASSERT_TRUE(program); From 1c5d4cc667046e9d3270898bfa658c786d74368d Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 9 Sep 2026 10:41:53 +0200 Subject: [PATCH 7/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Centralize=20control-f?= =?UTF-8?q?low=20capability=20IDs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the compiler snapshot identifiers beside ProgramCapability and ProgramConstraint so producers and legalization share their spellings. Do not depend on the draft QDMI capability API. Assisted-by: OpenAI Codex --- .../include/mlir/Compiler/TargetEnvironment.h | 14 +++++++++ .../Transforms/LegalizePayloadControlFlow.cpp | 29 ++++++++----------- .../Compiler/test_compiler_pipeline.cpp | 2 +- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/mlir/include/mlir/Compiler/TargetEnvironment.h b/mlir/include/mlir/Compiler/TargetEnvironment.h index a5f2739cde..eaaa1844c7 100644 --- a/mlir/include/mlir/Compiler/TargetEnvironment.h +++ b/mlir/include/mlir/Compiler/TargetEnvironment.h @@ -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; @@ -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 constraints; diff --git a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp index af266b76e0..76745600c7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp @@ -49,16 +49,6 @@ namespace mlir::qco { namespace { -constexpr llvm::StringLiteral FORWARD_BRANCHING = "forward-branching"; -constexpr llvm::StringLiteral COUNTED_ITERATION = "counted-iteration"; -constexpr llvm::StringLiteral CONDITIONAL_LOOP = "conditional-loop"; -constexpr llvm::StringLiteral MULTIWAY_BRANCHING = "multiway-branching"; - -constexpr llvm::StringLiteral MAX_NESTING_DEPTH = - "max-control-flow-nesting-depth"; -constexpr llvm::StringLiteral MAX_ITERATION_COUNT = "max-iteration-count"; -constexpr llvm::StringLiteral MAX_CASE_COUNT = "max-case-count"; - constexpr uint64_t MAX_UNROLLED_OPERATIONS = 65536U; enum class ControlFeature : uint8_t { @@ -93,10 +83,14 @@ class PayloadControlSupport { for (const ProgramCapability& capability : payload.capabilities()) { const auto feature = llvm::StringSwitch>(capability.id) - .Case(FORWARD_BRANCHING, ControlFeature::ForwardBranching) - .Case(COUNTED_ITERATION, ControlFeature::CountedIteration) - .Case(CONDITIONAL_LOOP, ControlFeature::ConditionalLoop) - .Case(MULTIWAY_BRANCHING, ControlFeature::MultiwayBranching) + .Case(ProgramCapability::FORWARD_BRANCHING, + ControlFeature::ForwardBranching) + .Case(ProgramCapability::COUNTED_ITERATION, + ControlFeature::CountedIteration) + .Case(ProgramCapability::CONDITIONAL_LOOP, + ControlFeature::ConditionalLoop) + .Case(ProgramCapability::MULTIWAY_BRANCHING, + ControlFeature::MultiwayBranching) .Default(std::nullopt); if (!feature) { continue; @@ -175,17 +169,18 @@ class PayloadControlSupport { group.usable = false; return; } - if (id == MAX_NESTING_DEPTH) { + if (id == ProgramConstraint::MAX_NESTING_DEPTH) { group.maxNestingDepth = value; return; } - if (id == MAX_ITERATION_COUNT && + if (id == ProgramConstraint::MAX_ITERATION_COUNT && (feature == ControlFeature::CountedIteration || feature == ControlFeature::ConditionalLoop)) { group.maxIterationCount = value; return; } - if (id == MAX_CASE_COUNT && feature == ControlFeature::MultiwayBranching) { + if (id == ProgramConstraint::MAX_CASE_COUNT && + feature == ControlFeature::MultiwayBranching) { group.maxCaseCount = value; return; } diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index a43c834249..2989df6899 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -260,7 +260,7 @@ makeCZTarget(std::initializer_list singleQubitGates) { }, { { - .id = "forward-branching", + .id = ProgramCapability::FORWARD_BRANCHING.str(), .constraints = { {