diff --git a/.agent/plans/payload-control-flow-legalization.md b/.agent/plans/payload-control-flow-legalization.md new file mode 100644 index 0000000000..9e64841acb --- /dev/null +++ b/.agent/plans/payload-control-flow-legalization.md @@ -0,0 +1,40 @@ +# Compiler-only control-flow legalization + +Status: implemented. Latest local validation: 2026-09-09. + +## Scope + +Legalize structured QCO/SCF control flow for the selected payload. Producers +normalize CFG branches before target compilation. Scalar operations, measurement +provenance, allocation, functions, and final payload-profile verification remain +separate checks. + +The implementation is in +`mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp`; compiler tests +are in `mlir/unittests/Compiler/test_compiler_pipeline.cpp`. Public contracts +are in `docs/mlir/target_compilation.md` and the QCO `Passes.td`. + +## Decisions + +- Keep SCCP and QCO cleanup between unrolling and residual legality checks: + unrolling exposes constant bounds and branches. +- Reuse MLIR trip counts, zero/one-trip promotion, and full unrolling. Require + literal bounds, signed-arithmetic safety, and a scaled step that fits the IV + type. 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 + +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/CHANGELOG.md b/CHANGELOG.md index 785e7105db..21adabf5db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -992,6 +993,7 @@ for previous changelogs._ [#2175]: https://github.com/munich-quantum-toolkit/core/pull/2175 [#2169]: https://github.com/munich-quantum-toolkit/core/pull/2169 [#2168]: https://github.com/munich-quantum-toolkit/core/pull/2168 +[#2162]: https://github.com/munich-quantum-toolkit/core/pull/2162 [#2158]: https://github.com/munich-quantum-toolkit/core/pull/2158 [#2157]: https://github.com/munich-quantum-toolkit/core/pull/2157 [#2156]: https://github.com/munich-quantum-toolkit/core/pull/2156 diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 8ce5c7703a..6133f7d02c 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -76,6 +76,58 @@ Payload versions accept one to three numeric components. A suffixes, and version ranges are rejected. The same rules apply when reading the typed `#mqt.payload_spec` attribute. +### Payload control flow + +Target compilation requires structured QCO/SCF input. Producers of raw CFG +branches must normalize them before target compilation; runtime assertions are +allowed. The pipeline removes unused symbols, propagates constants, unrolls +unsupported static loops, and then runs the standard QCO cleanup pipeline. It +uses `unroll-loops-for-payload` before cleanup and `legalize-control-flow` after +cleanup, so unrolling can expose constant branches before legality checks. The +latter pass applies these structural capabilities to the remaining control flow: + +| Capability | Residual operations | +| -------------------- | --------------------------------------------------- | +| `forward-branching` | `qco.if` and classical `scf.if` | +| `counted-iteration` | `scf.for` | +| `conditional-loop` | `scf.while` | +| `multiway-branching` | `qco.index_switch` and classical `scf.index_switch` | + +A finite `scf.for` that exceeds the selected counted-iteration contract is fully +unrolled when this clones at most 65,536 body operations. Cleanup runs again +because unrolling can make nested bounds and conditions constant. An unsupported +index switch is lowered to a linear chain of nested forward branches when that +form fits the selected contract. Before expansion, the compiler checks the +selected forward-branching nesting limit and a compiler safety limit of 256 +total control-flow levels, including enclosing control flow. This compiler limit +is not a QDMI requirement and does not apply to switches retained under multiway +branching. + +Generic SCF branches cannot capture or return QCO qubits or quantum tensors; use +the corresponding QCO branch operation for linear quantum state. SCF loops must +carry linear quantum state through their iteration arguments instead of +capturing it. Both control-flow passes validate this loop input restriction +before transforming loops or lowering switches. It is separate from QCO's +exactly-one-SSA-use check. + +The supported constraints are `max-control-flow-nesting-depth` on all four +capabilities, `max-iteration-count` on both iteration capabilities, and +`max-case-count` on multiway branching, counting explicit cases without the +default region. One explicit case plus a default is a supported index switch and +does not require forward branching. Limits are inclusive. The compiler must +prove a constrained loop's trip count. It currently proves constant `scf.for` +bounds and rejects a constrained `scf.while` because no general termination +bound is available. The proof requires literal loop bounds and a literal step; +it does not infer a trip count from symbolic bounds. MLIR computes static trip +counts; full unrolling additionally requires bounds and scaled steps that fit +its signed arithmetic. The scaled step must also fit the loop induction-variable +type. A zero, unknown, or misapplied constraint makes that capability group +unusable. Missing or incomplete optional metadata never implies support. + +This stage checks structural control flow only. Later lowering stages remain +responsible for scalar types and operations, measurement provenance, function +features, allocation, and final payload-profile conformance. + The target can also be constructed directly. Connectivity and native-operation support are required: 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/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/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 10e9f66175..6b77b38d46 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -110,6 +110,44 @@ def QuantumLoopUnroll "set it to -1, and it will fully unroll.">]; } +//===----------------------------------------------------------------------===// +// Payload legalization passes +//===----------------------------------------------------------------------===// + +def UnrollLoopsForPayload : Pass<"unroll-loops-for-payload", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::arith::ArithDialect", + "mlir::scf::SCFDialect"]; + let summary = "Unroll static loops unsupported by the selected payload"; + let description = [{ + Reads the selected payload from `mqt.target_env` and fully unrolls static + `scf.for` operations that its counted-iteration capability does not cover. + The pass limits the total number of cloned body operations to 65,536. + Run constant propagation and QCO cleanup after this pass, then + `legalize-control-flow` to check the remaining branches and loops. + After folding static branches, the pass checks that all SCF loops carry + linear QCO state through iteration arguments instead of capturing it. + }]; +} + +def LegalizeControlFlow : Pass<"legalize-control-flow", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect", "mlir::scf::SCFDialect", + "mlir::arith::ArithDialect"]; + let summary = "Legalize control flow for the selected payload"; + let description = [{ + Reads the selected payload from `mqt.target_env`. The pass retains covered + QCO and SCF branches and loops, lowers unsupported index switches to nested + forward branches when possible, and rejects residual structural control + flow that the payload does not support. + Switch expansion checks the selected forward-branching depth limit before + rewriting and limits the resulting total control-flow nesting depth to 256. + This compiler safety limit does not restrict retained native switches. + Run this pass after `unroll-loops-for-payload`, constant propagation, and + QCO cleanup so newly constant control flow can fold before legality checks. + Before conversion, the pass checks that all SCF loops carry linear QCO + state through iteration arguments instead of capturing it. + }]; +} + //===----------------------------------------------------------------------===// // Transpilation Passes //===----------------------------------------------------------------------===// diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index 590cce8990..d4f2d2aa7e 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -16,8 +16,10 @@ #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Support/Passes.h" +#include #include #include +#include #include #include @@ -26,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(); } @@ -49,10 +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(createSCCPPass()); + pm.addPass(qco::createUnrollLoopsForPayload()); + pm.addPass(createSCCPPass()); populateQCOCleanupPipeline(pm); + pm.addPass(qco::createLegalizeControlFlow()); pm.addPass(qco::createDecomposeMultiControlled(target)); populateDefaultQCOOptimizationPipeline(pm); /// ponytail: CX/CZ-cost fusion can increase square-root iSWAP counts; 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..13e55b16a5 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/LegalizePayloadControlFlow.cpp @@ -0,0 +1,637 @@ +/* + * 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_LEGALIZECONTROLFLOW +#define GEN_PASS_DEF_UNROLLLOOPSFORPAYLOAD +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" + +namespace { + +constexpr uint64_t MAX_UNROLLED_OPERATIONS = 65536U; +// Bound generated nesting to keep recursive IR processing manageable. +constexpr uint64_t MAX_SWITCH_EXPANSION_DEPTH = 256U; + +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(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; + } + + 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; + } + + /// 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 (isStructuredControl(parent)) { + ++depth; + } + } + return depth; + } + +private: + [[nodiscard]] static bool isStructuredControl(Operation* operation) { + return isa(operation); + } + + [[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 == ProgramConstraint::MAX_NESTING_DEPTH) { + group.maxNestingDepth = value; + return; + } + if (id == ProgramConstraint::MAX_ITERATION_COUNT && + (feature == ControlFeature::CountedIteration || + feature == ControlFeature::ConditionalLoop)) { + group.maxIterationCount = value; + return; + } + if (id == ProgramConstraint::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); +} + +// 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) { + if (!getConstantAPIntValue(getAsOpFoldResult(loop.getLowerBound())) || + !getConstantAPIntValue(getAsOpFoldResult(loop.getUpperBound())) || + !getConstantAPIntValue(getAsOpFoldResult(loop.getStep()))) { + return std::nullopt; + } + return loop.getStaticTripCount(); +} + +static LogicalResult foldStaticBranches(ModuleOp moduleOp) { + /// 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()); + 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 support.coversIteration(ControlFeature::CountedIteration, operation, + getExactConstantTripCount(operation)); +} + +[[nodiscard]] static bool isLegal(scf::WhileOp operation, + const PayloadControlSupport& support) { + return support.coversIteration(ControlFeature::ConditionalLoop, operation, + operation.getStaticTripCount()); +} + +[[nodiscard]] static bool isLegal(IndexSwitchOp operation, + const PayloadControlSupport& support) { + const uint64_t cases = operation.getNumCases(); + 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 > 0U && + 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; + 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()); + return !type || (loop.getUnsignedCmp() + ? llvm::isUIntN(type.getWidth(), + static_cast(scaledStep)) + : llvm::isIntN(type.getWidth(), scaledStep)); +} + +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->canLowerSwitch(operation, cases.size())) { + return rewriter.notifyMatchFailure( + operation, + "switch requires unsupported or excessive branching depth"); + } + + 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( + 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); + 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()); + break; + } + + Block& elseBlock = ifOp.getElseRegion().emplaceBlock(); + elseBlock.addArguments(targets.getTypes(), + SmallVector(targets.size(), operation.getLoc())); + rewriter.setInsertionPointToEnd(&elseBlock); + targets = elseBlock.getArguments(); + } + + 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->canLowerSwitch(operation, cases.size())) { + return rewriter.notifyMatchFailure( + operation, + "switch requires unsupported or excessive branching depth"); + } + + 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( + rewriter, operation.getLoc(), arith::CmpIPredicate::eq, + adaptor.getArg(), constant.getResult()); + 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(), + ifOp.getThenRegion().end()); + if (index + 1U == cases.size()) { + rewriter.inlineRegionBefore(*defaultRegion, ifOp.getElseRegion(), + ifOp.getElseRegion().end()); + break; + } + + Block& elseBlock = ifOp.getElseRegion().emplaceBlock(); + rewriter.setInsertionPointToEnd(&elseBlock); + } + + rewriter.replaceOp(operation, replacement.getResults()); + return success(); + } + +private: + const PayloadControlSupport* support; +}; + +struct UnrollLoopsForPayload final + : impl::UnrollLoopsForPayloadBase { + using UnrollLoopsForPayloadBase::UnrollLoopsForPayloadBase; + +protected: + void runOnOperation() override { + const auto support = PayloadControlSupport::read( + getOperation(), getAnalysis()); + if (!support) { + signalPassFailure(); + return; + } + + if (failed(foldStaticBranches(getOperation())) || + failed(verifyLoopCarriedState(getOperation()))) { + signalPassFailure(); + return; + } + + uint64_t clonedOperations = 0U; + IRRewriter rewriter(&getContext()); + while (true) { + SmallVector> loops; + 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; + } + + for (auto& [loop, tripCount] : loops) { + if (tripCount.ule(1)) { + if (failed(loop.promoteIfSingleIteration(rewriter))) { + loop.emitError( + "failed to simplify a zero- or 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 LegalizeControlFlow final + : impl::LegalizeControlFlowBase { + using LegalizeControlFlowBase::LegalizeControlFlowBase; + +protected: + void runOnOperation() override { + const auto support = PayloadControlSupport::read( + getOperation(), getAnalysis()); + if (!support || failed(verifyLoopCarriedState(getOperation()))) { + 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..f191f40cf8 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::registerLegalizeControlFlow(); qco::registerMeasurementLifting(); qco::registerMergeSingleQubitRotationGates(); qco::registerPauliTwirl2QGates(); @@ -63,6 +64,7 @@ void registerMQTCompilerPasses() { qco::registerReplaceClassicalControls(); qco::registerReuseQubits(); qco::registerTargetNativeSynthesis(); + 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 147699cf1b..cefd8a87a1 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" @@ -262,7 +263,7 @@ makeCZTarget(std::initializer_list singleQubitGates) { }, { { - .id = "forward-branching", + .id = ProgramCapability::FORWARD_BRANCHING.str(), .constraints = { { @@ -275,6 +276,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 +2036,1018 @@ 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( + 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, + 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({ + { + .id = "counted-iteration", + .constraints = {{.id = "max-iteration-count", .value = maximum}}, + }, + }); + }; + + auto atBoundary = input->copy(); + ASSERT_TRUE(atBoundary.compileForTarget( + TargetEnvironment(makeUnrestrictedTarget(), payload(3)))); + EXPECT_TRUE(StringRef(atBoundary.str()).contains("scf.for")); + + auto aboveBoundary = input->copy(); + ASSERT_TRUE(aboveBoundary.compileForTarget( + TargetEnvironment(makeUnrestrictedTarget(), payload(2)))); + EXPECT_FALSE(StringRef(aboveBoundary.str()).contains("scf.for")); +} + +TEST_F(CompilerPipelineTest, + 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 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")); + 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, PayloadControlChecksUnrolledStepWidth) { + struct LoopCase { + int64_t lower; + int64_t upper; + int64_t step; + bool unsignedComparison; + bool safe; + }; + for (const auto& test : { + 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); + 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, 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, + 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{ + .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 = input->copy(); + ASSERT_TRUE(preserved.compileForTarget(TargetEnvironment( + target, makeControlPayloadSpecification({multiway(2)})))); + EXPECT_TRUE(StringRef(preserved.str()).contains("qco.index_switch")); + + 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); + + auto tooDeep = input->copy(); + 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 + } + 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-control-flow")); + EXPECT_FALSE(StringRef(program->str()).contains("scf.index_switch")); + 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( + module { + func.func @main(%condition: i1, %left: !qco.qubit, %right: !qco.qubit) + attributes {mqt.entry_point} { + %result = scf.if %condition -> !qco.qubit { + %x = qco.x %left : !qco.qubit -> !qco.qubit + scf.yield %x : !qco.qubit + } else { + %h = qco.h %right : !qco.qubit -> !qco.qubit + scf.yield %h : !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} { + %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"; + constexpr llvm::StringLiteral nestedForCapture = 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 %outer = %c0 to %upper step %c1 { + scf.for %inner = %c0 to %c1 step %c1 { + %next = qco.x %q : !qco.qubit -> !qco.qubit + qco.sink %next : !qco.qubit + } + } + return + } + } + )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"}, + {.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; + } + + for (StringRef source : { + forCapture, + whileCapture, + nestedForCapture, + nestedWhileCapture, + }) { + SCOPED_TRACE(source.str()); + for (const auto* pass : + {"unroll-loops-for-payload", "legalize-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, + PayloadControlDiagnosesIllegalControlNestedInLegalBranch) { + constexpr llvm::StringLiteral source = R"mlir( + module { + func.func private @effect() + func.func @main(%condition: i1) attributes {mqt.entry_point} { + 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, PayloadControlRejectsUnstructuredCFG) { + 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): + return %value : i64 + } + } + )mlir"; + const auto payload = makeControlPayloadSpecification({ + { + .id = "forward-branching", + .constraints = + { + {.id = "max-control-flow-nesting-depth", .value = 1}, + }, + }, + }); + std::string diagnostics; + auto program = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(program); + 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, + 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; 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)