From 7f627262c34eb8e921d44a816cd11fe773a0e13d Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 24 Aug 2026 13:46:34 +0000 Subject: [PATCH 01/14] =?UTF-8?q?=E2=9C=A8=20Legalize=20control=20flow=20f?= =?UTF-8?q?or=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 | 589 ++++++++++++++++ 10 files changed, 1354 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 785e7105db..2c3ddec6f7 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], @@ -992,6 +992,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 8ce5c7703a..b96737e437 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -76,6 +76,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 10e9f66175..b53d9f6fe6 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 590cce8990..a72130fa19 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 147699cf1b..a2f3af7087 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -275,6 +275,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 + ")"; @@ -1974,6 +2035,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 1266c21dd1a9221b8d03999ebd39d8808cb7c08c Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 12:57:27 +0200 Subject: [PATCH 02/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Remove=20redundant?= =?UTF-8?q?=20modifier=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 fc3f6b5b212f8dc316a87be46d852d58e9bffecc Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 12:57:27 +0200 Subject: [PATCH 03/14] =?UTF-8?q?=F0=9F=93=9D=20Align=20control=20flow=20r?= =?UTF-8?q?elease=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 5220eb9eff9448374b28a96be552dad88f9691c8 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 8 Sep 2026 15:36:07 +0000 Subject: [PATCH 04/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20structure?= =?UTF-8?q?d=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 b96737e437..3fed363c07 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -78,11 +78,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 | | -------------------- | --------------------------------------------------- | @@ -102,13 +102,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 a72130fa19..0484960bc3 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); /// ponytail: CX/CZ-cost fusion can increase square-root iSWAP counts; 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 a2f3af7087..8147e4442c 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -2490,8 +2490,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} { @@ -2519,11 +2518,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 6a8bb7d467b0fcd5fd9169185105086abacc825e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 9 Sep 2026 10:28:15 +0200 Subject: [PATCH 05/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Validate=20loop=20in?= =?UTF-8?q?puts=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 3fed363c07..a26ba4cd7d 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -98,7 +98,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 b53d9f6fe6..d8ebebec2e 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 8147e4442c..4305c442f1 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -2433,6 +2433,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"}, @@ -2455,6 +2471,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 51fdc07b96f423e359e1d2e978d4ebd071073094 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 9 Sep 2026 10:37:53 +0200 Subject: [PATCH 06/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Clarify=20control-fl?= =?UTF-8?q?ow=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 a26ba4cd7d..35d618fe11 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -82,7 +82,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 d8ebebec2e..ced713a0ef 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 0484960bc3..d4f2d2aa7e 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); /// ponytail: CX/CZ-cost fusion can increase square-root iSWAP counts; 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 4305c442f1..ffc1007ad7 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -2363,7 +2363,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); } @@ -2478,8 +2478,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 0168bc6fc8950594f4e0d5dbca70a2190b030345 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 9 Sep 2026 10:41:53 +0200 Subject: [PATCH 07/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Centralize=20control?= =?UTF-8?q?-flow=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 ffc1007ad7..7603661f2e 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -262,7 +262,7 @@ makeCZTarget(std::initializer_list singleQubitGates) { }, { { - .id = "forward-branching", + .id = ProgramCapability::FORWARD_BRANCHING.str(), .constraints = { { From ab50ef51e2f3b295bf8f1afb5deefeb44214c08d Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 9 Sep 2026 12:31:16 +0200 Subject: [PATCH 08/14] =?UTF-8?q?=F0=9F=A7=AA=20Keep=20control=20fixtures?= =?UTF-8?q?=20allocation-independent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercise control-flow rejection with valid quantum inputs and remove unrelated quantum operations from classical fixtures. Assisted-by: OpenAI Codex --- .../Compiler/test_compiler_pipeline.cpp | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 7603661f2e..167276379d 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -2350,9 +2350,6 @@ TEST_F(CompilerPipelineTest, PayloadControlLowersClassicalSCFIndexSwitch) { } 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 } } @@ -2372,14 +2369,14 @@ TEST_F(CompilerPipelineTest, PayloadControlRejectsLinearStateInGenericSCFControl) { constexpr llvm::StringLiteral ifResult = R"mlir( module { - func.func @main(%condition: i1) + func.func @main(%condition: i1, %left: !qco.qubit, %right: !qco.qubit) attributes {mqt.entry_point} { %result = scf.if %condition -> !qco.qubit { - %left = qco.alloc : !qco.qubit - scf.yield %left : !qco.qubit + %x = qco.x %left : !qco.qubit -> !qco.qubit + scf.yield %x : !qco.qubit } else { - %right = qco.alloc : !qco.qubit - scf.yield %right : !qco.qubit + %h = qco.h %right : !qco.qubit -> !qco.qubit + scf.yield %h : !qco.qubit } qco.sink %result : !qco.qubit return @@ -2391,8 +2388,6 @@ TEST_F(CompilerPipelineTest, 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> @@ -2435,11 +2430,11 @@ TEST_F(CompilerPipelineTest, )mlir"; constexpr llvm::StringLiteral nestedForCapture = R"mlir( module { - func.func @main(%upper: index) attributes {mqt.entry_point} { + func.func @main(%upper: index, %q: !qco.qubit) + 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 @@ -2449,6 +2444,28 @@ TEST_F(CompilerPipelineTest, } } )mlir"; + constexpr llvm::StringLiteral nestedWhileCapture = R"mlir( + module { + func.func @main(%upper: index, %condition: i1, %q: !qco.qubit) + attributes {mqt.entry_point} { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %result = scf.for %outer = %c0 to %upper step %c1 + iter_args(%arg = %q) -> (!qco.qubit) { + %inner = scf.while : () -> !qco.qubit { + scf.condition(%condition) %arg : !qco.qubit + } do { + ^bb0(%value: !qco.qubit): + qco.sink %value : !qco.qubit + scf.yield + } + scf.yield %inner : !qco.qubit + } + qco.sink %result : !qco.qubit + return + } + } + )mlir"; const auto payload = makeControlPayloadSpecification({ {.id = "forward-branching"}, @@ -2476,6 +2493,7 @@ TEST_F(CompilerPipelineTest, forCapture, whileCapture, nestedForCapture, + nestedWhileCapture, }) { SCOPED_TRACE(source.str()); for (const auto* pass : @@ -2508,8 +2526,6 @@ TEST_F(CompilerPipelineTest, 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) @@ -2547,9 +2563,6 @@ TEST_F(CompilerPipelineTest, PayloadControlRejectsUnstructuredCFG) { %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 } } From 81f378faf83d2932ae32952aaabc05626083fa94 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 9 Sep 2026 12:36:10 +0200 Subject: [PATCH 09/14] =?UTF-8?q?=F0=9F=90=9B=20Check=20the=20width=20of?= =?UTF-8?q?=20fully=20unrolled=20loop=20steps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reject signed step truncation before native unrolling mutates the loop. Preserve safe signed and unsigned narrow-integer loops, and check their observed induction values. Assisted-by: OpenAI Codex --- .../Transforms/LegalizePayloadControlFlow.cpp | 13 +++- .../Compiler/test_compiler_pipeline.cpp | 68 +++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp index 76745600c7..84c41b2573 100644 --- a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -300,9 +301,15 @@ static LogicalResult foldStaticBranches(ModuleOp moduleOp) { int64_t scaledStep = 0; int64_t unrolledUpperBound = 0; - return llvm::MulOverflow(*step, static_cast(iterations), - scaledStep) == 0 && - llvm::AddOverflow(*lowerBound, scaledStep, unrolledUpperBound) == 0; + if (llvm::MulOverflow(*step, static_cast(iterations), scaledStep) || + llvm::AddOverflow(*lowerBound, scaledStep, unrolledUpperBound)) { + return false; + } + const auto type = dyn_cast(loop.getInductionVar().getType()); + return !type || + (loop.getUnsignedCmp() + ? llvm::isUIntN(type.getWidth(), static_cast(scaledStep)) + : llvm::isIntN(type.getWidth(), scaledStep)); } static void inlineDefaultRegion(Operation* operation, Block& block, diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 167276379d..fc9b15e40d 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -2287,6 +2287,74 @@ TEST_F(CompilerPipelineTest, PayloadControlBoundsFullUnrolling) { EXPECT_EQ(largeLoops, 1U); } +TEST_F(CompilerPipelineTest, + PayloadControlChecksUnrolledStepWidth) { + struct LoopCase { + int64_t lower; + int64_t upper; + int64_t step; + bool unsignedComparison; + bool safe; + }; + for (const auto& test : { + LoopCase{-120, 110, 80, false, false}, + LoopCase{-120, -10, 40, false, true}, + LoopCase{0, 110, 80, true, true}, + }) { + std::string source; + llvm::raw_string_ostream stream(source); + stream << "module {\n" + << " func.func private @observe(i8)\n" + << " func.func @main(%q: !qco.qubit) -> !qco.qubit " + "attributes {mqt.entry_point} {\n" + << " %lower = arith.constant " << test.lower << " : i8\n" + << " %upper = arith.constant " << test.upper << " : i8\n" + << " %step = arith.constant " << test.step << " : i8\n" + << " %result = scf.for " + << (test.unsignedComparison ? "unsigned " : "") + << "%i = %lower to %upper step %step " + "iter_args(%state = %q) -> (!qco.qubit) : i8 {\n" + << " func.call @observe(%i) : (i8) -> ()\n" + << " %next = qco.x %state : !qco.qubit -> !qco.qubit\n" + << " scf.yield %next : !qco.qubit\n" + << " }\n return %result : !qco.qubit\n }\n}\n"; + SCOPED_TRACE(source); + auto program = QCOProgram::fromMLIRString(source); + 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(); + }); + const bool transformed = program->runPassPipeline("unroll-loops-for-payload"); + ASSERT_EQ(transformed, test.safe) << diagnostics; + if (!test.safe) { + EXPECT_TRUE(StringRef(diagnostics).contains("cannot safely apply MLIR")); + EXPECT_EQ(program->str(), before); + continue; + } + EXPECT_TRUE(succeeded(verify(program->module()))); + std::vector observed; + program->module().walk([&](func::CallOp call) { + IntegerAttr value; + ASSERT_TRUE(matchPattern(call.getOperand(0), m_Constant(&value))); + observed.push_back(value.getInt()); + }); + std::vector expected; + for (auto value = test.lower; value < test.upper; value += test.step) { + expected.push_back(value); + } + EXPECT_EQ(observed, expected); + EXPECT_FALSE(StringRef(program->str()).contains("scf.for")); + } +} + TEST_F(CompilerPipelineTest, PayloadControlPreservesOrLowersQCOIndexSwitchAtBoundaries) { const auto target = makeUnrestrictedTarget(); From 0fb4b894ee02cb74289ed721c615a58712a15cce Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 9 Sep 2026 12:48:15 +0200 Subject: [PATCH 10/14] =?UTF-8?q?=F0=9F=90=9B=20Bound=20switch=20expansion?= =?UTF-8?q?=20before=20rewriting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Check payload depth and a compiler nesting ceiling before moving case regions. Build accepted chains iteratively and retain native multiway switches. Cover expansion boundaries, moved control, state selection, and cumulative loop cloning. Assisted-by: OpenAI Codex --- docs/mlir/target_compilation.md | 30 ++- .../mlir/Dialect/QCO/Transforms/Passes.td | 3 + .../Transforms/LegalizePayloadControlFlow.cpp | 95 +++++-- .../Compiler/test_compiler_pipeline.cpp | 238 +++++++++++++++++- 4 files changed, 325 insertions(+), 41 deletions(-) diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 35d618fe11..c8cc3b31ba 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -96,13 +96,19 @@ latter pass applies these structural capabilities to the remaining control flow: 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. 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. +index switch is lowered to a linear chain of nested forward branches when that +form fits the selected contract. Before expansion, the compiler checks the +selected forward-branching nesting limit and a compiler safety limit of 256 total +control-flow levels, including enclosing control flow. This compiler limit is +not a QDMI requirement and does not apply to switches retained under multiway +branching. + +Generic SCF branches cannot capture or return QCO qubits or quantum tensors; use +the corresponding QCO branch operation for linear quantum state. SCF loops must +carry linear quantum state through their iteration arguments instead of +capturing it. Both control-flow passes validate this loop input restriction +before transforming loops or lowering switches. It is separate from QCO's +exactly-one-SSA-use check. The supported constraints are `max-control-flow-nesting-depth` on all four capabilities, `max-iteration-count` on both iteration capabilities, and @@ -111,10 +117,12 @@ default region. One explicit case plus a default is a supported index switch and does not require forward branching. Limits are inclusive. The compiler must prove a constrained loop's trip count. It currently proves constant `scf.for` bounds and rejects a constrained `scf.while` because no general termination -bound is available. MLIR computes static trip counts; full unrolling -additionally requires bounds and scaled steps that fit its signed arithmetic. A -zero, unknown, or misapplied constraint makes that capability group unusable. -Missing or incomplete optional metadata never implies support. +bound is available. The proof requires literal loop bounds and a literal step; +it does not infer a trip count from symbolic bounds. MLIR computes static trip +counts; full unrolling additionally requires bounds and scaled steps that fit +its signed arithmetic. The scaled step must also fit the loop induction-variable +type. A zero, unknown, or misapplied constraint makes that capability group +unusable. Missing or incomplete optional metadata never implies support. This stage checks structural control flow only. Later lowering stages remain responsible for scalar types and operations, measurement provenance, function diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index ced713a0ef..6b77b38d46 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -138,6 +138,9 @@ def LegalizeControlFlow : Pass<"legalize-control-flow", "mlir::ModuleOp"> { QCO and SCF branches and loops, lowers unsupported index switches to nested forward branches when possible, and rejects residual structural control flow that the payload does not support. + Switch expansion checks the selected forward-branching depth limit before + rewriting and limits the resulting total control-flow nesting depth to 256. + This compiler safety limit does not restrict retained native switches. Run this pass after `unroll-loops-for-payload`, constant propagation, and QCO cleanup so newly constant control flow can fold before legality checks. Before conversion, the pass checks that all SCF loops carry linear QCO diff --git a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp index 84c41b2573..0749e119d2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp @@ -36,6 +36,7 @@ #include #include +#include #include #include #include @@ -51,6 +52,8 @@ namespace mlir::qco { namespace { constexpr uint64_t MAX_UNROLLED_OPERATIONS = 65536U; +// ponytail: bound linear expansion; use balanced trees for larger switches. +constexpr uint64_t MAX_SWITCH_EXPANSION_DEPTH = 256U; enum class ControlFeature : uint8_t { ForwardBranching, @@ -143,12 +146,40 @@ class PayloadControlSupport { return !group.maxCaseCount || caseCount <= *group.maxCaseCount; } + /// Check expansion of a switch with at least one explicit case. + [[nodiscard]] bool canLowerSwitch(Operation* operation, + uint64_t caseCount) const { + const auto& group = get(ControlFeature::ForwardBranching); + const auto depth = controlDepth(operation); + const auto maximum = + std::min(MAX_SWITCH_EXPANSION_DEPTH, + group.maxNestingDepth.value_or(MAX_SWITCH_EXPANSION_DEPTH)); + if (!group.usable || depth > maximum || caseCount > maximum - depth + 1U) { + return false; + } + // Region zero is the default; explicit case i gains i enclosing branches. + for (auto [index, region] : llvm::enumerate(operation->getRegions())) { + const auto extraDepth = index == 0 ? caseCount - 1U : index - 1U; + if (region + .walk([&](Operation* nested) { + return isStructuredControl(nested) && + controlDepth(nested) > + MAX_SWITCH_EXPANSION_DEPTH - extraDepth + ? WalkResult::interrupt() + : WalkResult::advance(); + }) + .wasInterrupted()) { + return false; + } + } + return true; + } + [[nodiscard]] static uint64_t controlDepth(Operation* operation) { uint64_t depth = 1U; for (Operation* parent = operation->getParentOp(); parent != nullptr; parent = parent->getParentOp()) { - if (isa(parent)) { + if (isStructuredControl(parent)) { ++depth; } } @@ -156,6 +187,11 @@ class PayloadControlSupport { } private: + [[nodiscard]] static bool isStructuredControl(Operation* operation) { + return isa(operation); + } + [[nodiscard]] CapabilityGroup& get(const ControlFeature feature) { return groups[static_cast(feature)]; } @@ -306,10 +342,10 @@ static LogicalResult foldStaticBranches(ModuleOp moduleOp) { return false; } const auto type = dyn_cast(loop.getInductionVar().getType()); - return !type || - (loop.getUnsignedCmp() - ? llvm::isUIntN(type.getWidth(), static_cast(scaledStep)) - : llvm::isIntN(type.getWidth(), scaledStep)); + return !type || (loop.getUnsignedCmp() + ? llvm::isUIntN(type.getWidth(), + static_cast(scaledStep)) + : llvm::isIntN(type.getWidth(), scaledStep)); } static void inlineDefaultRegion(Operation* operation, Block& block, @@ -341,13 +377,15 @@ class LowerQCOIndexSwitch final : public OpConversionPattern { adaptor.getTargets(), rewriter); return success(); } - if (!support->get(ControlFeature::ForwardBranching).usable) { + if (!support->canLowerSwitch(operation, cases.size())) { return rewriter.notifyMatchFailure( - operation, "selected payload cannot use forward branches"); + operation, + "switch requires unsupported or excessive branching depth"); } - const auto build = [&](auto&& self, const size_t index, - ValueRange targets) -> IfOp { + IfOp replacement; + ValueRange targets = adaptor.getTargets(); + for (size_t index = 0; index < cases.size(); ++index) { auto constant = arith::ConstantIndexOp::create( rewriter, operation.getLoc(), operation.getCases()[index]); auto condition = arith::CmpIOp::create( @@ -357,25 +395,26 @@ class LowerQCOIndexSwitch final : public OpConversionPattern { operation.getClassicalResults().getTypes(), operation.getLinearResults().getTypes(), condition, targets); + if (index == 0) { + replacement = ifOp; + } else { + YieldOp::create(rewriter, operation.getLoc(), ifOp.getResults()); + } rewriter.inlineRegionBefore(cases[index], ifOp.getThenRegion(), ifOp.getThenRegion().end()); if (index + 1U == cases.size()) { rewriter.inlineRegionBefore(*defaultRegion, ifOp.getElseRegion(), ifOp.getElseRegion().end()); - return ifOp; + break; } 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; - }; + targets = elseBlock.getArguments(); + } - IfOp replacement = build(build, 0U, adaptor.getTargets()); rewriter.replaceOp(operation, replacement.getResults()); return success(); } @@ -406,12 +445,14 @@ class LowerSCFIndexSwitch final inlineDefaultRegion(operation, defaultRegion->front(), {}, rewriter); return success(); } - if (!support->get(ControlFeature::ForwardBranching).usable) { + if (!support->canLowerSwitch(operation, cases.size())) { return rewriter.notifyMatchFailure( - operation, "selected payload cannot use forward branches"); + operation, + "switch requires unsupported or excessive branching depth"); } - const auto build = [&](auto&& self, const size_t index) -> scf::IfOp { + scf::IfOp replacement; + for (size_t index = 0; index < cases.size(); ++index) { auto constant = arith::ConstantIndexOp::create( rewriter, operation.getLoc(), operation.getCases()[index]); auto condition = arith::CmpIOp::create( @@ -420,6 +461,11 @@ class LowerSCFIndexSwitch final auto ifOp = scf::IfOp::create(rewriter, operation.getLoc(), operation.getResultTypes(), condition, true); + if (index == 0) { + replacement = ifOp; + } else { + scf::YieldOp::create(rewriter, operation.getLoc(), ifOp.getResults()); + } rewriter.eraseBlock(&ifOp.getThenRegion().front()); rewriter.eraseBlock(&ifOp.getElseRegion().front()); rewriter.inlineRegionBefore(cases[index], ifOp.getThenRegion(), @@ -427,18 +473,13 @@ class LowerSCFIndexSwitch final if (index + 1U == cases.size()) { rewriter.inlineRegionBefore(*defaultRegion, ifOp.getElseRegion(), ifOp.getElseRegion().end()); - return ifOp; + break; } 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(); } diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index fc9b15e40d..33503e3b3e 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -23,6 +23,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" @@ -2287,8 +2288,7 @@ TEST_F(CompilerPipelineTest, PayloadControlBoundsFullUnrolling) { EXPECT_EQ(largeLoops, 1U); } -TEST_F(CompilerPipelineTest, - PayloadControlChecksUnrolledStepWidth) { +TEST_F(CompilerPipelineTest, PayloadControlChecksUnrolledStepWidth) { struct LoopCase { int64_t lower; int64_t upper; @@ -2299,6 +2299,8 @@ TEST_F(CompilerPipelineTest, for (const auto& test : { LoopCase{-120, 110, 80, false, false}, LoopCase{-120, -10, 40, false, true}, + LoopCase{-120, -120, 40, false, true}, + LoopCase{-120, -119, 40, false, true}, LoopCase{0, 110, 80, true, true}, }) { std::string source; @@ -2332,7 +2334,8 @@ TEST_F(CompilerPipelineTest, diagnostics += diagnostic.str(); return success(); }); - const bool transformed = program->runPassPipeline("unroll-loops-for-payload"); + const bool transformed = + program->runPassPipeline("unroll-loops-for-payload"); ASSERT_EQ(transformed, test.safe) << diagnostics; if (!test.safe) { EXPECT_TRUE(StringRef(diagnostics).contains("cannot safely apply MLIR")); @@ -2355,6 +2358,52 @@ TEST_F(CompilerPipelineTest, } } +TEST_F(CompilerPipelineTest, PayloadControlBoundsTotalLoopCloning) { + for (const auto trips : {32769, 32770}) { + SCOPED_TRACE(trips); + std::string source; + llvm::raw_string_ostream stream(source); + stream << "module {\n" + << " func.func @main(%q: !qco.qubit) -> !qco.qubit " + "attributes {mqt.entry_point} {\n" + << " %c0 = arith.constant 0 : index\n" + << " %c1 = arith.constant 1 : index\n" + << " %limit = arith.constant " << trips << " : index\n"; + for (const auto index : {0, 1}) { + stream << " %out" << index << " = scf.for %i" << index + << " = %c0 to %limit step %c1 iter_args(%state" << index << " = " + << (index == 0 ? "%q" : "%out0") << ") -> (!qco.qubit) {\n" + << " %next" << index << " = qco.x %state" << index + << " : !qco.qubit -> !qco.qubit\n" + << " scf.yield %next" << index << " : !qco.qubit\n }\n"; + } + stream << " return %out1 : !qco.qubit\n }\n}\n"; + auto program = QCOProgram::fromMLIRString(source); + ASSERT_TRUE(program); + attachTargetEnvironment( + program->module(), + TargetEnvironment(makeUnrestrictedTarget(), + makeControlPayloadSpecification({}))); + std::string diagnostics; + ScopedDiagnosticHandler handler(program->module()->getContext(), + [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_EQ(program->runPassPipeline("unroll-loops-for-payload"), + trips == 32769) + << diagnostics; + EXPECT_TRUE(succeeded(verify(program->module()))); + EXPECT_TRUE(succeeded(qco::verifyLinearity(program->module()))); + if (trips == 32770) { + EXPECT_TRUE( + StringRef(diagnostics).contains("65536 loop-body operations")); + } else { + EXPECT_FALSE(StringRef(program->str()).contains("scf.for")); + } + } +} + TEST_F(CompilerPipelineTest, PayloadControlPreservesOrLowersQCOIndexSwitchAtBoundaries) { const auto target = makeUnrestrictedTarget(); @@ -2433,6 +2482,189 @@ TEST_F(CompilerPipelineTest, PayloadControlLowersClassicalSCFIndexSwitch) { EXPECT_EQ(StringRef(program->str()).count("scf.if"), 2U); } +[[nodiscard]] static std::string +makePayloadSwitchSource(size_t cases, bool quantum, + StringRef lastCaseBody = {}) { + std::string source = "module {\n"; + if (!lastCaseBody.empty()) { + source += "func.func private @effect()\n"; + } + source += "func.func @main(%selector: index, %condition: i1"; + source += quantum ? ", %q: !qco.qubit) -> (i64, !qco.qubit)\n" : ") -> i64\n"; + source += "attributes {mqt.entry_point} {\n"; + source += + quantum + ? "%result, %out = qco.index_switch %selector -> (i64, !qco.qubit)\n" + : "%result = scf.index_switch %selector -> i64\n"; + for (size_t i = 0; i <= cases; ++i) { + source += i == cases ? "default" : "case " + std::to_string(i); + source += quantum ? " args(%arg = %q) {\n" : " {\n"; + if (i + 1 == cases) { + source += lastCaseBody.str(); + } + source += "%value = arith.constant " + + (i == cases ? "-1" : std::to_string(i)) + " : i64\n"; + source += quantum ? "qco.yield %value, %arg : i64, !qco.qubit\n}\n" + : "scf.yield %value : i64\n}\n"; + } + source += quantum ? "return %result, %out : i64, !qco.qubit\n" + : "return %result : i64\n"; + return source + "}\n}\n"; +} + +TEST_F(CompilerPipelineTest, PayloadControlBoundsSwitchLowering) { + for (bool quantum : {false, true}) { + SCOPED_TRACE(quantum); + for (const auto& [cases, native, depth, expected] : + std::vector>{ + {256, false, 0, true}, + {257, false, 0, false}, + {5000, false, 0, false}, + {5000, true, 0, true}, + {2, false, 1, false}, + {2, false, 2, true}}) { + SCOPED_TRACE(cases); + SCOPED_TRACE(native); + SCOPED_TRACE(depth); + auto program = + QCOProgram::fromMLIRString(makePayloadSwitchSource(cases, quantum)); + ASSERT_TRUE(program); + ProgramCapability capability{.id = native ? "multiway-branching" + : "forward-branching"}; + if (depth != 0) { + capability.constraints.push_back( + {.id = "max-control-flow-nesting-depth", .value = depth}); + } + attachTargetEnvironment( + program->module(), + TargetEnvironment(makeUnrestrictedTarget(), + makeControlPayloadSpecification({capability}))); + const auto before = program->str(); + ScopedDiagnosticHandler handler(program->module()->getContext(), + [](Diagnostic&) { return success(); }); + EXPECT_EQ(program->runPassPipeline("legalize-control-flow"), expected); + EXPECT_TRUE(succeeded(verify(program->module()))); + EXPECT_TRUE(succeeded(qco::verifyLinearity(program->module()))); + if (expected) { + EXPECT_EQ(StringRef(program->str()).contains("index_switch"), native); + } else { + EXPECT_EQ(program->str(), before); + } + } + } +} + +TEST_F(CompilerPipelineTest, PayloadControlChecksMovedCaseDepthByCapability) { + constexpr llvm::StringLiteral nestedBranch = R"mlir( + scf.if %condition { + func.call @effect() : () -> () + } + )mlir"; + constexpr llvm::StringLiteral nestedLoop = R"mlir( + scf.while : () -> () { + scf.condition(%condition) + } do { + func.call @effect() : () -> () + scf.yield + } + )mlir"; + for (bool quantum : {false, true}) { + for (bool loop : {false, true}) { + SCOPED_TRACE(quantum); + SCOPED_TRACE(loop); + auto program = QCOProgram::fromMLIRString(makePayloadSwitchSource( + 2, quantum, loop ? nestedLoop : nestedBranch)); + ASSERT_TRUE(program); + attachTargetEnvironment( + program->module(), + TargetEnvironment( + makeUnrestrictedTarget(), + makeControlPayloadSpecification( + {{.id = "forward-branching", + .constraints = {{.id = "max-control-flow-nesting-depth", + .value = 2}}}, + {.id = "conditional-loop", + .constraints = {{.id = "max-control-flow-nesting-depth", + .value = 3}}}}))); + const auto before = program->str(); + ScopedDiagnosticHandler handler(program->module()->getContext(), + [](Diagnostic&) { return success(); }); + EXPECT_EQ(program->runPassPipeline("legalize-control-flow"), loop); + EXPECT_TRUE(succeeded(verify(program->module()))); + EXPECT_TRUE(succeeded(qco::verifyLinearity(program->module()))); + if (!loop) { + EXPECT_EQ(program->str(), before); + } + } + } +} + +TEST_F(CompilerPipelineTest, PayloadControlPreservesSwitchSelectionAndState) { + for (bool quantum : {false, true}) { + for (int64_t selector : {0, 1, 2, 99}) { + SCOPED_TRACE(quantum); + SCOPED_TRACE(selector); + auto program = + QCOProgram::fromMLIRString(makePayloadSwitchSource(3, quantum)); + ASSERT_TRUE(program); + attachTargetEnvironment( + program->module(), + TargetEnvironment( + makeUnrestrictedTarget(), + makeControlPayloadSpecification({{.id = "forward-branching"}}))); + ASSERT_TRUE(program->runPassPipeline("legalize-control-flow")); + EXPECT_FALSE(StringRef(program->str()).contains("index_switch")); + auto function = program->module().lookupSymbol("main"); + OpBuilder builder(function.getContext()); + builder.setInsertionPointToStart(&function.getBody().front()); + auto value = + arith::ConstantIndexOp::create(builder, function.getLoc(), selector); + function.getArgument(0).replaceAllUsesWith(value.getResult()); + ASSERT_TRUE(program->runPassPipeline("canonicalize")); + auto result = + cast(function.getBody().front().getTerminator()); + IntegerAttr returned; + ASSERT_TRUE(matchPattern(result.getOperand(0), m_Constant(&returned))); + EXPECT_EQ(returned.getInt(), selector == 99 ? -1 : selector); + EXPECT_TRUE(succeeded(qco::verifyLinearity(program->module()))); + if (quantum) { + EXPECT_EQ(result.getOperand(1), function.getArgument(2)); + } + } + } +} + +TEST_F(CompilerPipelineTest, PayloadControlBoundsMovedCaseDepth) { + for (bool quantum : {false, true}) { + for (size_t cases : {255U, 256U}) { + SCOPED_TRACE(quantum); + SCOPED_TRACE(cases); + auto program = QCOProgram::fromMLIRString( + makePayloadSwitchSource(cases, quantum, R"mlir( + scf.if %condition { + func.call @effect() : () -> () + } + )mlir")); + ASSERT_TRUE(program); + attachTargetEnvironment( + program->module(), + TargetEnvironment( + makeUnrestrictedTarget(), + makeControlPayloadSpecification({{.id = "forward-branching"}}))); + const auto before = program->str(); + ScopedDiagnosticHandler handler(program->module()->getContext(), + [](Diagnostic&) { return success(); }); + EXPECT_EQ(program->runPassPipeline("legalize-control-flow"), + cases == 255); + EXPECT_TRUE(succeeded(verify(program->module()))); + EXPECT_TRUE(succeeded(qco::verifyLinearity(program->module()))); + if (cases == 256) { + EXPECT_EQ(program->str(), before); + } + } + } +} + TEST_F(CompilerPipelineTest, PayloadControlRejectsLinearStateInGenericSCFControl) { constexpr llvm::StringLiteral ifResult = R"mlir( From 8c678f02ae9817fac094547633b7322c4b45bf3e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 9 Sep 2026 13:06:00 +0200 Subject: [PATCH 11/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Reuse=20native=20tri?= =?UTF-8?q?vial-loop=20promotion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reuse the native zero/one-trip utility, apply required C++ initializer style, and replace stale local handoff notes with scoped validation. Keep the switch safety comment purely technical. Assisted-by: OpenAI Codex --- .../payload-control-flow-legalization.md | 78 +++++++++---------- docs/mlir/target_compilation.md | 6 +- .../Transforms/LegalizePayloadControlFlow.cpp | 16 ++-- .../Compiler/test_compiler_pipeline.cpp | 77 ++++++++++++++---- 4 files changed, 107 insertions(+), 70 deletions(-) diff --git a/.agent/plans/payload-control-flow-legalization.md b/.agent/plans/payload-control-flow-legalization.md index 12db6a9256..9e64841acb 100644 --- a/.agent/plans/payload-control-flow-legalization.md +++ b/.agent/plans/payload-control-flow-legalization.md @@ -1,48 +1,40 @@ # Compiler-only control-flow legalization -Status: implemented. - -## Scope and release boundary - -Core #2162 follows #2219 without QDMI runtime or adapter ancestry and targets -Core 4.0. Core #2365 and QDMI #523 track the separate Core 4.1/QDMI 1.4 -adaptation and do not gate this prototype. The rebase preserves the capability -snapshot; human review must still settle the provider-neutral vocabulary. - -Legalize structural control flow against the selected target environment. Retain -supported constructs, lower unsupported static loops and switches where -possible, and fail closed when residual control flow cannot be represented. -Scalar computation, measurement provenance, allocation, functions and final QIR -profile verification remain separate work. - -## Implementation - -Keep two passes in one source: bounded static-loop unrolling before cleanup, -then dialect conversion for residual branches and loops. Reuse MLIR symbol DCE, -SCCP, native static trip counts, loop unrolling and conversion legality. Require -structured QCO/SCF input; producers normalize CFG branches before compilation. -Preserve literal-bound proofs, the 65,536 cloned-operation limit, and signed -arithmetic safety checks for full unrolling. Reject invalid linear captures; -carry quantum values explicitly through regions. - -The canonical pipeline receives one selected TargetEnvironment and shares its -prepared target with all passes. It does not reintroduce removed cleanup passes -or unknown-target fallbacks. Both legalization passes consume the existing -cached TargetEnvironmentAnalysis. Capability names and constraints remain -provisional. - -Single-case switches require only multiway branching. Cleanup after mapping -remains; the redundant cleanup immediately after control legalization is -removed. +Status: implemented. Latest local validation: 2026-09-09. + +## Scope + +Legalize structured QCO/SCF control flow for the selected payload. Producers +normalize CFG branches before target compilation. Scalar operations, measurement +provenance, allocation, functions, and final payload-profile verification remain +separate checks. + +The implementation is in +`mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp`; compiler tests +are in `mlir/unittests/Compiler/test_compiler_pipeline.cpp`. Public contracts +are in `docs/mlir/target_compilation.md` and the QCO `Passes.td`. + +## Decisions + +- Keep SCCP and QCO cleanup between unrolling and residual legality checks: + unrolling exposes constant bounds and branches. +- Reuse MLIR trip counts, zero/one-trip promotion, and full unrolling. Require + literal bounds, signed-arithmetic safety, and a scaled step that fits the IV + type. Limit the pass to 65,536 cloned body operations. +- Build switch fallbacks iteratively. Preflight the payload's branch-depth limit + and a compiler limit of 256 total control-flow levels, including moved case + bodies. Retained native multiway switches do not use this expansion limit. +- Require explicit quantum iteration arguments and QCO branch state transport. + Exactly one SSA use does not exclude captures in repeated regions. Keep + negative fixtures valid under allocation verification so they test this rule. +- Reuse the cached `TargetEnvironment`. Capability IDs remain a compiler + snapshot; the QDMI adapter and final payload-profile checks stay separate. ## Validation -The optimized native build passed all 3,217 configured tests, with one existing -optional-device skip. The compiler suite passed all 191 tests, including early -CFG rejection, runtime assertions, single-case quantum and classical switches, -full-width trip counts, unroll bounds, and linear-state constraints. MLIR -documentation, repository lint, and whole changed-file C++ lint passed. - -Simon Hofmann's human co-authorship and the existing review history are -preserved. The child commits are restacked on the shared-environment -implementation in #2219. +After rebasing on main `2bd6a88e1`, the LLVM/MLIR 23.1.0 release build passed +all 201 compiler tests. The full native suite passed 3,388 tests with one +optional `QueryJobId` skip. MLIR documentation generation, repository lint, and +whole changed-file C++ lint passed. Focused regressions cover IV values, +cumulative cloning, switch depth, case/default selection, quantum-state +forwarding, and invalid captures. diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index c8cc3b31ba..6133f7d02c 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -98,9 +98,9 @@ unrolled when this clones at most 65,536 body operations. Cleanup runs again because unrolling can make nested bounds and conditions constant. An unsupported index switch is lowered to a linear chain of nested forward branches when that form fits the selected contract. Before expansion, the compiler checks the -selected forward-branching nesting limit and a compiler safety limit of 256 total -control-flow levels, including enclosing control flow. This compiler limit is -not a QDMI requirement and does not apply to switches retained under multiway +selected forward-branching nesting limit and a compiler safety limit of 256 +total control-flow levels, including enclosing control flow. This compiler limit +is not a QDMI requirement and does not apply to switches retained under multiway branching. Generic SCF branches cannot capture or return QCO qubits or quantum tensors; use diff --git a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp index 0749e119d2..13e55b16a5 100644 --- a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp @@ -52,7 +52,7 @@ namespace mlir::qco { namespace { constexpr uint64_t MAX_UNROLLED_OPERATIONS = 65536U; -// ponytail: bound linear expansion; use balanced trees for larger switches. +// Bound generated nesting to keep recursive IR processing manageable. constexpr uint64_t MAX_SWITCH_EXPANSION_DEPTH = 256U; enum class ControlFeature : uint8_t { @@ -337,8 +337,9 @@ static LogicalResult foldStaticBranches(ModuleOp moduleOp) { int64_t scaledStep = 0; int64_t unrolledUpperBound = 0; - if (llvm::MulOverflow(*step, static_cast(iterations), scaledStep) || - llvm::AddOverflow(*lowerBound, scaledStep, unrolledUpperBound)) { + if (llvm::MulOverflow(*step, static_cast(iterations), scaledStep) != + 0 || + llvm::AddOverflow(*lowerBound, scaledStep, unrolledUpperBound) != 0) { return false; } const auto type = dyn_cast(loop.getInductionVar().getType()); @@ -528,13 +529,10 @@ struct UnrollLoopsForPayload final } for (auto& [loop, tripCount] : loops) { - if (tripCount.isZero()) { - rewriter.replaceOp(loop, loop.getInitArgs()); - continue; - } - if (tripCount.isOne()) { + if (tripCount.ule(1)) { if (failed(loop.promoteIfSingleIteration(rewriter))) { - loop.emitError("failed to promote a single-iteration loop"); + loop.emitError( + "failed to simplify a zero- or single-iteration loop"); signalPassFailure(); return; } diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 33503e3b3e..4958377d78 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -2297,11 +2297,41 @@ TEST_F(CompilerPipelineTest, PayloadControlChecksUnrolledStepWidth) { bool safe; }; for (const auto& test : { - LoopCase{-120, 110, 80, false, false}, - LoopCase{-120, -10, 40, false, true}, - LoopCase{-120, -120, 40, false, true}, - LoopCase{-120, -119, 40, false, true}, - LoopCase{0, 110, 80, true, true}, + LoopCase{ + .lower = -120, + .upper = 110, + .step = 80, + .unsignedComparison = false, + .safe = false, + }, + LoopCase{ + .lower = -120, + .upper = -10, + .step = 40, + .unsignedComparison = false, + .safe = true, + }, + LoopCase{ + .lower = -120, + .upper = -120, + .step = 40, + .unsignedComparison = false, + .safe = true, + }, + LoopCase{ + .lower = -120, + .upper = -119, + .step = 40, + .unsignedComparison = false, + .safe = true, + }, + LoopCase{ + .lower = 0, + .upper = 110, + .step = 80, + .unsignedComparison = true, + .safe = true, + }, }) { std::string source; llvm::raw_string_ostream stream(source); @@ -2522,15 +2552,17 @@ TEST_F(CompilerPipelineTest, PayloadControlBoundsSwitchLowering) { {5000, false, 0, false}, {5000, true, 0, true}, {2, false, 1, false}, - {2, false, 2, true}}) { + {2, false, 2, true}, + }) { SCOPED_TRACE(cases); SCOPED_TRACE(native); SCOPED_TRACE(depth); auto program = QCOProgram::fromMLIRString(makePayloadSwitchSource(cases, quantum)); ASSERT_TRUE(program); - ProgramCapability capability{.id = native ? "multiway-branching" - : "forward-branching"}; + ProgramCapability capability{ + .id = native ? "multiway-branching" : "forward-branching", + }; if (depth != 0) { capability.constraints.push_back( {.id = "max-control-flow-nesting-depth", .value = depth}); @@ -2579,13 +2611,28 @@ TEST_F(CompilerPipelineTest, PayloadControlChecksMovedCaseDepthByCapability) { program->module(), TargetEnvironment( makeUnrestrictedTarget(), - makeControlPayloadSpecification( - {{.id = "forward-branching", - .constraints = {{.id = "max-control-flow-nesting-depth", - .value = 2}}}, - {.id = "conditional-loop", - .constraints = {{.id = "max-control-flow-nesting-depth", - .value = 3}}}}))); + makeControlPayloadSpecification({ + { + .id = "forward-branching", + .constraints = + { + { + .id = "max-control-flow-nesting-depth", + .value = 2, + }, + }, + }, + { + .id = "conditional-loop", + .constraints = + { + { + .id = "max-control-flow-nesting-depth", + .value = 3, + }, + }, + }, + }))); const auto before = program->str(); ScopedDiagnosticHandler handler(program->module()->getContext(), [](Diagnostic&) { return success(); }); From 776f9601c0434d137d8edfd767161235b874dbb9 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 9 Sep 2026 13:39:31 +0200 Subject: [PATCH 12/14] =?UTF-8?q?=F0=9F=A7=AA=20Exercise=20payload=20limit?= =?UTF-8?q?s=20through=20frontends?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace three handwritten positive fixtures with OpenQASM imports and test a live Qiskit classical switch against selected capabilities. Keep the focused raw-MLIR safety regressions and production contract unchanged. Assisted-by: OpenAI Codex --- .../Compiler/test_compiler_pipeline.cpp | 138 +++++++++--------- test/python/test_mlir_qiskit_translation.py | 42 ++++++ 2 files changed, 111 insertions(+), 69 deletions(-) diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 4958377d78..cefd8a87a1 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -2036,6 +2036,8 @@ TEST_F(CompilerPipelineTest, QCOProgramCompilesForTarget) { unsupportedQCO->module()->hasAttr(mlir::mqt::TargetEnvAttr::name)); } +// Handwritten MLIR isolates unsupported-input and capability-boundary behavior. +// Frontend-backed cases below are named FromOpenQASM. TEST_F(CompilerPipelineTest, PayloadControlRejectsUnsupportedResidualOperations) { constexpr llvm::StringLiteral forwardBranch = R"mlir( @@ -2126,24 +2128,18 @@ TEST_F(CompilerPipelineTest, } 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"; + PayloadControlFromOpenQASMUsesInclusiveIterationConstraint) { + constexpr llvm::StringLiteral source = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +for int i in [0:2] { x q; } +)qasm"; + auto qc = QCProgram::fromQASMString(source); + ASSERT_TRUE(qc); + auto input = std::move(*qc).intoQCO(); + ASSERT_TRUE(input); + ASSERT_TRUE(StringRef(input->str()).contains("scf.for")); const auto payload = [](const uint64_t maximum) { return makeControlPayloadSpecification({ { @@ -2153,52 +2149,37 @@ TEST_F(CompilerPipelineTest, }); }; - auto atBoundary = QCOProgram::fromMLIRString(source.str()); - ASSERT_TRUE(atBoundary); - ASSERT_TRUE(atBoundary->compileForTarget( + auto atBoundary = input->copy(); + ASSERT_TRUE(atBoundary.compileForTarget( TargetEnvironment(makeUnrestrictedTarget(), payload(3)))); - EXPECT_TRUE(StringRef(atBoundary->str()).contains("scf.for")); + EXPECT_TRUE(StringRef(atBoundary.str()).contains("scf.for")); - auto aboveBoundary = QCOProgram::fromMLIRString(source.str()); - ASSERT_TRUE(aboveBoundary); - ASSERT_TRUE(aboveBoundary->compileForTarget( + auto aboveBoundary = input->copy(); + ASSERT_TRUE(aboveBoundary.compileForTarget( TargetEnvironment(makeUnrestrictedTarget(), payload(2)))); - EXPECT_FALSE(StringRef(aboveBoundary->str()).contains("scf.for")); + 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"; + PayloadControlFromOpenQASMUnrollsNewlyStaticNestedLoops) { + constexpr llvm::StringLiteral source = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +for int outer in [0:2] { + for int inner in [0:outer - 1] { + if (inner == 0) { x q; } + else { h q; } + } +} +)qasm"; - auto program = QCOProgram::fromMLIRString(source.str()); + auto qc = QCProgram::fromQASMString(source); + ASSERT_TRUE(qc); + auto program = std::move(*qc).intoQCO(); ASSERT_TRUE(program); + ASSERT_EQ(StringRef(program->str()).count("scf.for"), 2U); + ASSERT_TRUE(StringRef(program->str()).contains("qco.if")); ASSERT_TRUE(program->compileForTarget(TargetEnvironment( makeUnrestrictedTarget(), makeControlPayloadSpecification({}, true)))); EXPECT_FALSE(StringRef(program->str()).contains("scf.for")); @@ -2435,7 +2416,29 @@ TEST_F(CompilerPipelineTest, PayloadControlBoundsTotalLoopCloning) { } TEST_F(CompilerPipelineTest, - PayloadControlPreservesOrLowersQCOIndexSwitchAtBoundaries) { + PayloadControlFromOpenQASMPreservesOrLowersSwitchAtBoundaries) { + constexpr llvm::StringLiteral source = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +bit[2] bits; +h q; +bits[0] = measure q; +h q; +bits[1] = measure q; +uint[2] selector = uint[2](bits); +output int result; +switch (selector) { + case 0 { x q; result = 0; } + case 1 { h q; result = 1; } + default { result = 2; } +} +)qasm"; + auto qc = QCProgram::fromQASMString(source); + ASSERT_TRUE(qc); + auto input = std::move(*qc).intoQCO(); + ASSERT_TRUE(input); + ASSERT_TRUE(StringRef(input->str()).contains("qco.index_switch")); const auto target = makeUnrestrictedTarget(); const auto multiway = [](const uint64_t maximum) { return ProgramCapability{ @@ -2453,24 +2456,21 @@ TEST_F(CompilerPipelineTest, }; }; - auto preserved = QCOProgram::fromMLIRString(QCO_INDEX_SWITCH_SOURCE.str()); - ASSERT_TRUE(preserved); - ASSERT_TRUE(preserved->compileForTarget(TargetEnvironment( + auto preserved = input->copy(); + ASSERT_TRUE(preserved.compileForTarget(TargetEnvironment( target, makeControlPayloadSpecification({multiway(2)})))); - EXPECT_TRUE(StringRef(preserved->str()).contains("qco.index_switch")); + 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( + auto lowered = input->copy(); + 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); + 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); + auto tooDeep = input->copy(); std::string diagnostics; EXPECT_FALSE(compileForTargetWithDiagnostics( - *tooDeep, makeControlPayloadSpecification({multiway(1), forward(1)}), + tooDeep, makeControlPayloadSpecification({multiway(1), forward(1)}), diagnostics)); EXPECT_TRUE(StringRef(diagnostics).contains("qco.index_switch")) << diagnostics; diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index be43030bd2..8e585709fe 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -46,6 +46,7 @@ PayloadEncoding, PayloadFormat, PayloadSpecification, + ProgramCapability, QCProgram, TargetEnvironment, compile_program, @@ -2018,6 +2019,47 @@ def test_nested_structured_control_and_bound_loop_parameter() -> None: QCProgram.from_qiskit(restored) +@pytest.mark.parametrize("capability", ["multiway-branching", "forward-branching", None]) +def test_classical_switch_compiles_for_selected_payload_capabilities(capability: str | None) -> None: + """Preserve, lower, or reject a live imported classical switch.""" + circuit = QuantumCircuit(1, 2) + circuit.h(0) + circuit.measure(0, 0) + circuit.h(0) + circuit.measure(0, 1) + with circuit.switch(circuit.cregs[0], None, None, None, label=None) as case: + with case(0): + circuit.store(circuit.cregs[0], expr.lift(3, types.Uint(2))) + with case(1): + circuit.store(circuit.cregs[0], expr.lift(2, types.Uint(2))) + with case(case.DEFAULT): + circuit.store(circuit.cregs[0], expr.lift(0, types.Uint(2))) + original = circuit.copy() + program = QCProgram.from_qiskit(circuit).to_qco() + assert "scf.index_switch" in program.ir + + target = CompilerTarget( + 1, + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations.unrestricted(), + ) + payload = PayloadSpecification( + PayloadFormat("qir", "2.1.0", "adaptive", PayloadEncoding.BINARY), + [ProgramCapability(capability)] if capability is not None else [], + optional_capabilities_known=True, + ) + environment = TargetEnvironment(target, payload) + if capability is None: + with pytest.raises(RuntimeError, match="MLIR operation failed"): + program.compile_for_target(environment) + else: + program.compile_for_target(environment) + assert program.is_valid + assert ("scf.index_switch" in program.ir) is (capability == "multiway-branching") + assert ("scf.if" in program.ir) is (capability == "forward-branching") + assert circuit == original + + def test_control_flow_and_controlled_unitary_preserve_instruction_order() -> None: """Keep both deferred instruction kinds at their original positions.""" circuit = QuantumCircuit(2, 1) From 276bab8cf5d7a657b30b4bbe18e0d1104aa2a21d Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 9 Sep 2026 13:45:24 +0200 Subject: [PATCH 13/14] =?UTF-8?q?=F0=9F=93=9D=20Group=20payload=20legaliza?= =?UTF-8?q?tion=20with=20target=20compilation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the #2162 reference beside #2219 and describe payload-aware control-flow legalization in the existing target-compilation entry. Assisted-by: OpenAI Codex --- CHANGELOG.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c3ddec6f7..21adabf5db 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], [#2162], [#2194], [#2210], [#2211], [#2215], - [#2218], [#2220], [#2323], [#2336], [#2344]) ([**@burgholzer**], - [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], - [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**], [**@J4MMlE**]) + [#2149], [#2150], [#2158], [#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], @@ -38,11 +38,12 @@ releases may include breaking changes. direct lowering and dense-array helpers for supported compiler inputs ([#1915], [#1973], [#2077], [#2078], [#2079], [#2334]) ([**@simon1hofmann**], [**@burgholzer**]) -- ✨ Add immutable MLIR compiler targets, selected payload specifications, QDMI - device integration, ordered operation applicability, directional native - synthesis, and target compilation through C++, Python, and `mqt-cc` ([#2285], - [#2219], [#2049], [#1999], [#1993], [#1687]) ([**@MatthiasReumann**], - [**@simon1hofmann**], [**@burgholzer**]) +- ✨ Add immutable MLIR compiler targets, selected payload specifications, + payload-aware control-flow legalization, QDMI device integration, ordered + operation applicability, directional native synthesis, and target compilation + through C++, Python, and `mqt-cc` ([#2285], [#2219], [#2162], [#2049], + [#1999], [#1993], [#1687]) ([**@MatthiasReumann**], [**@simon1hofmann**], + [**@burgholzer**]) #### Import and export From dcf86450e794615cd8422639cb5d41cc74cf1b6b Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 9 Sep 2026 14:49:05 +0000 Subject: [PATCH 14/14] =?UTF-8?q?=F0=9F=90=9B=20Legalize=20empty=20and=20u?= =?UTF-8?q?nsigned=20counted=20loops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reuse LLVM's state and induction-value remapping for terminator-only loops within the existing clone budget. Zero-extend unsigned bounds before checking whether full unrolling is safe. Add frontend state-permutation and direct induction-value regressions, and cover unsigned bounds above the signed range of their type. Assisted-by: GPT-6 via Codex --- .../payload-control-flow-legalization.md | 8 +- .../Transforms/LegalizePayloadControlFlow.cpp | 23 +++++- .../Compiler/test_compiler_pipeline.cpp | 81 ++++++++++++++++++- 3 files changed, 106 insertions(+), 6 deletions(-) diff --git a/.agent/plans/payload-control-flow-legalization.md b/.agent/plans/payload-control-flow-legalization.md index 9e64841acb..eda02f053d 100644 --- a/.agent/plans/payload-control-flow-legalization.md +++ b/.agent/plans/payload-control-flow-legalization.md @@ -20,7 +20,9 @@ are in `docs/mlir/target_compilation.md` and the QCO `Passes.td`. unrolling exposes constant bounds and branches. - Reuse MLIR trip counts, zero/one-trip promotion, and full unrolling. Require literal bounds, signed-arithmetic safety, and a scaled step that fits the IV - type. Limit the pass to 65,536 cloned body operations. + type. Interpret unsigned bounds with zero extension. Limit the pass to 65,536 + cloned body operations. A temporary constant lets LLVM unroll terminator-only + state updates under the same budget; cleanup removes it. - Build switch fallbacks iteratively. Preflight the payload's branch-depth limit and a compiler limit of 256 total control-flow levels, including moved case bodies. Retained native multiway switches do not use this expansion limit. @@ -38,3 +40,7 @@ optional `QueryJobId` skip. MLIR documentation generation, repository lint, and whole changed-file C++ lint passed. Focused regressions cover IV values, cumulative cloning, switch depth, case/default selection, quantum-state forwarding, and invalid captures. + +The loop-boundary fixes pass all 203 compiler tests, including frontend state +permutations, terminator-only induction values, and unsigned bounds above the +signed range of their type. diff --git a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp index 13e55b16a5..052248bcb5 100644 --- a/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -322,9 +323,15 @@ static LogicalResult foldStaticBranches(ModuleOp moduleOp) { const uint64_t iterations) { const auto constant = [&](Value value) -> std::optional { const auto result = getConstantAPIntValue(getAsOpFoldResult(value)); - if (!result || (loop.getUnsignedCmp() && result->first.isNegative())) { + if (!result) { return std::nullopt; } + if (loop.getUnsignedCmp()) { + if (result->first.getActiveBits() > 63) { + return std::nullopt; + } + return static_cast(result->first.getZExtValue()); + } return result->first.trySExtValue(); }; @@ -546,9 +553,6 @@ struct UnrollLoopsForPayload final rewriter.replaceOp(loop, yielded); continue; } - loop.emitError("cannot fully unroll a terminator-only loop"); - signalPassFailure(); - return; } uint64_t bodyOperations = 0U; @@ -556,6 +560,10 @@ struct UnrollLoopsForPayload final loop.getRegion().walk([&](Operation* operation) { bodyOperations += operation != terminator; }); + /// LLVM skips terminator-only bodies. Budget a temporary constant so + /// its unroller can remap iteration arguments and induction values. + const bool emptyBody = bodyOperations == 0U; + bodyOperations = std::max(bodyOperations, uint64_t{1}); const uint64_t remaining = MAX_UNROLLED_OPERATIONS - clonedOperations; const uint64_t maximumTripCount = (remaining / bodyOperations) + 1U; if (!tripCount.ule(maximumTripCount)) { @@ -573,6 +581,13 @@ struct UnrollLoopsForPayload final signalPassFailure(); return; } + if (emptyBody) { + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPointToStart(loop.getBody()); + /// Remove this workaround when LLVM unrolls empty bodies. The + /// static-branch cleanup below removes the unused constants. + arith::ConstantIndexOp::create(rewriter, loop.getLoc(), 0); + } if (failed(loopUnrollFull(loop))) { loop.emitError("failed to fully unroll a static counted loop"); signalPassFailure(); diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index cefd8a87a1..83d50e6655 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -75,6 +75,7 @@ #include #include +#include #include #include #include @@ -2186,6 +2187,74 @@ for int outer in [0:2] { EXPECT_FALSE(StringRef(program->str()).contains("qco.if")); } +TEST_F(CompilerPipelineTest, + PayloadControlFromOpenQASMUnrollsStatePermutation) { + constexpr llvm::StringLiteral source = R"qasm( +OPENQASM 3.1; +include "stdgates.inc"; +qubit q; +int a = 0; +int b = 1; +for int i in [0:2] { + int tmp = a; + a = b; + b = tmp; +} +rx(a) q; +bit c; +c = measure q; +)qasm"; + auto qc = QCProgram::fromQASMString(source); + ASSERT_TRUE(qc); + auto program = std::move(*qc).intoQCO(); + ASSERT_TRUE(program); + ASSERT_TRUE(program->compileForTarget(TargetEnvironment( + makeUnrestrictedTarget(), makeControlPayloadSpecification({})))); + EXPECT_TRUE(succeeded(verify(program->module()))); + EXPECT_TRUE(succeeded(qco::verifyLinearity(program->module()))); + EXPECT_FALSE(StringRef(program->str()).contains("scf.for")); + auto entry = program->module().lookupSymbol("main"); + auto result = cast(entry.getBody().front().getTerminator()); + for (auto [operand, expected] : + llvm::zip_equal(result.getOperands().take_front(2), std::array{1, 0})) { + IntegerAttr value; + ASSERT_TRUE(matchPattern(operand, m_Constant(&value))); + EXPECT_EQ(value.getInt(), expected); + } +} + +TEST_F(CompilerPipelineTest, + PayloadControlUnrollsTerminatorOnlyInductionValue) { + constexpr llvm::StringLiteral source = R"mlir( + module { + func.func @main() -> index attributes {mqt.entry_point} { + %lb = arith.constant 2 : index + %ub = arith.constant 10 : index + %step = arith.constant 3 : index + %out = scf.for %i = %lb to %ub step %step + iter_args(%value = %lb) -> (index) { + scf.yield %i : index + } + return %out : index + } + } + )mlir"; + auto program = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(program); + attachTargetEnvironment( + program->module(), + TargetEnvironment(makeUnrestrictedTarget(), + makeControlPayloadSpecification({}))); + ASSERT_TRUE(program->runPassPipeline("unroll-loops-for-payload")); + EXPECT_TRUE(succeeded(verify(program->module()))); + EXPECT_FALSE(StringRef(program->str()).contains("scf.for")); + auto entry = program->module().lookupSymbol("main"); + auto result = cast(entry.getBody().front().getTerminator()); + IntegerAttr value; + ASSERT_TRUE(matchPattern(result.getOperand(0), m_Constant(&value))); + EXPECT_EQ(value.getInt(), 8); +} + TEST_F(CompilerPipelineTest, PayloadControlBoundsFullUnrolling) { constexpr llvm::StringLiteral source = R"mlir( module { @@ -2306,6 +2375,13 @@ TEST_F(CompilerPipelineTest, PayloadControlChecksUnrolledStepWidth) { .unsignedComparison = false, .safe = true, }, + LoopCase{ + .lower = 128, + .upper = 160, + .step = 10, + .unsignedComparison = true, + .safe = true, + }, LoopCase{ .lower = 0, .upper = 110, @@ -2358,7 +2434,10 @@ TEST_F(CompilerPipelineTest, PayloadControlChecksUnrolledStepWidth) { program->module().walk([&](func::CallOp call) { IntegerAttr value; ASSERT_TRUE(matchPattern(call.getOperand(0), m_Constant(&value))); - observed.push_back(value.getInt()); + observed.push_back( + test.unsignedComparison + ? static_cast(value.getValue().getZExtValue()) + : value.getInt()); }); std::vector expected; for (auto value = test.lower; value < test.upper; value += test.step) {