From 55684212384d732185a644e54a918923fd7387df Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 16:20:20 +0200 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=90=9B=20Diagnose=20conditional=20Ada?= =?UTF-8?q?ptive=20QIR=20allocation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Require dynamic qubit allocations in the entry function to remain in its original entry block. Reject unsupported conditional allocations before QIR lowering can move their releases outside the allocation scope. Assisted-by: GPT-6 via Codex --- .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.td | 3 + .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp | 20 +++++ .../test_qc_to_qir_adaptive.cpp | 89 +++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td index d75ff31440..6e983b703a 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td @@ -21,6 +21,9 @@ def QCToQIRAdaptive : Pass<"qc-to-qir-adaptive", "mlir::ModuleOp"> { - Input is a valid module in the QC dialect. - The entry function must be marked with `mqt.entry_point`. + - Dynamic qubit allocations in the entry function, including qubit-register + `memref.alloc`, must appear directly in its entry block. Allocations in + SCF regions or other blocks are diagnosed before lowering. Behavior: diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index a0c6691dca..f707e852a2 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -764,6 +765,25 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { signalPassFailure(); return; } + const auto allocations = entryPoint.walk([&](Operation* operation) { + bool isQubitAllocation = isa(operation); + if (auto allocation = dyn_cast(operation)) { + isQubitAllocation = + isa(allocation.getType().getElementType()); + } + if (isQubitAllocation && + operation->getBlock() != &entryPoint.getBody().front()) { + operation->emitOpError( + "adaptive QIR conversion requires dynamic qubit allocations in " + "the entry block of the entry function"); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (allocations.wasInterrupted()) { + signalPassFailure(); + return; + } auto entryPointName = entryPoint.getSymNameAttr(); if (failed(mqt::normalizeGlobalPhases(moduleOp))) { signalPassFailure(); diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp index fed2299b58..1cc7fb73ad 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp @@ -100,6 +100,95 @@ static LogicalResult runQCToQIRAdaptiveConversionSimple(ModuleOp moduleOp) { return pm.run(moduleOp); } +TEST(QCToQIRAdaptiveNativeTest, RejectsConditionalQubitAllocations) { + const auto sources = { + R"mlir(module { + func.func private @condition() -> i1 + func.func @main() attributes {mqt.entry_point} { + %c = func.call @condition() : () -> i1 + scf.if %c { + %q = qc.alloc : !qc.qubit + qc.x %q : !qc.qubit + qc.dealloc %q : !qc.qubit + } + return + } + })mlir", + R"mlir(module { + func.func private @condition() -> i1 + func.func @main() attributes {mqt.entry_point} { + %c = func.call @condition() : () -> i1 + scf.if %c { + %reg = memref.alloc() : memref<1x!qc.qubit> + memref.dealloc %reg : memref<1x!qc.qubit> + } + return + } + })mlir", + R"mlir(module { + func.func private @condition() -> i1 + func.func @main() attributes {mqt.entry_point} { + %c = func.call @condition() : () -> i1 + cf.cond_br %c, ^then, ^end + ^then: + %q = qc.alloc : !qc.qubit + qc.x %q : !qc.qubit + qc.dealloc %q : !qc.qubit + cf.br ^end + ^end: + return + } + })mlir", + }; + for (const auto* source : sources) { + SCOPED_TRACE(source); + MLIRContext context; + context + .loadDialect(); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()) + .contains("adaptive QIR conversion requires dynamic qubit " + "allocations in the entry block"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(*moduleOp))); + EXPECT_TRUE(sawExpectedDiagnostic); + } +} + +TEST(QCToQIRAdaptiveNativeTest, PreservesEntryBlockQubitAllocations) { + MLIRContext context; + context.loadDialect(); + auto moduleOp = parseSourceString(R"mlir(module { + func.func private @condition() -> i1 + func.func @main() attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %c = func.call @condition() : () -> i1 + scf.if %c { + qc.x %q : !qc.qubit + } + %reg = memref.alloc() : memref<1x!qc.qubit> + qc.dealloc %q : !qc.qubit + memref.dealloc %reg : memref<1x!qc.qubit> + return + } + })mlir", + &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(*moduleOp))); + EXPECT_TRUE(succeeded(verify(*moduleOp))); +} + TEST(QCToQIRAdaptiveNativeTest, NormalizesFactorableControlledGlobalPhaseBeforeLowering) { MLIRContext context; From 004e0221bba2b9f416d53b3aaf2b694dbcb3820a Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 16:39:31 +0200 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=94=87=20Preserve=20the=20GoogleTest?= =?UTF-8?q?=20setup=20override=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-6 via Codex --- .../QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp index 1cc7fb73ad..cb290eff76 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp @@ -74,6 +74,8 @@ class QCToQIRAdaptiveTest protected: std::unique_ptr context; + // GoogleTest requires this override name. + // NOLINTNEXTLINE(readability-identifier-naming) void SetUp() override { DialectRegistry registry; registry.insert Date: Mon, 7 Sep 2026 18:32:11 +0200 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=94=87=20Remove=20redundant=20SetUp?= =?UTF-8?q?=20naming=20suppression?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-6 via Codex --- .../QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp index cb290eff76..1cc7fb73ad 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp @@ -74,8 +74,6 @@ class QCToQIRAdaptiveTest protected: std::unique_ptr context; - // GoogleTest requires this override name. - // NOLINTNEXTLINE(readability-identifier-naming) void SetUp() override { DialectRegistry registry; registry.insert Date: Tue, 8 Sep 2026 07:39:48 +0200 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=90=9B=20Verify=20quantum=20allocatio?= =?UTF-8?q?n=20scope=20at=20program=20entry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enforce the shared QC/QCO program contract during verification and construction. Keep helpers resource-parameterized and remove redundant pass-local placement checks. Assisted-by: GPT-6 via Codex --- .agent/plans/quantum-allocation-scope.md | 44 +++++++ mlir/include/mlir/Compiler/Programs.h | 6 +- .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.td | 3 - mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h | 6 + .../include/mlir/Dialect/MQT/IR/MQTDialect.td | 6 +- .../Dialect/QC/Builder/QCProgramBuilder.h | 7 ++ mlir/include/mlir/Dialect/QC/IR/QCDialect.td | 5 + .../Dialect/QCO/Builder/QCOProgramBuilder.h | 7 ++ .../include/mlir/Dialect/QCO/IR/QCODialect.td | 5 + mlir/lib/Compiler/Programs.cpp | 10 +- .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp | 20 --- mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 32 ++++- .../Dialect/QC/Builder/QCProgramBuilder.cpp | 9 ++ .../Dialect/QCO/Builder/QCOProgramBuilder.cpp | 9 ++ .../QCO/Transforms/Mapping/Mapping.cpp | 33 ++--- .../Compiler/test_compiler_pipeline.cpp | 105 ++++++++++++++-- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 33 +---- .../test_qc_to_qir_adaptive.cpp | 63 ---------- mlir/unittests/Dialect/MQT/IR/CMakeLists.txt | 4 +- mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 118 +++++++++++++++++- mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp | 38 +++++- mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp | 59 ++++++++- .../QCO/Transforms/Mapping/test_mapping.cpp | 75 ----------- .../Target/OpenQASM/test_openqasm_emitter.cpp | 18 +++ 24 files changed, 472 insertions(+), 243 deletions(-) create mode 100644 .agent/plans/quantum-allocation-scope.md diff --git a/.agent/plans/quantum-allocation-scope.md b/.agent/plans/quantum-allocation-scope.md new file mode 100644 index 0000000000..6997e69f94 --- /dev/null +++ b/.agent/plans/quantum-allocation-scope.md @@ -0,0 +1,44 @@ +# Quantum allocation scope + +Status: complete; validated locally. + +## Goal and scope + +Dynamic quantum allocations in QC/QCO programs belong in the entry block of the +function marked `mqt.entry_point`. Helper functions receive quantum resources as +arguments. This covers `qc.alloc`, `qco.alloc`, qubit `memref.alloc`, and +`qtensor.alloc`. Classical allocations and static qubit references are +unchanged. + +## Decisions + +The MQT entry-point attribute verifier owns the whole-program rule. It already +checks module-level entry-point uniqueness and can inspect all four allocation +forms without extending an upstream operation. QC/QCO program construction loads +the MQT verifier even for caller-supplied contexts and checks modules without an +entry marker. Raw unmarked MLIR fragments can be verified independently; +operation verification alone does not establish this program-wide invariant. + +Builders reject invalid allocation placement before creating an operation. +OpenQASM semantic analysis already rejects non-global qubit declarations, and +loop emission restores the entry-block insertion point for later declarations. +The Adaptive conversion no longer scans allocation placement; Mapping discovers +allocations directly in the entry block. + +Tests cover all four allocation forms, allowed and forbidden placement, missing +entry markers, caller-supplied contexts, builders, and frontend loop emission. +Pass tests use valid quantum-resource arguments or static references where the +behavior under test does not require allocation. + +## Validation + +With LLVM/MLIR 23.1.0, the full lint-preset build and all 2,358 configured MLIR +CTest entries passed, including the verifier, compiler, builder, and frontend +regressions. Commands from the repository root: + +- `uvx nox -s lint` +- `uvx nox -s cpp-lint -- ec799daa09f855bd0edcbc5592a5fedd90836516` +- `ctest --test-dir build/cpp-lint -L mqt-mlir-unittests --output-on-failure -j8` + +Full changed-file C++ lint passed with local clang-tidy 23.0.0git and the macOS +SDK headers configured. Hosted CI was not run for this local revision. diff --git a/mlir/include/mlir/Compiler/Programs.h b/mlir/include/mlir/Compiler/Programs.h index 2ac278ccc6..9420b5d334 100644 --- a/mlir/include/mlir/Compiler/Programs.h +++ b/mlir/include/mlir/Compiler/Programs.h @@ -164,7 +164,9 @@ class QCProgram final : public Program { /// /// The context must own every dialect referenced by the module and must /// remain the module's context. The factory verifies the module and rejects - /// QCO and QTensor operations. QC operations are not required. + /// QCO and QTensor operations. QC operations are not required. Dynamic + /// quantum allocations require an `mqt.entry_point` function and must appear + /// directly in its entry block. [[nodiscard]] static std::optional fromModule(std::shared_ptr context, OwningOpRef moduleOp); @@ -237,6 +239,8 @@ class QCOProgram final : public Program { /// The context must own every dialect referenced by the module and must /// remain the module's context. The factory verifies the module and QCO /// linearity and rejects QC operations. QCO operations are not required. + /// Dynamic quantum allocations require an `mqt.entry_point` function and + /// must appear directly in its entry block. [[nodiscard]] static std::optional fromModule(std::shared_ptr context, OwningOpRef moduleOp); diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td index 6e983b703a..d75ff31440 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td @@ -21,9 +21,6 @@ def QCToQIRAdaptive : Pass<"qc-to-qir-adaptive", "mlir::ModuleOp"> { - Input is a valid module in the QC dialect. - The entry function must be marked with `mqt.entry_point`. - - Dynamic qubit allocations in the entry function, including qubit-register - `memref.alloc`, must appear directly in its entry block. Allocations in - SCF regions or other blocks are diagnosed before lowering. Behavior: diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h index babc0d1da7..b4bec4db0c 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h @@ -15,6 +15,7 @@ #include #include #include +#include //===----------------------------------------------------------------------===// // Dialect @@ -53,4 +54,9 @@ void setUnitaryFunction(Operation* operation); } return nullptr; } + +/// Check that dynamic quantum allocations belong to the program entry block. +/// Modules without an entry point must not contain dynamic quantum allocations. +/// Nested modules have separate program scopes. +[[nodiscard]] LogicalResult verifyQuantumAllocations(ModuleOp moduleOp); } // namespace mlir::mqt diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td index 6607a81fec..a1b745ad7f 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -33,7 +33,11 @@ def MQTDialect : Dialect { register allocation. Input and register names share one function-wide namespace. `mqt.entry_point` marks the single public, defined `func.func` program entry - in a module. + in a module. Dynamic quantum allocations (`qc.alloc`, `qco.alloc`, + qubit `memref.alloc`, and `qtensor.alloc`) must appear directly in its + entry block. Other functions receive quantum resources as arguments; + they cannot allocate them. Classical allocations and static qubit references + are not subject to this restriction. `mqt.unitary` marks a private function that defines a unitary operation. Its body admits unitary operations and memory-effect-free, region-free classical computation, matching the quantum modifier-body contract. Parameter diff --git a/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h b/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h index aeb0f6a63d..834694dac7 100644 --- a/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h +++ b/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h @@ -48,6 +48,8 @@ namespace qc { /// allocation /// (`allocQubit` / `allocQubitRegister`), never both. The builder terminates /// with a usage error if the modes are mixed. +/// Dynamic allocation is only allowed directly in the entry block of the +/// `mqt.entry_point` function. Helpers receive allocated qubits as arguments. /// /// @par Example Usage: /// ```c++ @@ -120,6 +122,7 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { /// Create a complete private function and infer its result types. /// /// Borrowed qubit arguments are updated in place and must not be returned. + /// The body must not dynamically allocate qubits or qubit registers. func::FuncOp createFunction(StringRef name, TypeRange argumentTypes, function_ref(ValueRange)> body); @@ -169,6 +172,7 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { }; /// Allocate a single qubit initialized to |0⟩ + /// Requires an insertion point in the entry block of `mqt.entry_point`. /// @return A qubit reference /// /// @par Example: @@ -196,6 +200,7 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { /// Allocate a qubit register and eagerly load every element. /// /// Every allocated qubit is initialized to |0⟩. + /// Requires an insertion point in the entry block of `mqt.entry_point`. /// /// \param size Number of qubits; must be positive. /// \param name Optional source-level register name. @@ -217,6 +222,7 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { /// Every allocated qubit is initialized to |0⟩. The builder tracks the /// register for automatic deallocation. Use `loadQubit` to obtain references /// at their points of use. + /// Requires an insertion point in the entry block of `mqt.entry_point`. /// /// \param size Number of qubits; must be positive. /// \param name Optional source-level register name. @@ -1375,6 +1381,7 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { AllocationMode allocationMode = AllocationMode::Unset; /// Ensure static and dynamic qubit allocation modes are not mixed. + /// Dynamic allocation also requires the entry-point entry block. void ensureAllocationMode(AllocationMode requestedMode); }; } // namespace qc diff --git a/mlir/include/mlir/Dialect/QC/IR/QCDialect.td b/mlir/include/mlir/Dialect/QC/IR/QCDialect.td index 82c9a85399..38adb056f2 100644 --- a/mlir/include/mlir/Dialect/QC/IR/QCDialect.td +++ b/mlir/include/mlir/Dialect/QC/IR/QCDialect.td @@ -28,6 +28,11 @@ def QCDialect : Dialect { The name "QC" stands for "Quantum Circuit." + In a program, dynamic qubit and qubit-register allocations must be in + the entry block of the `mqt.entry_point` function. Helper functions + receive quantum resources as arguments. The MQT entry-point verifier + checks this rule across the program. + Example: ```mlir qc.h %q // Applies Hadamard to qubit %q in place diff --git a/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h b/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h index 227c8bee99..2f9e06922d 100644 --- a/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h +++ b/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h @@ -54,6 +54,8 @@ namespace qco { /// allocation /// (`allocQubit`, `allocQubitRegister`, or `qtensorAlloc`), never both. The /// builder terminates with a usage error if the modes are mixed. +/// Dynamic allocation is only allowed directly in the entry block of the +/// `mqt.entry_point` function. Helpers receive allocated qubits as arguments. /// /// @par Example Usage: /// ```c++ @@ -106,6 +108,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { /// /// The callback must return one trailing qubit for every qubit argument, in /// qubit-argument order. + /// The body must not dynamically allocate qubits or qubit tensors. func::FuncOp createFunction(StringRef name, TypeRange argumentTypes, function_ref(ValueRange)> body); @@ -260,6 +263,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { }; /// Allocate a single qubit initialized to |0⟩ + /// Requires an insertion point in the entry block of `mqt.entry_point`. /// @return A tracked qubit handle (convertible to `Value`) /// /// @par Example: @@ -285,6 +289,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { Qubit staticQubit(uint64_t index); /// Allocate a qubit tensor and eagerly extract every element + /// Requires an insertion point in the entry block of `mqt.entry_point`. /// @param size Number of qubits (must be positive) /// @param name Optional source-level register name /// @return A `QubitRegister` containing the residual tensor and one @@ -342,6 +347,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { /// `!qco.qubit` values. No elements are extracted. If the size is a constant, /// the tensor has static size; otherwise it has dynamic size. Its qubits are /// initialized in the |0> state, and the tensor is tracked automatically. + /// Requires an insertion point in the entry block of `mqt.entry_point`. /// /// @param size Number of qubits (must be positive) /// @return The allocated tensor @@ -1946,6 +1952,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { AllocationMode allocationMode = AllocationMode::Unset; /// Ensure static and dynamic qubit allocation modes are not mixed. + /// Dynamic allocation also requires the entry-point entry block. void ensureAllocationMode(AllocationMode requestedMode); }; } // namespace qco diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCODialect.td b/mlir/include/mlir/Dialect/QCO/IR/QCODialect.td index 62d1f5bba4..5f977df429 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCODialect.td +++ b/mlir/include/mlir/Dialect/QCO/IR/QCODialect.td @@ -28,6 +28,11 @@ def QCODialect : Dialect { The name "QCO" stands for "Quantum Circuit Optimization." + In a program, dynamic qubit and qubit-tensor allocations must be in + the entry block of the `mqt.entry_point` function. Helper functions + receive quantum resources as arguments. The MQT entry-point verifier + checks this rule across the program. + Example: ```mlir %q_out = qco.h %q_in // Consumes %q_in, produces %q_out diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index 1b36c96553..7f3e60d91e 100644 --- a/mlir/lib/Compiler/Programs.cpp +++ b/mlir/lib/Compiler/Programs.cpp @@ -282,7 +282,10 @@ QCProgram::fromModule(std::shared_ptr context, "cannot construct a QC program with a different MLIR context"); return std::nullopt; } - if (failed(verify(*storage.mod))) { + storage.context->getOrLoadDialect(); + if (failed(verify(*storage.mod)) || + (!mqt::getEntryPoint(*storage.mod) && + failed(mqt::verifyQuantumAllocations(*storage.mod)))) { return std::nullopt; } if (moduleUsesDialect(*storage.mod, "qco") || @@ -377,7 +380,10 @@ QCOProgram::fromModule(std::shared_ptr context, "cannot construct a QCO program with a different MLIR context"); return std::nullopt; } - if (failed(verify(*storage.mod))) { + storage.context->getOrLoadDialect(); + if (failed(verify(*storage.mod)) || + (!mqt::getEntryPoint(*storage.mod) && + failed(mqt::verifyQuantumAllocations(*storage.mod)))) { return std::nullopt; } if (moduleUsesDialect(*storage.mod, "qc")) { diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index f707e852a2..a0c6691dca 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -38,7 +38,6 @@ #include #include #include -#include #include #include @@ -765,25 +764,6 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { signalPassFailure(); return; } - const auto allocations = entryPoint.walk([&](Operation* operation) { - bool isQubitAllocation = isa(operation); - if (auto allocation = dyn_cast(operation)) { - isQubitAllocation = - isa(allocation.getType().getElementType()); - } - if (isQubitAllocation && - operation->getBlock() != &entryPoint.getBody().front()) { - operation->emitOpError( - "adaptive QIR conversion requires dynamic qubit allocations in " - "the entry block of the entry function"); - return WalkResult::interrupt(); - } - return WalkResult::advance(); - }); - if (allocations.wasInterrupted()) { - signalPassFailure(); - return; - } auto entryPointName = entryPoint.getSymNameAttr(); if (failed(mqt::normalizeGlobalPhases(moduleOp))) { signalPassFailure(); diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 804575a7df..af226da22b 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -285,6 +286,35 @@ LogicalResult CompilationTargetAttr::verify( return success(); } +LogicalResult mlir::mqt::verifyQuantumAllocations(ModuleOp moduleOp) { + auto entryPoint = getEntryPoint(moduleOp); + Block* entryBlock = entryPoint && !entryPoint.isExternal() + ? &entryPoint.getBody().front() + : nullptr; + const auto result = + moduleOp.walk([&](Operation* operation) { + if (isa(operation) && operation != moduleOp.getOperation()) { + return WalkResult::skip(); + } + bool allocatesQubits = + isa(operation); + if (isa(operation) && + operation->getNumResults() == 1) { + auto type = dyn_cast(operation->getResult(0).getType()); + allocatesQubits = type && isa(type.getElementType()); + } + if (allocatesQubits && + (!entryBlock || operation->getBlock() != entryBlock)) { + operation->emitOpError( + "dynamic quantum allocations must be in the entry " + "block of the 'mqt.entry_point' function"); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return success(!result.wasInterrupted()); +} + [[nodiscard]] static LogicalResult verifyEntryPoint(Operation* operation, const NamedAttribute attribute) { if (!isa(attribute.getValue())) { @@ -308,7 +338,7 @@ verifyEntryPoint(Operation* operation, const NamedAttribute attribute) { << "module must contain at most one program entry point"; } } - return success(); + return verifyQuantumAllocations(moduleOp); } template diff --git a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp index bb8d10fd28..9952f16709 100644 --- a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp +++ b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp @@ -961,6 +961,15 @@ void QCProgramBuilder::checkFinalized() const { void QCProgramBuilder::ensureAllocationMode( const AllocationMode requestedMode) { + if (requestedMode == AllocationMode::Dynamic) { + auto entryPoint = mqt::getEntryPoint(cast(moduleOp_)); + if (!entryPoint || entryPoint.getBody().empty() || + getInsertionBlock() != &entryPoint.getBody().front()) { + llvm::reportFatalUsageError( + "Dynamic qubit allocation requires the entry block of the " + "mqt.entry_point function"); + } + } if (allocationMode == AllocationMode::Unset) { allocationMode = requestedMode; return; diff --git a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp index d0917dcdce..45595341a1 100644 --- a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp +++ b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp @@ -1586,6 +1586,15 @@ void QCOProgramBuilder::checkFinalized() const { void QCOProgramBuilder::ensureAllocationMode( const AllocationMode requestedMode) { + if (requestedMode == AllocationMode::Dynamic) { + auto entryPoint = mqt::getEntryPoint(cast(moduleOp_)); + if (!entryPoint || entryPoint.getBody().empty() || + getInsertionBlock() != &entryPoint.getBody().front()) { + llvm::reportFatalUsageError( + "Dynamic qubit allocation requires the entry block of the " + "mqt.entry_point function"); + } + } if (allocationMode == AllocationMode::Unset) { allocationMode = requestedMode; return; diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 72ddfb0aa3..810c692a36 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -173,30 +173,15 @@ static LogicalResult validateRoutingOperations(func::FuncOp func) { static FailureOr discoverComputation(func::FuncOp func) { Computation computation; - const auto discovery = func.walk([&](Operation* op) { - if (!isa(op)) { - return WalkResult::advance(); - } - if (op->getParentRegion() == &func.getFunctionBody()) { - TypeSwitch(op) - .Case([&](AllocOp alloc) { - computation.scalarAllocations.emplace_back(alloc); - }) - .Case([&](qtensor::AllocOp alloc) { - computation.tensorAllocations.emplace_back( - TensorAllocation{.allocation = alloc}); - }); - return WalkResult::advance(); - } - - op->emitError() - << "target placement requires dynamic qubit allocations in the entry " - "function body"; - return WalkResult::interrupt(); - }); - - if (discovery.wasInterrupted()) { - return failure(); + for (Operation& op : func.getBody().front()) { + TypeSwitch(&op) + .Case([&](AllocOp alloc) { + computation.scalarAllocations.emplace_back(alloc); + }) + .Case([&](qtensor::AllocOp alloc) { + computation.tensorAllocations.emplace_back( + TensorAllocation{.allocation = alloc}); + }); } for (auto alloc : computation.scalarAllocations) { diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 23be48efec..d41da3b321 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -59,6 +59,7 @@ #include #include #include +#include #include #include #include @@ -1049,8 +1050,11 @@ INSTANTIATE_TEST_SUITE_P(OpenQASMPrograms, OpenQASMJeffBoundaryTest, // Test: typed programs import MLIR and OpenQASM from their public APIs TEST_F(CompilerPipelineTest, TypedProgramImportsAndCopies) { const std::string mlir = R"(module { - %0 = qc.alloc : !qc.qubit - qc.dealloc %0 : !qc.qubit + func.func @main() attributes {mqt.entry_point} { + %0 = qc.alloc : !qc.qubit + qc.dealloc %0 : !qc.qubit + return + } })"; const std::string qasm = R"(OPENQASM 3.0; include "stdgates.inc"; @@ -1138,7 +1142,7 @@ TEST_F(CompilerPipelineTest, EmptyCompiledProgramsRoundTrip) { TEST_F(CompilerPipelineTest, ProgramImportsRejectMixedQuantumDialects) { const std::string source = R"mlir(module { - func.func @main() { + func.func @main() attributes {mqt.entry_point} { %reference = qc.alloc : !qc.qubit qc.dealloc %reference : !qc.qubit %value = qco.alloc : !qco.qubit @@ -1156,7 +1160,7 @@ TEST_F(CompilerPipelineTest, ProgramImportsRejectMixedQuantumDialects) { TEST_F(CompilerPipelineTest, ProgramImportsRecognizeQTensorOnlyModules) { const std::string source = R"mlir(module { - func.func @main() { + func.func @main() attributes {mqt.entry_point} { %c1 = arith.constant 1 : index %register = qtensor.alloc(%c1) : tensor<1x!qco.qubit> qtensor.dealloc %register : tensor<1x!qco.qubit> @@ -1171,10 +1175,88 @@ TEST_F(CompilerPipelineTest, ProgramImportsRecognizeQTensorOnlyModules) { EXPECT_FALSE(QCProgram::fromMLIRString(source)); } +TEST_F(CompilerPipelineTest, QuantumAllocationsRequireProgramEntryPoint) { + for (const auto& [isQC, body] : { + std::pair{true, "%q = qc.alloc : !qc.qubit\n" + "qc.dealloc %q : !qc.qubit"}, + std::pair{true, "%r = memref.alloc() : memref<1x!qc.qubit>\n" + "%i = arith.constant 0 : index\n" + "%q = memref.load %r[%i] : memref<1x!qc.qubit>\n" + "qc.h %q : !qc.qubit\n" + "memref.dealloc %r : memref<1x!qc.qubit>"}, + std::pair{false, "%q = qco.alloc : !qco.qubit\n" + "qco.sink %q : !qco.qubit"}, + std::pair{false, "%i = arith.constant 0 : index\n" + "%n = arith.constant 1 : index\n" + "%r = qtensor.alloc(%n) : tensor<1x!qco.qubit>\n" + "%rest, %q = qtensor.extract %r[%i] " + ": tensor<1x!qco.qubit>\n" + "qco.sink %q : !qco.qubit\n" + "qtensor.dealloc %rest : tensor<1x!qco.qubit>"}, + }) { + SCOPED_TRACE(body); + auto compilerContext = createCompilerContext(); + const auto source = + std::string("module { func.func @main() {\n") + body + "\nreturn\n} }"; + auto moduleOp = parseSourceString(source, compilerContext.get()); + ASSERT_TRUE(moduleOp); + bool diagnosed = false; + ScopedDiagnosticHandler handler( + compilerContext.get(), [&](Diagnostic& diag) { + diagnosed |= diag.str().find("dynamic quantum allocations must be") != + std::string::npos; + return success(); + }); + if (isQC) { + EXPECT_FALSE(QCProgram::fromModule(compilerContext, std::move(moduleOp))); + } else { + EXPECT_FALSE( + QCOProgram::fromModule(compilerContext, std::move(moduleOp))); + } + EXPECT_TRUE(diagnosed); + } +} + +TEST_F(CompilerPipelineTest, ProgramImportsLoadEntryPointVerifier) { + for (const bool isQC : {true, false}) { + SCOPED_TRACE(isQC ? "QC" : "QCO"); + auto compilerContext = std::make_shared(); + compilerContext + ->loadDialect(); + ASSERT_EQ(compilerContext->getLoadedDialect(), + nullptr); + const auto source = + std::string("module { func.func private @helper() {\n") + + (isQC ? "%q = qc.alloc : !qc.qubit\nqc.dealloc %q : !qc.qubit\n" + : "%q = qco.alloc : !qco.qubit\nqco.sink %q : !qco.qubit\n") + + "return } func.func @main() attributes {mqt.entry_point} { return } }"; + auto moduleOp = parseSourceString(source, compilerContext.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_EQ(compilerContext->getLoadedDialect(), + nullptr); + + bool diagnosed = false; + ScopedDiagnosticHandler handler( + compilerContext.get(), [&](Diagnostic& diag) { + diagnosed |= diag.str().find("dynamic quantum allocations must be") != + std::string::npos; + return success(); + }); + if (isQC) { + EXPECT_FALSE(QCProgram::fromModule(compilerContext, std::move(moduleOp))); + } else { + EXPECT_FALSE( + QCOProgram::fromModule(compilerContext, std::move(moduleOp))); + } + EXPECT_TRUE(diagnosed); + } +} + // Test: QCO imports require each linear value to have one use. TEST_F(CompilerPipelineTest, QCOProgramImportsEnforceLinearity) { const std::string valid = R"mlir(module { - func.func @main() { + func.func @main() attributes {mqt.entry_point} { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %reg = qtensor.alloc(%c1) : tensor<1x!qco.qubit> @@ -1186,13 +1268,13 @@ TEST_F(CompilerPipelineTest, QCOProgramImportsEnforceLinearity) { } })mlir"; const std::string unusedResult = R"mlir(module { - func.func @main() { + func.func @main() attributes {mqt.entry_point} { %qubit = qco.alloc : !qco.qubit return } })mlir"; const std::string reusedBlockArgument = R"mlir(module { - func.func @main(%reg: tensor<1x!qco.qubit>) { + func.func @main(%reg: tensor<1x!qco.qubit>) attributes {mqt.entry_point} { qtensor.dealloc %reg : tensor<1x!qco.qubit> qtensor.dealloc %reg : tensor<1x!qco.qubit> %qubit = qco.alloc : !qco.qubit @@ -1201,7 +1283,7 @@ TEST_F(CompilerPipelineTest, QCOProgramImportsEnforceLinearity) { } })mlir"; const std::string unusedVectorArgument = R"mlir(module { - func.func @main(%qubits: vector<2x!qco.qubit>) { + func.func @main(%qubits: vector<2x!qco.qubit>) attributes {mqt.entry_point} { %qubit = qco.alloc : !qco.qubit qco.sink %qubit : !qco.qubit return @@ -1293,7 +1375,7 @@ TEST_F(CompilerPipelineTest, TypedOpenQASMExportDropsUnusedGates) { TEST_F(CompilerPipelineTest, TypedOpenQASMExportReportsUnsupportedQC) { constexpr llvm::StringLiteral source = R"mlir(module { - func.func @main(%value: i64) { + func.func @main(%value: i64) attributes {mqt.entry_point} { %qubit = qc.alloc : !qc.qubit qc.dealloc %qubit : !qc.qubit return @@ -2604,9 +2686,8 @@ barrier q[0], q[1]; TEST_F(CompilerPipelineTest, QCProgramCountGatesWithoutEntryPoint) { constexpr llvm::StringLiteral source = R"mlir(module { - func.func @helper() { - %qubit = qc.alloc : !qc.qubit - qc.dealloc %qubit : !qc.qubit + func.func @helper(%qubit: !qc.qubit) { + qc.h %qubit : !qc.qubit return } })mlir"; diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index fe393dcafb..257c6d89d0 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -1080,7 +1080,7 @@ module { R"mlir( module { func.func private @duplicate() -> (!qc.qubit, !qc.qubit) { - %q = qc.alloc : !qc.qubit + %q = qc.static 0 : !qc.qubit return %q, %q : !qc.qubit, !qc.qubit } func.func @main() attributes {mqt.entry_point} { @@ -1684,37 +1684,6 @@ INSTANTIATE_TEST_SUITE_P( return info.param.name; }); -TEST_F(QCToQCORegressionTest, DoesNotCaptureQubitsAllocatedInsideIf) { - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main(%condition: i1) - attributes {mqt.entry_point} { - scf.if %condition { - %q = qc.alloc : !qc.qubit - qc.h %q : !qc.qubit - qc.dealloc %q : !qc.qubit - } - return - } -} -)mlir"; - - auto moduleOp = parseSourceString(source, &context); - ASSERT_TRUE(moduleOp); - ASSERT_TRUE(succeeded(verify(*moduleOp))); - ASSERT_TRUE(succeeded(runQCToQCOConversion(*moduleOp))); - ASSERT_TRUE(succeeded(verify(*moduleOp))); - - scf::IfOp ifOp; - moduleOp->walk([&](scf::IfOp candidate) { ifOp = candidate; }); - ASSERT_TRUE(ifOp); - EXPECT_EQ(ifOp.getNumResults(), 0); - std::size_t allocations = 0; - ifOp.getThenRegion().walk([&](qco::AllocOp) { ++allocations; }); - EXPECT_EQ(allocations, 1); - expectNoQCOperations(*moduleOp); -} - TEST_F(QCToQCORegressionTest, RejectsSameDynamicRegisterIndexWithinOneOperation) { constexpr llvm::StringLiteral source = R"mlir( diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp index 1cc7fb73ad..453942b6d8 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp @@ -100,69 +100,6 @@ static LogicalResult runQCToQIRAdaptiveConversionSimple(ModuleOp moduleOp) { return pm.run(moduleOp); } -TEST(QCToQIRAdaptiveNativeTest, RejectsConditionalQubitAllocations) { - const auto sources = { - R"mlir(module { - func.func private @condition() -> i1 - func.func @main() attributes {mqt.entry_point} { - %c = func.call @condition() : () -> i1 - scf.if %c { - %q = qc.alloc : !qc.qubit - qc.x %q : !qc.qubit - qc.dealloc %q : !qc.qubit - } - return - } - })mlir", - R"mlir(module { - func.func private @condition() -> i1 - func.func @main() attributes {mqt.entry_point} { - %c = func.call @condition() : () -> i1 - scf.if %c { - %reg = memref.alloc() : memref<1x!qc.qubit> - memref.dealloc %reg : memref<1x!qc.qubit> - } - return - } - })mlir", - R"mlir(module { - func.func private @condition() -> i1 - func.func @main() attributes {mqt.entry_point} { - %c = func.call @condition() : () -> i1 - cf.cond_br %c, ^then, ^end - ^then: - %q = qc.alloc : !qc.qubit - qc.x %q : !qc.qubit - qc.dealloc %q : !qc.qubit - cf.br ^end - ^end: - return - } - })mlir", - }; - for (const auto* source : sources) { - SCOPED_TRACE(source); - MLIRContext context; - context - .loadDialect(); - auto moduleOp = parseSourceString(source, &context); - ASSERT_TRUE(moduleOp); - ASSERT_TRUE(succeeded(verify(*moduleOp))); - - bool sawExpectedDiagnostic = false; - ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { - sawExpectedDiagnostic |= - StringRef(diagnostic.str()) - .contains("adaptive QIR conversion requires dynamic qubit " - "allocations in the entry block"); - return success(); - }); - EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(*moduleOp))); - EXPECT_TRUE(sawExpectedDiagnostic); - } -} - TEST(QCToQIRAdaptiveNativeTest, PreservesEntryBlockQubitAllocations) { MLIRContext context; context.loadDialect #include #include +#include #include #include +#include #include #include #include @@ -31,9 +33,11 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -47,9 +51,11 @@ class MQTIRTest : public ::testing::Test { void SetUp() override { DialectRegistry registry; - registry.insert(); + registry + .insert(); context = std::make_unique(registry); context->loadAllAvailableDialects(); } @@ -373,6 +379,112 @@ TEST_F(MQTIRTest, RejectsInvalidEntryPoints) { )mlir")); } +TEST_F(MQTIRTest, ChecksQuantumAllocationPlacement) { + struct Placement { + StringRef prefix; + StringRef suffix; + bool allowed; + }; + const std::array placements{ + { + { + .prefix = "module { func.func @main() {\n", + .suffix = "return } }", + .allowed = true, + }, + { + .prefix = "module { func.func @main(%condition: i1) {\n" + "scf.if %condition {\n", + .suffix = "} return } }", + .allowed = false, + }, + { + .prefix = "module { func.func private @helper() {\n", + .suffix = "return } func.func @main() { return } }", + .allowed = false, + }, + { + .prefix = "module { func.func @main() {\ncf.br ^body\n^body:\n", + .suffix = "return } }", + .allowed = false, + }, + { + .prefix = "module {\n", + .suffix = "func.func @main() { return } }", + .allowed = false, + }, + }, + }; + for (StringRef allocation : { + "%q = qc.alloc : !qc.qubit\nqc.dealloc %q : !qc.qubit\n", + "%q = qco.alloc : !qco.qubit\nqco.sink %q : !qco.qubit\n", + "%q = memref.alloc() : memref<1x!qc.qubit>\n" + "memref.dealloc %q : memref<1x!qc.qubit>\n", + "%size = arith.constant 1 : index\n" + "%q = qtensor.alloc(%size) : tensor<1x!qco.qubit>\n" + "qtensor.dealloc %q : tensor<1x!qco.qubit>\n", + }) { + for (const auto& placement : placements) { + const auto source = + placement.prefix.str() + allocation.str() + placement.suffix.str(); + SCOPED_TRACE(source); + auto moduleOp = parse(source); + ASSERT_TRUE(moduleOp); + auto main = moduleOp->lookupSymbol("main"); + ASSERT_TRUE(main); + mqt::setEntryPoint(main); + + bool sawPlacementError = false; + ScopedDiagnosticHandler handler( + context.get(), [&](Diagnostic& diagnostic) { + sawPlacementError |= + StringRef(diagnostic.str()) + .contains("dynamic quantum allocations must be in the " + "entry block of the " + "'mqt.entry_point' function"); + return success(); + }); + EXPECT_EQ(succeeded(verify(*moduleOp)), placement.allowed); + EXPECT_EQ(sawPlacementError, !placement.allowed); + } + } +} + +TEST_F(MQTIRTest, KeepsNestedProgramAllocationScopesSeparate) { + EXPECT_TRUE(parse(R"mlir( + module { + func.func @main() attributes {mqt.entry_point} { return } + module @nested { + func.func @main() attributes {mqt.entry_point} { + %q = qco.alloc : !qco.qubit + qco.sink %q : !qco.qubit + return + } + } + } + )mlir")); +} + +TEST_F(MQTIRTest, DoesNotRestrictClassicalAllocationsOrStaticReferences) { + EXPECT_TRUE(parse(R"mlir( + module { + func.func @main() attributes {mqt.entry_point} { return } + func.func private @helper(%condition: i1) { + scf.if %condition { + %bits = memref.alloc() : memref<1xi1> + memref.dealloc %bits : memref<1xi1> + %values = memref.alloc() : memref<1xf64> + memref.dealloc %values : memref<1xf64> + %qc = qc.static 0 : !qc.qubit + %qco = qco.static 0 : !qco.qubit + qco.sink %qco : !qco.qubit + } + return + } + } + )mlir")); +} + TEST_F(MQTIRTest, RejectsMutuallyRecursiveUnitaryFunctions) { for (StringRef source : { R"mlir( diff --git a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp index 9ddd30ead1..c98abfb91b 100644 --- a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp +++ b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp @@ -233,7 +233,9 @@ TEST_F(QCTest, BuilderRejectsMixedStaticAndDynamicQubitAllocationModes) { }); }, "Cannot mix dynamic and static qubit allocation modes"); +} +TEST_F(QCTest, BuilderRejectsDynamicAllocationOutsideEntryBlock) { EXPECT_DEATH( { QCProgramBuilder builder(context.get()); @@ -242,9 +244,36 @@ TEST_F(QCTest, BuilderRejectsMixedStaticAndDynamicQubitAllocationModes) { builder.allocQubit(); return SmallVector{}; }); - builder.staticQubit(0); }, - "Cannot mix dynamic and static qubit allocation modes"); + "Dynamic qubit allocation requires the entry block"); + + EXPECT_DEATH( + { + QCProgramBuilder builder(context.get()); + builder.initialize(); + builder.createFunction("dynamic_helper", {}, [&](ValueRange) { + builder.allocQubitRegister(1); + return SmallVector{}; + }); + }, + "Dynamic qubit allocation requires the entry block"); + + EXPECT_DEATH( + { + QCProgramBuilder builder(context.get()); + builder.initialize(); + builder.allocQubit(); + builder.scfIf(true, [&] { builder.allocQubit(); }); + }, + "Dynamic qubit allocation requires the entry block"); + + EXPECT_DEATH( + { + QCProgramBuilder builder(context.get()); + builder.initialize(); + builder.scfIf(true, [&] { builder.allocQubitRegisterStorage(1); }); + }, + "Dynamic qubit allocation requires the entry block"); } TEST_F(QCTest, BuilderRejectsOutOfBoundsClassicalRegisterIndices) { @@ -437,11 +466,14 @@ TEST_F(QCTest, BuilderFinalizesRenamedEntryPoint) { builder.initialize(); auto entry = cast(builder.getInsertionBlock()->getParentOp()); entry.setName("entry"); + builder.allocQubit(); + builder.allocQubitRegister(1); auto moduleOp = builder.finalize(); ASSERT_TRUE(moduleOp); EXPECT_EQ(mlir::mqt::getEntryPoint(*moduleOp).getName(), "entry"); + EXPECT_TRUE(succeeded(verify(*moduleOp))); } TEST_F(QCTest, BuilderCreatesFunctionLocalStaticQubits) { @@ -1103,6 +1135,8 @@ TEST_F(QCTest, ModifiersRecursivelyRejectEveryForbiddenOperation) { auto moduleOp = buildInvalidNestedModifierProgram(context.get(), modifier, forbiddenOperation); ASSERT_TRUE(moduleOp); + // Check the modifier contract independently of program allocation scope. + mlir::mqt::removeEntryPoint(mlir::mqt::getEntryPoint(*moduleOp)); bool sawExpectedDiagnostic = false; ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp index 3a32529557..18b6eceb95 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp @@ -175,6 +175,53 @@ TEST_F(QCOTest, BuilderRejectsMixedStaticAndDynamicQubitAllocationModes) { "Cannot mix dynamic and static qubit allocation modes"); } +TEST_F(QCOTest, BuilderRejectsDynamicAllocationOutsideEntryBlock) { + EXPECT_DEATH( + { + QCOProgramBuilder builder(context.get()); + builder.initialize(); + builder.createFunction("dynamic_helper", {}, [&](ValueRange) { + builder.allocQubit(); + return SmallVector{}; + }); + }, + "Dynamic qubit allocation requires the entry block"); + + EXPECT_DEATH( + { + QCOProgramBuilder builder(context.get()); + builder.initialize(); + builder.createFunction("dynamic_helper", {}, [&](ValueRange) { + builder.allocQubitRegister(1); + return SmallVector{}; + }); + }, + "Dynamic qubit allocation requires the entry block"); + + EXPECT_DEATH( + { + QCOProgramBuilder builder(context.get()); + builder.initialize(); + builder.allocQubit(); + builder.qcoIf(true, ValueRange{}, [&](ValueRange) { + builder.allocQubit(); + return SmallVector{}; + }); + }, + "Dynamic qubit allocation requires the entry block"); + + EXPECT_DEATH( + { + QCOProgramBuilder builder(context.get()); + builder.initialize(); + builder.qcoIf(true, ValueRange{}, [&](ValueRange) { + builder.qtensorAlloc(1); + return SmallVector{}; + }); + }, + "Dynamic qubit allocation requires the entry block"); +} + TEST_F(QCOTest, BuilderReturnsTrackedQubit) { static_assert(std::is_convertible_v); static_assert(std::is_constructible_v); @@ -372,11 +419,14 @@ TEST_F(QCOTest, BuilderFinalizesRenamedEntryPoint) { builder.initialize(); auto entry = cast(builder.getInsertionBlock()->getParentOp()); entry.setName("entry"); + builder.allocQubit(); + builder.allocQubitRegister(1); auto moduleOp = builder.finalize(); ASSERT_TRUE(moduleOp); EXPECT_EQ(mlir::mqt::getEntryPoint(*moduleOp).getName(), "entry"); + EXPECT_TRUE(succeeded(verify(*moduleOp))); } TEST_F(QCOTest, UnitaryVerifierDiagnosesMalformedCalls) { @@ -521,12 +571,15 @@ TEST_F(QCOTest, CleanupPrunesUnitaryFunctionsAndSignatures) { } func.func @main() attributes {mqt.entry_point} { %false = arith.constant false - scf.if %false { - %branchQ = qco.alloc : !qco.qubit + %branchQ = qco.alloc : !qco.qubit + %branchResult = scf.if %false -> !qco.qubit { %branchOut = qco.call @conditional(%branchQ) : (!qco.qubit) -> !qco.qubit - qco.sink %branchOut : !qco.qubit + scf.yield %branchOut : !qco.qubit + } else { + scf.yield %branchQ : !qco.qubit } + qco.sink %branchResult : !qco.qubit %unusedTheta = arith.constant 2.0 : f64 %q = qco.alloc : !qco.qubit %out = qco.call @used(%unusedTheta, %q) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 1a4d1560a3..fcc3252c5b 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -886,81 +886,6 @@ TEST_P(MappingPassTest, MapProgramAfterQubitReuse) { EXPECT_EQ(numResets, 1); } -TEST_P(MappingPassTest, FailNestedScalarAllocation) { - const auto& target = GetParam(); - constexpr StringLiteral source = R"mlir( - module { - func.func @main() attributes {mqt.entry_point} { - %condition = arith.constant true - %q0 = qco.alloc : !qco.qubit - %q1 = qco.if %condition args(%arg0 = %q0) -> (!qco.qubit) { - %nested = qco.alloc : !qco.qubit - qco.sink %nested : !qco.qubit - qco.yield %arg0 : !qco.qubit - } else args(%arg0 = %q0) { - qco.yield %arg0 : !qco.qubit - } - qco.sink %q1 : !qco.qubit - return - } - } - )mlir"; - - auto m = parseSourceString(source, context.get()); - ASSERT_TRUE(m); - ASSERT_TRUE(succeeded(verify(*m))); - - std::string diagnostics; - ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { - diagnostics += diagnostic.str(); - return success(); - }); - EXPECT_TRUE(failed(runPass(m.get(), target, MappingPassOptions{}))); - EXPECT_TRUE(StringRef(diagnostics) - .contains("target placement requires dynamic qubit " - "allocations in the entry " - "function body")) - << diagnostics; -} - -TEST_P(MappingPassTest, FailNestedTensorAllocation) { - const auto& target = GetParam(); - constexpr StringLiteral source = R"mlir( - module { - func.func @main() attributes {mqt.entry_point} { - %condition = arith.constant true - %c1 = arith.constant 1 : index - %q0 = qco.alloc : !qco.qubit - %q1 = qco.if %condition args(%arg0 = %q0) -> (!qco.qubit) { - %nested = qtensor.alloc(%c1) : tensor<1x!qco.qubit> - qtensor.dealloc %nested : tensor<1x!qco.qubit> - qco.yield %arg0 : !qco.qubit - } else args(%arg0 = %q0) { - qco.yield %arg0 : !qco.qubit - } - qco.sink %q1 : !qco.qubit - return - } - } - )mlir"; - - auto m = parseSourceString(source, context.get()); - ASSERT_TRUE(m); - ASSERT_TRUE(succeeded(verify(*m))); - - std::string diagnostics; - ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { - diagnostics += diagnostic.str(); - return success(); - }); - EXPECT_TRUE(failed(runPass(m.get(), target, MappingPassOptions{}))); - EXPECT_TRUE(StringRef(diagnostics) - .contains("target placement requires dynamic qubit " - "allocations in the entry " - "function body")) - << diagnostics; -} - TEST_P(MappingPassTest, FailNestedHigherArityUnitary) { const auto& target = GetParam(); diff --git a/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp b/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp index d47690984c..8aef23bce5 100644 --- a/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp +++ b/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp @@ -2212,6 +2212,24 @@ unsignedValue = unsignedValue ** unsignedOperand; EXPECT_EQ(powerLoops, 2); } +TEST(OpenQASMTargetTest, AllocatesGlobalQubitsAfterLoopWithBreak) { + constexpr llvm::StringLiteral source = R"qasm( +OPENQASM 3.1; +qubit q; +for int i in [0:2] { + x q; + if (i == 1) { break; } +} +qubit later; +qubit[2] reg; +)qasm"; + + MLIRContext context; + auto moduleOp = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(moduleOp); + EXPECT_TRUE(succeeded(verify(*moduleOp))); +} + TEST(OpenQASMTargetTest, UsesConstantBoundsForStaticInclusiveRanges) { constexpr auto sources = std::to_array({ "OPENQASM 3.1; qubit q; for int i in [0:1:2] { x q; }", From 24e4ac80b884950e94dd10166d28a4495c9c38bc Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 8 Sep 2026 12:22:27 +0200 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=90=9B=20Fix=20allocation=20tests=20a?= =?UTF-8?q?nd=20capability=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the whitespace fixture valid and check loop allocation rejection at program construction. Qualify the PennyLane capability reference so strict docs builds resolve it. Assisted-by: GPT-5 via Codex --- .agent/plans/quantum-allocation-scope.md | 13 ++++++++++++- test/python/test_mlir.py | 5 ++++- test/python/test_mlir_loops.py | 16 +++++++--------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.agent/plans/quantum-allocation-scope.md b/.agent/plans/quantum-allocation-scope.md index 6997e69f94..186569ea45 100644 --- a/.agent/plans/quantum-allocation-scope.md +++ b/.agent/plans/quantum-allocation-scope.md @@ -41,4 +41,15 @@ regressions. Commands from the repository root: - `ctest --test-dir build/cpp-lint -L mqt-mlir-unittests --output-on-failure -j8` Full changed-file C++ lint passed with local clang-tidy 23.0.0git and the macOS -SDK headers configured. Hosted CI was not run for this local revision. +SDK headers configured. + +With the built package and test environment active, +`python -m pytest -n4 test/python` passed all 1,131 tests on Python 3.14 with +Qiskit 2.5.2. The revised fixtures preserve whitespace-prefixed input handling +and check that loop-local allocations fail during program construction, before +export. + +`uvx nox --non-interactive -s docs` passed with strict reference checking and +all seven executable notebooks. `uvx nox -s lint` passed after these fixture and +documentation fixes; C++ sources are unchanged. These results are local; hosted +CI has not run for this update. diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index e3e5935d78..d9d4a29fea 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -113,7 +113,10 @@ def test_compile_program_mlir_string() -> None: def test_compile_program_mlir_string_with_leading_whitespace() -> None: """Compile a whitespace-prefixed single-line MLIR string.""" - source = " module { %0 = qc.alloc : !qc.qubit qc.dealloc %0 : !qc.qubit }" + source = ( + " module { func.func @main() attributes {mqt.entry_point} {" + " %0 = qc.alloc : !qc.qubit qc.dealloc %0 : !qc.qubit return } }" + ) result = compile_program(source) diff --git a/test/python/test_mlir_loops.py b/test/python/test_mlir_loops.py index d198d9e85d..1f5156be18 100644 --- a/test/python/test_mlir_loops.py +++ b/test/python/test_mlir_loops.py @@ -479,9 +479,10 @@ def test_runtime_gate_parameter_is_distinct_from_local_state() -> None: assert "constant or symbolic" in str(error.value) -def test_loop_resource_allocation_is_actionable() -> None: - """Constant-true loops are valid; resource allocation inside them is a target restriction.""" - program = QCProgram.from_mlir_str(""" +def test_loop_resource_allocation_is_rejected_at_import(capfd: pytest.CaptureFixture[str]) -> None: + """Reject loop-local quantum allocations when constructing the program.""" + with pytest.raises(RuntimeError, match="MLIR operation failed"): + QCProgram.from_mlir_str(""" module { func.func @main() attributes {mqt.entry_point} { %true = arith.constant true @@ -497,12 +498,9 @@ def test_loop_resource_allocation_is_actionable() -> None: } } """) - with pytest.raises(RuntimeError, match="allocate") as qasm_error: - program.to_openqasm3() - assert "OpenQASM" in str(qasm_error.value) - with pytest.raises(RuntimeError, match="allocate them before the loop") as qiskit_error: - program.to_qiskit() - assert "qc.alloc" in str(qiskit_error.value) + diagnostic = capfd.readouterr().err + assert "'qc.alloc' op dynamic quantum allocations must be in the entry block" in diagnostic + assert "of the 'mqt.entry_point' function" in diagnostic def test_first_measurement_initializes_do_while_output() -> None: From e89cb4bf633035720b2b54b39934176efda80e22 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 8 Sep 2026 16:00:35 +0000 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=90=9B=20Enforce=20QIR=20output=20and?= =?UTF-8?q?=20resource=20ownership=20contracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use shared allocation checks at standalone QIR and mapping boundaries. Preserve quantum release control flow, allocate Adaptive scalar results consistently with result registers, and validate output-store fusion before mutation. Diagnose multiple entry returns and document the supported subset. Keep builder result ownership consistent and model qc.yield as effect-free for recursive effect analysis. Add native regressions and migration guidance. Assisted-by: GPT-6 via Codex --- .agent/plans/qir-output-contract.md | 50 ++++ CHANGELOG.md | 6 + UPGRADING.md | 13 + .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.td | 12 +- .../Conversion/QCToQIR/QIRBase/QCToQIRBase.td | 4 + .../Conversion/QCToQIR/QIRCommon/QIRCommon.h | 29 +- mlir/include/mlir/Dialect/QC/IR/QCOps.td | 2 +- .../Dialect/QIR/Builder/QIRProgramBuilder.h | 13 +- .../include/mlir/Dialect/QIR/Utils/QIRUtils.h | 2 +- .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp | 54 ++-- .../QCToQIR/QIRBase/QCToQIRBase.cpp | 10 +- .../QCToQIR/QIRCommon/QIRCommon.cpp | 270 +++++++++++------- .../QCO/Transforms/Mapping/Mapping.cpp | 8 + .../Dialect/QIR/Builder/QIRProgramBuilder.cpp | 57 +++- .../test_qc_to_qir_adaptive.cpp | 229 +++++++++++++++ .../QCToQIRBase/test_qc_to_qir_base.cpp | 30 ++ .../QCO/Transforms/Mapping/test_mapping.cpp | 41 +++ mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp | 65 ++++- 18 files changed, 715 insertions(+), 180 deletions(-) create mode 100644 .agent/plans/qir-output-contract.md diff --git a/.agent/plans/qir-output-contract.md b/.agent/plans/qir-output-contract.md new file mode 100644 index 0000000000..d04101d57d --- /dev/null +++ b/.agent/plans/qir-output-contract.md @@ -0,0 +1,50 @@ +# QIR output and resource contracts + +Status: complete. + +## Goal and scope + +Close the standalone allocation-verifier gap and repair Adaptive release and +result ownership and shared measurement/store fusion in PR #2446. Keep QC/QCO +allocation ownership at the MQT program boundary. QIR-specific restrictions +belong to conversion, with native diagnostic and semantic regression coverage. + +## Decisions + +- Standalone QIR and mapping passes invoke the shared MQT allocation check; pass + dependency loading alone does not verify input before execution. +- QIR output preparation supports a single entry-function return. Reject + multiple exits before mutation; use the actual return block rather than + block-list order. +- Preserve quantum releases at their source control-flow positions. +- Adaptive scalar results use dynamic allocation, matching returned result + arrays. Base scalar results retain static IDs. The public QIR builder must + enforce consistent result ownership independently of qubit ownership. +- Fuse same-block measurement/store pairs only with an available index and no + intervening classical interference. Known quantum effects and stores to + distinct constant indices are safe to cross. Reject uncertain cases before + mutation. +- QIR builder finalization releases owned qubits at its current insertion point; + output recording and result releases remain in the output epilogue. + +## Validation + +Native QC IR, QC-to-QCO, mapping, Base and Adaptive QIR conversion, QIR +IR/builder, compiler, and JIT suites pass: 1,253 tests. Runtime probes also +execute mixed scalar/register results and both conditional-release paths without +ownership errors. Negative tests preserve valid source IR when rejecting +multiple exits or unsafe output stores. Existing target compilation covers +stores to distinct bits across quantum modifiers; `qc.yield` supplies the +effect-free terminator contract needed by recursive effect analysis. + +`uvx nox -s lint` and `uvx nox -s cpp-lint` pass. Hosted CI is separate; full +Python and documentation suites were not run locally. + +## Outcome + +Allocation checks have one implementation, including standalone pass boundaries. +Quantum release control flow and result ownership are explicit. Output +preparation checks its supported subset before mutating returns or stores. +General multiple return normalization and arbitrary classical output-store +lowering remain outside this subset; callers receive diagnostics rather than +incorrect QIR. diff --git a/CHANGELOG.md b/CHANGELOG.md index 315aace9c5..a438ac7968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -206,6 +206,10 @@ _If you are upgrading: please see ### Fixed +- 🐛 Enforce entry-block quantum allocation across compiler entry points and + preserve Adaptive QIR release control flow and result ownership. Diagnose + unsupported output stores and multiple entry-function returns before lowering + ([#2446]) ([**@simon1hofmann**], [**@burgholzer**]) - 🐛 Initialize Qiskit classical bits before OpenQASM 3 serialization so partially measured circuits preserve their zero values ([#2399]) ([**@burgholzer**]) @@ -1411,3 +1415,5 @@ for previous changelogs._ [munich-quantum-toolkit/workflows]: https://github.com/munich-quantum-toolkit/workflows [MQT QMAP]: https://github.com/munich-quantum-toolkit/qmap [MQT QCEC]: https://github.com/munich-quantum-toolkit/qcec + +[#2446]: https://github.com/munich-quantum-toolkit/core/pull/2446 diff --git a/UPGRADING.md b/UPGRADING.md index 5d2d3de8db..2c62dc1cab 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -52,6 +52,19 @@ MQT Core 4 provides the separate `MQT::CoreBench` library and `mqt-core-bench` CLI for typed structured benchmarks. These interfaces are not drop-in replacements for the circuit factories removed in MQT Core 3.10. +### QIR conversion + +Dynamic QC/QCO qubit allocations must be in the entry block of the function +marked `mqt.entry_point`. Pass resources to helpers instead of allocating there. +QIR conversion requires a single entry-function return; route multiple exits to +one return before conversion. Keep returned CBit stores in the same block as +measurement, compute their indices beforehand, and avoid intervening accesses +that may observe or overwrite their destinations. + +Adaptive QIR now allocates scalar results dynamically, as it already did for +result registers. In `QIRProgramBuilder`, do not mix explicit `staticResult()` +references with Adaptive measurements or dynamic result registers. + ### QIR execution Dynamic QIR inputs must use the current QIR 2.1 resource-management interface. diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td index d75ff31440..b41130f74f 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td @@ -20,7 +20,11 @@ def QCToQIRAdaptive : Pass<"qc-to-qir-adaptive", "mlir::ModuleOp"> { Requirements: - Input is a valid module in the QC dialect. - - The entry function must be marked with `mqt.entry_point`. + - The entry function must be marked with `mqt.entry_point` and have a single return. + - Stores to returned CBit registers must share a block with their measurement, + use an index available at measurement (or a constant), and have no + intervening classical memory effects except stores to provably distinct + constant indices of the same register. Behavior: @@ -30,9 +34,13 @@ def QCToQIRAdaptive : Pass<"qc-to-qir-adaptive", "mlir::ModuleOp"> { 0. Initialization block: Sets up the execution environment and performs required runtime initialization. 1. Epilogue block: Records measurement results and returns from the entry function. Any blocks in-between have no restrictions regarding their operations as long as they are supported. + - Quantum releases retain their original control-flow positions. + - Scalar and register results are dynamically allocated and released after output recording. - Measurement results may be used as classical values to drive conditional branches. - Non-quantum dialects are lowered via MLIR's built-in conversions. }]; - let dependentDialects = ["mlir::LLVM::LLVMDialect"]; + let dependentDialects = ["mlir::LLVM::LLVMDialect", + "mlir::cf::ControlFlowDialect", + "mlir::arith::ArithDialect"]; } diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td index f9adceeefe..81029df93f 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td @@ -19,6 +19,10 @@ def QCToQIRBase : Pass<"qc-to-qir-base", "mlir::ModuleOp"> { Requirements: + - Stores to returned CBit registers must share a block with their measurement, + use an index available at measurement (or a constant), and have no + intervening classical memory effects except stores to provably distinct + constant indices of the same register. - Input is a valid module in the QC dialect. - The entry function must be marked with `mqt.entry_point`. - The input entry function must consist of a single block. diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h b/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h index 5e03632def..123ae1ff17 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h @@ -63,12 +63,13 @@ struct LoweringState { /// Destination register index and bit index of each stored measurement. DenseMap> cregMeasurements; - /// Map from index to `StaticResult` - DenseMap staticResults; + /// Indexed scalar results, dynamically allocated in Adaptive and static in + /// Base. + DenseMap scalarResults; - /// Metadata for returned static measurement results. Each entry is a defining + /// Metadata for returned scalar measurement results. Each entry is a defining /// `qc::MeasureOp` - DenseSet returnedStaticResults; + DenseSet returnedScalarResults; /// Converted controls associated with their specific body unitary. DenseMap> controlledGates; @@ -172,15 +173,15 @@ void addOutputRecording(LLVM::LLVMFuncOp& main, MLIRContext* ctx, * @brief Prepares classical result registers for QIR conversion * * @details - * Inventories classical result registers, records the destination of each - * stored measurement, consumes supported classical-register stores, and strips - * classical results from `func::ReturnOp` operations so QIR output recording - * can replace them. - * - * A direct measurement-result store is consumed because the QIR measurement - * call writes to the corresponding result slot. Other classical-register - * stores are rejected. Register initialization comes from `cbit.alloc` and - * needs no operation-order recognition. + * Requires a single entry-function return. Inventories classical result + * registers and validates output stores before rewriting returns or stores. + * A returned-register store must share a block with its measurement and use + * an index available there (or a constant). Intervening operations must be + * effect-free, affect only quantum resources, or store to a provably distinct + * constant index of the same register. The QIR measurement can then write + * directly to the destination without changing observable order or control + * flow. Other stores to returned registers are rejected; local CBit stores + * retain their ordinary semantics. * * This must be called **before** func-to-LLVM conversion, while * `func::ReturnOp`, `qc::MeasureOp`, and `cbit::StoreOp` are still in the IR. @@ -196,6 +197,6 @@ void addOutputRecording(LLVM::LLVMFuncOp& main, MLIRContext* ctx, * returned classical bit register */ Value getResultPtr(LoweringState& state, Operation* op, - ConversionPatternRewriter& rewriter); + ConversionPatternRewriter& rewriter, bool dynamic); } // namespace mlir diff --git a/mlir/include/mlir/Dialect/QC/IR/QCOps.td b/mlir/include/mlir/Dialect/QC/IR/QCOps.td index c29933e21b..913060aec4 100644 --- a/mlir/include/mlir/Dialect/QC/IR/QCOps.td +++ b/mlir/include/mlir/Dialect/QC/IR/QCOps.td @@ -1021,7 +1021,7 @@ def CallOp // Modifiers //===----------------------------------------------------------------------===// -def YieldOp : QCOp<"yield", traits = [Terminator]> { +def YieldOp : QCOp<"yield", traits = [Pure, Terminator]> { let summary = "Yield from a modifier region"; let description = [{ Terminates a modifier region, yielding control back to the enclosing operation. diff --git a/mlir/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h b/mlir/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h index 05b9759482..859ed3bebc 100644 --- a/mlir/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h +++ b/mlir/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h @@ -316,6 +316,11 @@ class QIRProgramBuilder final : public ImplicitLocOpBuilder { * is recorded during `finalize()`. * * @param qubit The qubit to measure + * Base uses static result IDs. Adaptive allocates dynamic result slots once + * in the entry block and releases them after output recording. An explicit + * `staticResult()` cannot be mixed with Adaptive measurements or result + * arrays. + * * @param index The index for result pointer * @param record Whether the measurement should be recorded in the output * @return An LLVM pointer to the measurement result @@ -1233,8 +1238,8 @@ class QIRProgramBuilder final : public ImplicitLocOpBuilder { /// Map from register name to `ClassicalRegister` llvm::StringMap cregs; - /// Map from index to `StaticResult` - DenseMap staticResults; + /// Indexed scalar results for output recording. + DenseMap scalarResults; /// Helper variable for storing the LLVM pointer type Type ptrType; @@ -1273,6 +1278,10 @@ class QIRProgramBuilder final : public ImplicitLocOpBuilder { /// Track whether static or dynamic qubit allocation is used. AllocationMode allocationMode = AllocationMode::Unset; + AllocationMode resultAllocationMode = AllocationMode::Unset; + + Value getResult(int64_t index, bool record, AllocationMode mode); + void ensureResultAllocationMode(AllocationMode requestedMode); /// Track whether Base or Adaptive Profile is used. Profile profile = Profile::Adaptive; diff --git a/mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h b/mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h index 8ab843a618..7d9a78feef 100644 --- a/mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h +++ b/mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h @@ -214,7 +214,7 @@ struct ClassicalRegister { Value array; }; -/// A static result (i.e., a result that is not part of a classical register). +/// An indexed scalar result for output recording; its pointer may be dynamic. struct StaticResult { /// The result pointer. Value pointer; diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index a0c6691dca..bc4edf29e7 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -446,22 +446,16 @@ struct ConvertMemRefDeallocOp final auto i64Type = rewriter.getI64Type(); auto ptrType = LLVM::LLVMPointerType::get(ctx); - // Save current insertion point - const OpBuilder::InsertionGuard guard(rewriter); - - // Release resources in output block - rewriter.setInsertionPoint(state.outputBlock->getTerminator()); + auto size = state.qregSizes.lookup(op.getMemref()); + if (!size) { + return rewriter.notifyMatchFailure(op, "unknown qubit register"); + } auto fnSig = LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(ctx), {i64Type, ptrType}); auto fnDec = getOrCreateFunctionDeclaration(rewriter, op, QIR_QUBIT_ARRAY_RELEASE, fnSig); - auto size = state.qregSizes.lookup(op.getMemref()); - if (!size) { - return rewriter.notifyMatchFailure(op, "unknown qubit register"); - } - // Create the release call LLVM::CallOp::create(rewriter, op.getLoc(), fnDec, ValueRange{size, adaptor.getMemref()}); @@ -529,16 +523,9 @@ struct ConvertQCDeallocOp final : StatefulOpConversionPattern { LogicalResult matchAndRewrite(DeallocOp op, OpAdaptor adaptor, ConversionPatternRewriter& rewriter) const override { - auto& state = getState(); auto* ctx = getContext(); auto ptrType = LLVM::LLVMPointerType::get(ctx); - // Save current insertion point - const OpBuilder::InsertionGuard guard(rewriter); - - // Release resources in output block - rewriter.setInsertionPoint(state.outputBlock->getTerminator()); - auto fnSig = LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(ctx), {ptrType}); auto fnDec = @@ -594,7 +581,7 @@ struct ConvertQCResetOp final : StatefulOpConversionPattern { * @details * For measurements with register information, a result array is allocated and * all result pointers are loaded. - * For measurements without register information, a static result pointer is + * For measurements without register information, a dynamic result pointer is * used. * If the operation has an user, a read result call operation is created to * convert the result !llvm.ptr to an i1 value. @@ -623,7 +610,7 @@ struct ConvertQCMeasureOp final : StatefulOpConversionPattern { auto result = resolveRegisterMeasurement(state, op.getOperation(), rewriter); if (!result) { - result = getResultPtr(state, op.getOperation(), rewriter); + result = getResultPtr(state, op.getOperation(), rewriter, true); } // Create measure call @@ -683,8 +670,8 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { * 1. **Entry block**: Contains constant operations and initialization * 2. **Intermediate blocks**: Original function structure containing * quantum operations - * 3. **Output block**: Contains output recording calls and qubit release - * calls + * 3. **Output block**: Contains output recording and result release calls. + * Quantum releases remain at their source locations. * * @param main The main LLVM function to restructure * @param state The LoweringState of the conversion pass @@ -692,7 +679,9 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { static void ensureBlocks(LLVM::LLVMFuncOp& main, LoweringState& state) { OpBuilder builder(main.getBody()); auto* firstBlock = &main.front(); - auto* lastBlock = &main.back(); + LLVM::ReturnOp returnOp; + main.walk([&](LLVM::ReturnOp op) { returnOp = op; }); + auto* returnBlock = returnOp->getBlock(); auto* entryBlock = builder.createBlock(&main.getBody()); main.getBlocks().splice(Region::iterator(firstBlock), main.getBlocks(), @@ -704,10 +693,9 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { builder.setInsertionPointToEnd(entryBlock); LLVM::BrOp::create(builder, main->getLoc(), firstBlock); - auto* terminatorOp = lastBlock->getTerminator(); - terminatorOp->moveBefore(outputBlock, outputBlock->end()); + returnOp->moveBefore(outputBlock, outputBlock->end()); - builder.setInsertionPointToEnd(lastBlock); + builder.setInsertionPointToEnd(returnBlock); LLVM::BrOp::create(builder, main->getLoc(), outputBlock); // Move up all constants to the beginning @@ -736,7 +724,7 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { builder.setInsertionPoint(state->outputBlock->getTerminator()); - for (auto& [_, result] : state->staticResults) { + for (auto& [_, result] : state->scalarResults) { auto sig = LLVM::LLVMFunctionType::get(voidType, {ptrType}); auto dec = getOrCreateFunctionDeclaration(builder, main, QIR_RESULT_RELEASE, sig); @@ -758,6 +746,10 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { void runOnOperation() override { MLIRContext* ctx = &getContext(); auto moduleOp = getOperation(); + if (failed(mqt::verifyQuantumAllocations(moduleOp))) { + signalPassFailure(); + return; + } auto entryPoint = mqt::getEntryPoint(moduleOp); if (!entryPoint) { moduleOp->emitError("no main function with mqt.entry_point found"); @@ -775,6 +767,11 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { target.addLegalDialect(); + if (failed(prepareClassicalResults(moduleOp, state))) { + signalPassFailure(); + return; + } + // Stage 1: Convert scf dialect to cf { RewritePatternSet scfPatterns(ctx); @@ -789,11 +786,6 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { } } - // Stage 2.0: Prepare classical result registers - if (failed(prepareClassicalResults(moduleOp, state))) { - signalPassFailure(); - return; - } { RewritePatternSet patterns(ctx); cbit::populateCBitDecompositionPatterns(patterns); diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index c67a0166cf..0d4ff11e22 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -148,13 +148,13 @@ struct ConvertCBitAllocOp final : StatefulOpConversionPattern { OpBuilder::InsertionGuard guard(rewriter); rewriter.setInsertionPoint(state.entryBlock->getTerminator()); reg.results.reserve(static_cast(*size)); - const auto base = static_cast(state.staticResults.size()); + const auto base = static_cast(state.scalarResults.size()); for (int64_t i = 0; i < *size; ++i) { const auto index = base + i; auto result = createPointerFromIndex(rewriter, op.getLoc(), index); reg.results.push_back(result); // The results are recorded as part of the register - state.staticResults.try_emplace( + state.scalarResults.try_emplace( index, qir::StaticResult{.pointer = result, .record = false}); } @@ -373,7 +373,7 @@ struct ConvertQCMeasureOp final : StatefulOpConversionPattern { } auto result = *registerResult; if (!result) { - result = getResultPtr(state, op.getOperation(), rewriter); + result = getResultPtr(state, op.getOperation(), rewriter, false); } /// Preserve instruction order until terminal measurements are verified. @@ -478,6 +478,10 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { void runOnOperation() override { MLIRContext* ctx = &getContext(); auto moduleOp = getOperation(); + if (failed(mqt::verifyQuantumAllocations(moduleOp))) { + signalPassFailure(); + return; + } auto entryPoint = mqt::getEntryPoint(moduleOp); if (!entryPoint) { moduleOp->emitError("no main function with mqt.entry_point found"); diff --git a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp index 13fa52d2f6..4c20dcafa9 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp @@ -17,6 +17,7 @@ #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" +#include #include #include #include @@ -30,15 +31,18 @@ #include #include #include +#include #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -416,7 +420,7 @@ void addOutputRecording(LLVM::LLVMFuncOp& main, MLIRContext* ctx, for (const auto registerIndex : state.returnedCregs) { returnedRegisters.push_back(std::move(state.cregs[registerIndex])); } - emitOutputRecording(builder, main, returnedRegisters, state.staticResults); + emitOutputRecording(builder, main, returnedRegisters, state.scalarResults); } void populateQCToQIRPatterns(RewritePatternSet& patterns, @@ -434,13 +438,25 @@ void populateQCToQIRPatterns(RewritePatternSet& patterns, } Value getResultPtr(LoweringState& state, Operation* op, - ConversionPatternRewriter& rewriter) { + ConversionPatternRewriter& rewriter, bool dynamic) { OpBuilder::InsertionGuard guard(rewriter); rewriter.setInsertionPoint(state.entryBlock->getTerminator()); - const auto index = static_cast(state.staticResults.size()); - const auto record = state.returnedStaticResults.contains(op); - auto result = createPointerFromIndex(rewriter, op->getLoc(), index); - state.staticResults.try_emplace( + const auto index = static_cast(state.scalarResults.size()); + const auto record = state.returnedScalarResults.contains(op); + Value result; + if (dynamic) { + auto ptrType = LLVM::LLVMPointerType::get(rewriter.getContext()); + auto signature = LLVM::LLVMFunctionType::get(ptrType, {ptrType}); + auto declaration = getOrCreateFunctionDeclaration( + rewriter, op, QIR_RESULT_ALLOC, signature); + auto zero = LLVM::ZeroOp::create(rewriter, op->getLoc(), ptrType); + result = LLVM::CallOp::create(rewriter, op->getLoc(), declaration, + zero.getResult()) + .getResult(); + } else { + result = createPointerFromIndex(rewriter, op->getLoc(), index); + } + state.scalarResults.try_emplace( index, qir::StaticResult{.pointer = result, .record = record}); return result; } @@ -466,120 +482,156 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, if (hasInvalidMemory) { return failure(); } + auto funcOp = mqt::getEntryPoint(cast(moduleOp)); + SmallVector returns; + funcOp.walk([&](func::ReturnOp op) { returns.push_back(op); }); + if (returns.size() != 1) { + return funcOp.emitError( + "QIR output requires a single return in the entry function"); + } + auto returnOp = returns.front(); + SmallVector keptOperands; + SmallVector keptReturnTypes; SmallVector consumedStores; - moduleOp->walk([&](func::FuncOp funcOp) { - if (!mqt::isEntryPoint(funcOp)) { - return; + DominanceInfo dominance(funcOp); + + funcOp.walk([&](memref::AllocOp allocOp) { + const auto type = allocOp.getType(); + if (type.getRank() != 1 || !isa(type.getElementType())) { + allocOp.emitError( + "QIR conversion only supports generic memrefs for " + "one-dimensional qc.qubit registers; use CBit for classical " + "registers"); + hasInvalidMemory = true; } + }); - funcOp.walk([&](memref::AllocOp allocOp) { - const auto type = allocOp.getType(); - if (type.getRank() != 1 || !isa(type.getElementType())) { - allocOp.emitError( - "QIR conversion only supports generic memrefs for " - "one-dimensional qc.qubit registers; use CBit for classical " - "registers"); - hasInvalidMemory = true; - } - }); - - funcOp.walk([&](cbit::AllocOp allocOp) { - const auto [it, inserted] = state.cregIndices.try_emplace( - allocOp.getOperation(), state.cregs.size()); - if (inserted) { - state.cregs.emplace_back(); - } - auto& reg = state.cregs[it->second]; - reg.record = false; - if (const auto name = allocOp->getAttrOfType( - mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { - reg.label = name.str(); - } - const auto size = allocOp.getResult().getType().getWidth(); - reg.size = size; - }); - - const auto markRegisterForRecording = [&](const size_t registerIndex) { - auto& reg = state.cregs[registerIndex]; - if (reg.record) { - return; - } - if (reg.label.empty()) { - reg.label = "c" + std::to_string(state.returnedCregs.size()); - } - reg.record = true; - state.returnedCregs.push_back(registerIndex); - }; - - funcOp.walk([&](func::ReturnOp returnOp) { - SmallVector keptOperands; - SmallVector keptReturnTypes; - - for (auto operand : returnOp.getOperands()) { - if (auto measureOp = operand.getDefiningOp()) { - state.returnedStaticResults.insert(measureOp.getOperation()); - } else if (auto allocOp = operand.getDefiningOp(); - allocOp && - state.cregIndices.contains(allocOp.getOperation())) { - markRegisterForRecording( - state.cregIndices.at(allocOp.getOperation())); - } else { - keptOperands.push_back(operand); - keptReturnTypes.push_back(operand.getType()); - } - } - - if (keptOperands.empty() && !returnOp.getOperands().empty()) { - OpBuilder builder(returnOp); - auto zero = - arith::ConstantIntOp::create(builder, returnOp.getLoc(), 0, 64); - keptOperands.push_back(zero); - keptReturnTypes.push_back(zero.getType()); - } - - returnOp.getOperandsMutable().assign(keptOperands); + funcOp.walk([&](cbit::AllocOp allocOp) { + const auto [it, inserted] = state.cregIndices.try_emplace( + allocOp.getOperation(), state.cregs.size()); + if (inserted) { + state.cregs.emplace_back(); + } + auto& reg = state.cregs[it->second]; + reg.record = false; + if (const auto name = allocOp->getAttrOfType( + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { + reg.label = name.str(); + } + const auto size = allocOp.getResult().getType().getWidth(); + reg.size = size; + }); - funcOp.setFunctionType(FunctionType::get( - funcOp.getContext(), funcOp.getFunctionType().getInputs(), - keptReturnTypes)); - }); + const auto markRegisterForRecording = [&](const size_t registerIndex) { + auto& reg = state.cregs[registerIndex]; + if (reg.record) { + return; + } + if (reg.label.empty()) { + reg.label = "c" + std::to_string(state.returnedCregs.size()); + } + reg.record = true; + state.returnedCregs.push_back(registerIndex); + }; + + for (auto operand : returnOp.getOperands()) { + if (auto measureOp = operand.getDefiningOp()) { + state.returnedScalarResults.insert(measureOp.getOperation()); + } else if (auto allocOp = operand.getDefiningOp(); + allocOp && state.cregIndices.contains(allocOp.getOperation())) { + markRegisterForRecording(state.cregIndices.at(allocOp.getOperation())); + } else { + keptOperands.push_back(operand); + keptReturnTypes.push_back(operand.getType()); + } + } - funcOp.walk([&](cbit::StoreOp storeOp) { - auto allocOp = storeOp.getReg().getDefiningOp(); - if (!allocOp || !state.cregIndices.contains(allocOp.getOperation())) { - storeOp.emitError( - "QIR conversion requires direct CBit register allocations"); - hasInvalidMemory = true; - return; - } - const auto registerIndex = state.cregIndices.at(allocOp.getOperation()); - if (!state.cregs[registerIndex].record) { - return; - } - auto measureOp = storeOp.getValue().getDefiningOp(); - if (!measureOp) { - storeOp.emitError( - "QIR conversion does not support non-measurement stores to " - "returned CBit registers"); - hasInvalidMemory = true; - return; + funcOp.walk([&](cbit::StoreOp storeOp) { + auto allocOp = storeOp.getReg().getDefiningOp(); + if (!allocOp || !state.cregIndices.contains(allocOp.getOperation())) { + storeOp.emitError( + "QIR conversion requires direct CBit register allocations"); + hasInvalidMemory = true; + return; + } + const auto registerIndex = state.cregIndices.at(allocOp.getOperation()); + if (!state.cregs[registerIndex].record) { + return; + } + auto measureOp = storeOp.getValue().getDefiningOp(); + if (!measureOp) { + storeOp.emitError( + "QIR conversion does not support non-measurement stores to " + "returned CBit registers"); + hasInvalidMemory = true; + return; + } + auto* indexProducer = storeOp.getIndex().getDefiningOp(); + bool canFuse = + measureOp->getBlock() == storeOp->getBlock() && + (dominance.dominates(storeOp.getIndex(), measureOp) || + (indexProducer && indexProducer->hasTrait())); + for (auto* next = measureOp->getNextNode(); + canFuse && next != storeOp.getOperation(); + next = next->getNextNode()) { + /// These unscoped quantum effects cannot access CBit storage. + if (isa(next)) { + continue; } - const auto destination = - std::pair{registerIndex, storeOp.getIndex()}; - const auto [it, inserted] = state.cregMeasurements.try_emplace( - measureOp.getOperation(), destination); - if (!inserted && it->second != destination) { - storeOp.emitError( - "a measurement result cannot be stored in multiple classical " - "register locations during QIR conversion"); - hasInvalidMemory = true; + if (auto otherStore = dyn_cast(next); + otherStore && otherStore.getReg() == storeOp.getReg()) { + const auto index = getConstantIntValue(storeOp.getIndex()); + const auto otherIndex = getConstantIntValue(otherStore.getIndex()); + if (index && otherIndex && *index != *otherIndex) { + continue; + } } - consumedStores.push_back(storeOp); - }); + const auto effects = getEffectsRecursively(next); + canFuse = effects && llvm::all_of(*effects, [](const auto& effect) { + auto value = effect.getValue(); + if (!value) { + return false; + } + if (isa(value.getType())) { + return true; + } + auto memref = dyn_cast(value.getType()); + return memref && isa(memref.getElementType()); + }); + } + if (!canFuse) { + storeOp.emitError("QIR output cannot fuse this measurement/store pair: " + "require the same " + "block, an index available at measurement, and no " + "intervening classical memory effects"); + hasInvalidMemory = true; + return; + } + const auto destination = + std::pair{registerIndex, storeOp.getIndex()}; + const auto [it, inserted] = state.cregMeasurements.try_emplace( + measureOp.getOperation(), destination); + if (!inserted && it->second != destination) { + storeOp.emitError("a measurement result cannot be stored in multiple " + "classical register locations during QIR conversion"); + hasInvalidMemory = true; + } + consumedStores.push_back(storeOp); }); if (hasInvalidMemory) { return failure(); } + + if (keptOperands.empty() && !returnOp.getOperands().empty()) { + OpBuilder builder(returnOp); + auto zero = arith::ConstantIntOp::create(builder, returnOp.getLoc(), 0, 64); + keptOperands.push_back(zero); + keptReturnTypes.push_back(zero.getType()); + } + returnOp.getOperandsMutable().assign(keptOperands); + funcOp.setFunctionType(FunctionType::get(funcOp.getContext(), + funcOp.getFunctionType().getInputs(), + keptReturnTypes)); for (auto storeOp : consumedStores) { storeOp.erase(); } diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 810c692a36..579a4011c9 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -320,6 +320,10 @@ struct PlacementPass final protected: void runOnOperation() override { auto moduleOp = getOperation(); + if (failed(mqt::verifyQuantumAllocations(moduleOp))) { + signalPassFailure(); + return; + } auto func = mqt::getEntryPoint(moduleOp); if (!func) { moduleOp.emitError() << "does not contain an entry point function"; @@ -565,6 +569,10 @@ struct MappingPass : impl::MappingPassBase { } auto moduleOp = getOperation(); + if (failed(mqt::verifyQuantumAllocations(moduleOp))) { + signalPassFailure(); + return; + } if (target->connectivityKind() != CompilerTarget::Connectivity::Kind::Explicit) { moduleOp.emitError() diff --git a/mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp b/mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp index ca0a9b11f1..73fb7348a7 100644 --- a/mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp +++ b/mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp @@ -193,8 +193,14 @@ Value QIRProgramBuilder::staticQubit(const int64_t index) { return qubit; } -Value QIRProgramBuilder::staticResult(const int64_t index, const bool record) { +Value QIRProgramBuilder::staticResult(int64_t index, bool record) { + return getResult(index, record, AllocationMode::Static); +} + +Value QIRProgramBuilder::getResult(int64_t index, bool record, + AllocationMode mode) { checkFinalized(); + ensureResultAllocationMode(mode); // Save current insertion point InsertionGuard guard(*this); @@ -207,16 +213,25 @@ Value QIRProgramBuilder::staticResult(const int64_t index, const bool record) { } Value result; - if (const auto it = staticResults.find(index); it != staticResults.end()) { + if (const auto it = scalarResults.find(index); it != scalarResults.end()) { result = it->second.pointer; if (record) { it->second.record = true; } } else { - result = createPointerFromIndex(*this, getLoc(), index); - staticResults.try_emplace( + if (mode == AllocationMode::Dynamic) { + auto signature = LLVM::LLVMFunctionType::get(ptrType, {ptrType}); + auto declaration = getOrCreateFunctionDeclaration( + *this, module, QIR_RESULT_ALLOC, signature); + auto zero = LLVM::ZeroOp::create(*this, ptrType); + result = LLVM::CallOp::create(*this, declaration, zero.getResult()) + .getResult(); + resultPtrs.insert(result); + } else { + result = createPointerFromIndex(*this, getLoc(), index); + } + scalarResults.try_emplace( index, qir::StaticResult{.pointer = result, .record = record}); - resultPtrs.insert(result); } // Update result count @@ -324,7 +339,8 @@ QIRProgramBuilder::allocClassicalBitRegister(const int64_t size, setInsertionPoint(entryBlock->getTerminator()); if (profile == Profile::Adaptive) { - // Adaptive Profile: Create a dynamic result array + /// Adaptive Profile: Create a dynamic result array. + ensureResultAllocationMode(AllocationMode::Dynamic); auto fnSig = LLVM::LLVMFunctionType::get(voidType, {getI64Type(), ptrType, ptrType}); auto fnDec = getOrCreateFunctionDeclaration(*this, module, @@ -386,7 +402,10 @@ Value QIRProgramBuilder::measure(Value qubit, const int64_t index, setInsertionPoint(entryBlock->getTerminator()); // Get or create result pointer - auto result = staticResult(index, record); + auto result = + getResult(index, record, + profile == Profile::Adaptive ? AllocationMode::Dynamic + : AllocationMode::Static); // Only set the insertion point if the Base Profile is used if (profile == Profile::Base) { @@ -970,12 +989,22 @@ void QIRProgramBuilder::ensureAllocationMode( llvm::reportFatalUsageError(message.c_str()); } +void QIRProgramBuilder::ensureResultAllocationMode( + AllocationMode requestedMode) { + if (resultAllocationMode != AllocationMode::Unset && + resultAllocationMode != requestedMode) { + llvm::reportFatalUsageError("Cannot mix static and dynamic result " + "allocation modes in QIRProgramBuilder"); + } + resultAllocationMode = requestedMode; +} + void QIRProgramBuilder::generateOutputRecording() { InsertionGuard guard(*this); setInsertionPoint(outputBlock->getTerminator()); emitOutputRecording(*this, module, llvm::to_vector(llvm::make_second_range(cregs)), - staticResults); + scalarResults); } OwningOpRef QIRProgramBuilder::finalize() { @@ -992,13 +1021,7 @@ OwningOpRef QIRProgramBuilder::finalize(Value returnValue) { // Save current insertion point InsertionGuard guard(*this); - // Add return statement with the given return values to the main function - setInsertionPointToEnd(outputBlock); - LLVM::ReturnOp::create(*this, returnValue); - - // Release resources in output block - setInsertionPoint(outputBlock->getTerminator()); - + /// Release owned qubits at the finalization point, before leaving the body. if (isAdaptive) { for (auto qubit : qubitPtrs) { auto sig = LLVM::LLVMFunctionType::get(voidType, {ptrType}); @@ -1016,6 +1039,10 @@ OwningOpRef QIRProgramBuilder::finalize(Value returnValue) { } } + setInsertionPointToEnd(outputBlock); + LLVM::ReturnOp::create(*this, returnValue); + setInsertionPoint(outputBlock->getTerminator()); + // Generate output recording in output block generateOutputRecording(); diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp index 453942b6d8..3d6892d816 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp @@ -12,6 +12,7 @@ #include "TestCaseUtils.h" #include "mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.h" #include "mlir/Dialect/CBit/IR/CBitOps.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/Passes.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" @@ -22,6 +23,7 @@ #include "qir_programs.h" #include +#include #include #include #include @@ -100,6 +102,233 @@ static LogicalResult runQCToQIRAdaptiveConversionSimple(ModuleOp moduleOp) { return pm.run(moduleOp); } +TEST(QCToQIRAdaptiveNativeTest, UsesSharedAllocationVerifierForStandalonePass) { + MLIRContext context; + context.loadDialect(); + auto module = parseSourceString(R"mlir(module { + func.func @main() attributes {mqt.entry_point} { + %condition = arith.constant false + scf.if %condition { + %q = qc.alloc : !qc.qubit + qc.dealloc %q : !qc.qubit + } + return + } + })mlir", + &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_EQ(context.getLoadedDialect(), nullptr); + bool diagnosed = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + diagnosed |= diagnostic.str().find("dynamic quantum allocations must be") != + std::string::npos; + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(*module))); + EXPECT_TRUE(diagnosed); + EXPECT_TRUE(module->lookupSymbol("main")); +} + +TEST(QCToQIRAdaptiveNativeTest, RejectsMultipleReturnsBeforeOutputPreparation) { + MLIRContext context; + context + .loadDialect(); + auto module = parseSourceString(R"mlir(module { + func.func @main() -> i1 attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %bit = qc.measure %q : !qc.qubit -> i1 + cf.cond_br %bit, ^left, ^right + ^left: + qc.dealloc %q : !qc.qubit + return %bit : i1 + ^right: + qc.dealloc %q : !qc.qubit + return %bit : i1 + } + })mlir", + &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + bool diagnosed = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + diagnosed |= diagnostic.str().find("single return") != std::string::npos; + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(*module))); + EXPECT_TRUE(diagnosed); + size_t returns = 0; + module->walk([&](func::ReturnOp op) { + ++returns; + EXPECT_EQ(op.getNumOperands(), 1); + }); + EXPECT_EQ(returns, 2); + EXPECT_TRUE(succeeded(verify(*module))); +} + +TEST(QCToQIRAdaptiveNativeTest, + PreservesConditionalReleasesAndNonFinalReturnBlock) { + MLIRContext context; + context.loadDialect(); + auto module = parseSourceString(R"mlir(module { + func.func private @condition() -> i1 + func.func @main() attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %reg = memref.alloc() : memref<1x!qc.qubit> + %condition = func.call @condition() : () -> i1 + cf.cond_br %condition, ^left, ^right + ^exit: + return + ^left: + qc.dealloc %q : !qc.qubit + memref.dealloc %reg : memref<1x!qc.qubit> + cf.br ^exit + ^right: + qc.dealloc %q : !qc.qubit + memref.dealloc %reg : memref<1x!qc.qubit> + cf.br ^exit + } + })mlir", + &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + SmallVector releases; + module->walk([&](LLVM::CallOp call) { + if (call.getCallee() == qir::QIR_QUBIT_RELEASE || + call.getCallee() == qir::QIR_QUBIT_ARRAY_RELEASE) { + releases.push_back(call); + } + }); + ASSERT_EQ(releases.size(), 4); + EXPECT_EQ(releases[0]->getBlock(), releases[1]->getBlock()); + EXPECT_EQ(releases[2]->getBlock(), releases[3]->getBlock()); + EXPECT_NE(releases[0]->getBlock(), releases[2]->getBlock()); + for (auto release : releases) { + EXPECT_TRUE(isa(release->getBlock()->getTerminator())); + } +} + +TEST(QCToQIRAdaptiveNativeTest, RejectsUnsafeOutputStoresBeforeMutation) { + for (const auto* body : { + "%a = qc.measure %q : !qc.qubit -> i1\n" + "qc.x %q : !qc.qubit\n" + "%b = qc.measure %q : !qc.qubit -> i1\n" + "cbit.store %b, %r[%i] : !cbit.reg<1>\n" + "cbit.store %a, %r[%i] : !cbit.reg<1>\n", + "%a = qc.measure %q : !qc.qubit -> i1\n" + "scf.if %condition { cbit.store %a, %r[%i] : !cbit.reg<1> }\n", + "%a = qc.measure %q : !qc.qubit -> i1\n" + "func.call @observe() : () -> ()\n" + "cbit.store %a, %r[%i] : !cbit.reg<1>\n", + "%a = qc.measure %q : !qc.qubit -> i1\n" + "%old = cbit.load %r[%i] : !cbit.reg<1>\n" + "cbit.store %a, %r[%i] : !cbit.reg<1>\n", + }) { + SCOPED_TRACE(body); + MLIRContext context; + context.loadDialect(); + auto source = std::string(R"mlir(module { + func.func private @observe() + func.func @main() -> !cbit.reg<1> attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %r = cbit.alloc(#cbit.init) : !cbit.reg<1> + %i = arith.constant 0 : index + %condition = arith.constant false + )mlir") + body + + R"mlir( + qc.dealloc %q : !qc.qubit + return %r : !cbit.reg<1> + } + })mlir"; + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + bool diagnosed = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + diagnosed |= + diagnostic.str().find("cannot fuse this measurement/store pair") != + std::string::npos; + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(*module))); + EXPECT_TRUE(diagnosed); + auto main = module->lookupSymbol("main"); + ASSERT_TRUE(main); + EXPECT_TRUE(isa(main.getResultTypes().front())); + size_t stores = 0; + main.walk([&](cbit::StoreOp) { ++stores; }); + EXPECT_GT(stores, 0); + EXPECT_TRUE(succeeded(verify(*module))); + } +} + +TEST(QCToQIRAdaptiveNativeTest, FusesStoresAcrossDisjointConstantIndices) { + MLIRContext context; + context.loadDialect(); + auto module = parseSourceString(R"mlir(module { + func.func @main() -> !cbit.reg<2> attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %r = cbit.alloc(#cbit.init) : !cbit.reg<2> + %zero = arith.constant 0 : index + %one = arith.constant 1 : index + %a = qc.measure %q : !qc.qubit -> i1 + qc.x %q : !qc.qubit + %b = qc.measure %q : !qc.qubit -> i1 + cbit.store %b, %r[%one] : !cbit.reg<2> + cbit.store %a, %r[%zero] : !cbit.reg<2> + qc.dealloc %q : !qc.qubit + return %r : !cbit.reg<2> + } + })mlir", + &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + EXPECT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(*module))); + EXPECT_TRUE(succeeded(verify(*module))); +} + +TEST(QCToQIRAdaptiveNativeTest, DynamicallyAllocatesScalarAndRegisterResults) { + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto q = builder.allocQubit(); + auto reg = builder.allocClassicalBitRegister(1); + auto scalar = builder.measure(q); + builder.measure(q, reg, 0); + builder.retype(TypeRange{scalar.getType(), reg.getType()}); + auto module = builder.finalize(ValueRange{scalar, reg}); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + LLVM::CallOp allocation; + LLVM::CallOp release; + size_t arrays = 0; + module->walk([&](LLVM::CallOp call) { + if (call.getCallee() == qir::QIR_RESULT_ALLOC) { + allocation = call; + } + if (call.getCallee() == qir::QIR_RESULT_RELEASE) { + release = call; + } + if (call.getCallee() == qir::QIR_RESULT_ARRAY_ALLOC) { + ++arrays; + } + }); + ASSERT_TRUE(allocation); + ASSERT_TRUE(release); + EXPECT_EQ(release.getOperand(0), allocation.getResult()); + EXPECT_EQ(arrays, 1); +} + TEST(QCToQIRAdaptiveNativeTest, PreservesEntryBlockQubitAllocations) { MLIRContext context; context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto q0 = builder.allocQubit(); + auto q1 = builder.allocQubit(); + auto reg = builder.allocClassicalBitRegister(1); + builder.x(q1); + auto zero = builder.measure(q0); + auto one = builder.measure(q1); + builder.storeClassicalBit(one, reg, 0); + builder.storeClassicalBit(zero, reg, 0); + builder.retype(reg.getType()); + auto module = builder.finalize(reg); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + bool diagnosed = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + diagnosed |= + diagnostic.str().find("cannot fuse this measurement/store pair") != + std::string::npos; + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRBaseConversion(*module))); + EXPECT_TRUE(diagnosed); + EXPECT_TRUE(succeeded(verify(*module))); +} + TEST(QCToQIRBaseNativeTest, RejectsMultiBlockEntryFunctionWithoutMutation) { MLIRContext context; context.loadDialect(); + auto module = parseSourceString(R"mlir(module { + func.func @main() attributes {mqt.entry_point} { + %condition = arith.constant true + scf.if %condition { + %q = qco.alloc : !qco.qubit + qco.sink %q : !qco.qubit + } + return + } + })mlir", + &rawContext); + ASSERT_TRUE(module); + ASSERT_EQ(rawContext.getLoadedDialect(), nullptr); + ASSERT_TRUE(succeeded(verify(*module))); + bool diagnosed = false; + ScopedDiagnosticHandler handler(&rawContext, [&](Diagnostic& diagnostic) { + diagnosed |= + diagnostic.str().find("dynamic quantum allocations must be") != + std::string::npos; + return success(); + }); + PassManager pm(&rawContext); + if (placement) { + pm.addPass(createPlacementPass(target)); + } else { + pm.addPass(createMappingPass(target, {})); + } + EXPECT_TRUE(failed(pm.run(*module))); + EXPECT_TRUE(diagnosed); + } +} + TEST_F(MappingPassFixture, MapTopologyOnlyWithEmptyOperationSet) { constexpr int64_t size = 3; diff --git a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp index 226ad685cd..65e8deb599 100644 --- a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp +++ b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp @@ -160,6 +160,62 @@ TEST_F(QIRTest, BuilderRejectsMixedStaticAndDynamicQubitAllocationModes) { "Cannot mix dynamic and static qubit allocation modes"); } +TEST_F(QIRTest, AdaptiveBuilderOwnsScalarAndRegisterResults) { + QIRProgramBuilder builder(context.get()); + builder.initialize(); + auto q = builder.allocQubit(); + auto scalar = builder.measure(q, 0); + auto reg = builder.allocClassicalBitRegister(1); + builder.measure(q, reg, 0); + auto module = builder.finalize(); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + auto allocation = scalar.getDefiningOp(); + ASSERT_TRUE(allocation); + EXPECT_EQ(allocation.getCallee(), QIR_RESULT_ALLOC); + size_t scalarReleases = 0; + size_t arrayReleases = 0; + module->walk([&](LLVM::CallOp call) { + if (call.getCallee() == QIR_RESULT_RELEASE) { + ++scalarReleases; + EXPECT_EQ(call.getOperand(0), scalar); + } + if (call.getCallee() == QIR_RESULT_ARRAY_RELEASE) + ++arrayReleases; + }); + EXPECT_EQ(scalarReleases, 1); + EXPECT_EQ(arrayReleases, 1); +} + +TEST_F(QIRTest, AdaptiveBuilderDoesNotReleaseExplicitStaticResults) { + QIRProgramBuilder builder(context.get()); + builder.initialize(); + builder.staticResult(0); + auto module = builder.finalize(); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + EXPECT_FALSE(module->lookupSymbol(QIR_RESULT_RELEASE)); +} + +TEST_F(QIRTest, BuilderRejectsMixedResultAllocationModes) { + EXPECT_DEATH( + { + QIRProgramBuilder builder(context.get()); + builder.initialize(); + builder.staticResult(0); + builder.allocClassicalBitRegister(1); + }, + "Cannot mix static and dynamic result allocation modes"); + EXPECT_DEATH( + { + QIRProgramBuilder builder(context.get()); + builder.initialize(); + builder.allocClassicalBitRegister(1); + builder.staticResult(0); + }, + "Cannot mix static and dynamic result allocation modes"); +} + TEST_F(QIRTest, BuilderRejectsOutOfBoundsClassicalRegisterIndices) { EXPECT_DEATH( { @@ -317,7 +373,7 @@ TEST_F(QIRTest, PreservesUnrelatedMetadataIdempotently) { OperationEquivalence::Flags::None)); } -TEST_F(QIRTest, MetadataDeclaresCapacityForStaticResourceIds) { +TEST_F(QIRTest, MetadataDeclaresResourceCapacities) { struct CapacityCase { SmallVector indices; StringRef requiredCapacity; @@ -360,7 +416,12 @@ TEST_F(QIRTest, MetadataDeclaresCapacityForStaticResourceIds) { {"required_num_qubits", "required_num_results"}) { EXPECT_TRUE(llvm::is_contained( passthrough, - builder.getStrArrayAttr({attribute, testCase.requiredCapacity}))); + builder.getStrArrayAttr( + {attribute, + attribute == "required_num_results" && + profile == QIRProgramBuilder::Profile::Adaptive + ? StringRef("0") + : testCase.requiredCapacity}))); } } } From 85444bfda21772ac85dda02f38deba36930a1b68 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 8 Sep 2026 16:25:03 +0000 Subject: [PATCH 7/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Keep=20allocation=20te?= =?UTF-8?q?sts=20on=20structured=20control=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the added CF dialect dependency from the MQT verifier tests and use SCF for the conditional-release regression. Defer changelog and upgrade guide entries for unreleased v4 functionality to the release cleanup. Assisted-by: GPT-6 via Codex --- CHANGELOG.md | 6 ----- UPGRADING.md | 13 ----------- .../test_qc_to_qir_adaptive.cpp | 22 ++++++++----------- mlir/unittests/Dialect/MQT/IR/CMakeLists.txt | 1 - mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 15 ++++--------- mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp | 3 ++- 6 files changed, 15 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a438ac7968..315aace9c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -206,10 +206,6 @@ _If you are upgrading: please see ### Fixed -- 🐛 Enforce entry-block quantum allocation across compiler entry points and - preserve Adaptive QIR release control flow and result ownership. Diagnose - unsupported output stores and multiple entry-function returns before lowering - ([#2446]) ([**@simon1hofmann**], [**@burgholzer**]) - 🐛 Initialize Qiskit classical bits before OpenQASM 3 serialization so partially measured circuits preserve their zero values ([#2399]) ([**@burgholzer**]) @@ -1415,5 +1411,3 @@ for previous changelogs._ [munich-quantum-toolkit/workflows]: https://github.com/munich-quantum-toolkit/workflows [MQT QMAP]: https://github.com/munich-quantum-toolkit/qmap [MQT QCEC]: https://github.com/munich-quantum-toolkit/qcec - -[#2446]: https://github.com/munich-quantum-toolkit/core/pull/2446 diff --git a/UPGRADING.md b/UPGRADING.md index 2c62dc1cab..5d2d3de8db 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -52,19 +52,6 @@ MQT Core 4 provides the separate `MQT::CoreBench` library and `mqt-core-bench` CLI for typed structured benchmarks. These interfaces are not drop-in replacements for the circuit factories removed in MQT Core 3.10. -### QIR conversion - -Dynamic QC/QCO qubit allocations must be in the entry block of the function -marked `mqt.entry_point`. Pass resources to helpers instead of allocating there. -QIR conversion requires a single entry-function return; route multiple exits to -one return before conversion. Keep returned CBit stores in the same block as -measurement, compute their indices beforehand, and avoid intervening accesses -that may observe or overwrite their destinations. - -Adaptive QIR now allocates scalar results dynamically, as it already did for -result registers. In `QIRProgramBuilder`, do not mix explicit `staticResult()` -references with Adaptive measurements or dynamic result registers. - ### QIR execution Dynamic QIR inputs must use the current QIR 2.1 resource-management interface. diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp index 3d6892d816..57582df7ac 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp @@ -167,10 +167,9 @@ TEST(QCToQIRAdaptiveNativeTest, RejectsMultipleReturnsBeforeOutputPreparation) { EXPECT_TRUE(succeeded(verify(*module))); } -TEST(QCToQIRAdaptiveNativeTest, - PreservesConditionalReleasesAndNonFinalReturnBlock) { +TEST(QCToQIRAdaptiveNativeTest, PreservesConditionalReleases) { MLIRContext context; - context.loadDialect(); auto module = parseSourceString(R"mlir(module { func.func private @condition() -> i1 @@ -178,17 +177,14 @@ TEST(QCToQIRAdaptiveNativeTest, %q = qc.alloc : !qc.qubit %reg = memref.alloc() : memref<1x!qc.qubit> %condition = func.call @condition() : () -> i1 - cf.cond_br %condition, ^left, ^right - ^exit: + scf.if %condition { + qc.dealloc %q : !qc.qubit + memref.dealloc %reg : memref<1x!qc.qubit> + } else { + qc.dealloc %q : !qc.qubit + memref.dealloc %reg : memref<1x!qc.qubit> + } return - ^left: - qc.dealloc %q : !qc.qubit - memref.dealloc %reg : memref<1x!qc.qubit> - cf.br ^exit - ^right: - qc.dealloc %q : !qc.qubit - memref.dealloc %reg : memref<1x!qc.qubit> - cf.br ^exit } })mlir", &context); diff --git a/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt index b93d2ac3e1..5eac7a2d13 100644 --- a/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt +++ b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt @@ -13,7 +13,6 @@ target_link_libraries( PRIVATE GTest::gtest_main MLIRArithDialect MLIRCBitDialect - MLIRControlFlowDialect MLIRFuncDialect MLIRMemRefDialect MLIRMQTDialect diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index 6b5fe3c399..2d98c6fc10 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -52,10 +51,9 @@ class MQTIRTest : public ::testing::Test { void SetUp() override { DialectRegistry registry; registry - .insert(); + .insert(); context = std::make_unique(registry); context->loadAllAvailableDialects(); } @@ -385,7 +383,7 @@ TEST_F(MQTIRTest, ChecksQuantumAllocationPlacement) { StringRef suffix; bool allowed; }; - const std::array placements{ + const std::array placements{ { { .prefix = "module { func.func @main() {\n", @@ -403,11 +401,6 @@ TEST_F(MQTIRTest, ChecksQuantumAllocationPlacement) { .suffix = "return } func.func @main() { return } }", .allowed = false, }, - { - .prefix = "module { func.func @main() {\ncf.br ^body\n^body:\n", - .suffix = "return } }", - .allowed = false, - }, { .prefix = "module {\n", .suffix = "func.func @main() { return } }", diff --git a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp index 65e8deb599..f2294a4bc4 100644 --- a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp +++ b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp @@ -180,8 +180,9 @@ TEST_F(QIRTest, AdaptiveBuilderOwnsScalarAndRegisterResults) { ++scalarReleases; EXPECT_EQ(call.getOperand(0), scalar); } - if (call.getCallee() == QIR_RESULT_ARRAY_RELEASE) + if (call.getCallee() == QIR_RESULT_ARRAY_RELEASE) { ++arrayReleases; + } }); EXPECT_EQ(scalarReleases, 1); EXPECT_EQ(arrayReleases, 1);