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/.agent/plans/quantum-allocation-scope.md b/.agent/plans/quantum-allocation-scope.md new file mode 100644 index 0000000000..186569ea45 --- /dev/null +++ b/.agent/plans/quantum-allocation-scope.md @@ -0,0 +1,55 @@ +# 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. + +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/mlir/include/mlir/Compiler/Programs.h b/mlir/include/mlir/Compiler/Programs.h index c264e67b70..d4c3bdd59b 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 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/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 442d15a0bb..e44d0c8a46 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/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/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/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/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 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/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 7fd6c7eaad..7eba962dde 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -368,6 +369,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())) { @@ -391,7 +421,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 b243345472..013fe17ae5 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -174,30 +174,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) { @@ -336,6 +321,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"; @@ -572,6 +561,10 @@ struct MappingPass : impl::MappingPassBase { assert(ntrials > 0 && "expected ntrials > 0"); auto moduleOp = getOperation(); + if (failed(mqt::verifyQuantumAllocations(moduleOp))) { + signalPassFailure(); + return; + } const auto& environment = getAnalysis(); if (!environment) { 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/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index d3d42aad27..ac6988909a 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -61,6 +61,7 @@ #include #include #include +#include #include #include #include @@ -1074,8 +1075,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"; @@ -1164,7 +1168,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 @@ -1182,7 +1186,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> @@ -1197,10 +1201,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> @@ -1212,13 +1294,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 @@ -1227,7 +1309,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 @@ -1319,7 +1401,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 @@ -1822,8 +1904,9 @@ TEST_F(CompilerPipelineTest, QCOProgramCompilesForTarget) { auto compiled = parseRecordedModule(qco->str()); ASSERT_TRUE(compiled) << qco->str(); EXPECT_TRUE(verify(*compiled).succeeded()); - const auto environment = (*compiled)->getAttrOfType( - mlir::mqt::TargetEnvAttr::name); + const auto environment = + compiled.get()->getAttrOfType( + mlir::mqt::TargetEnvAttr::name); ASSERT_TRUE(environment); EXPECT_EQ(environment.getPayloadSpecification(), payload.materialize(*context)); @@ -2673,9 +2756,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 81c76e0271..6089ca020b 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 fed2299b58..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 @@ -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,255 @@ 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, PreservesConditionalReleases) { + 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 + 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 + } + })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(); + 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; diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp index dda279d4b0..81483b4378 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp @@ -129,6 +129,36 @@ TEST(QCToQIRBaseNativeTest, EmptyCtrlDoesNotControlFollowingGate) { }); } +TEST(QCToQIRBaseNativeTest, RejectsReorderedOverlappingOutputStores) { + 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 #include #include +#include #include #include #include @@ -31,9 +32,11 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -47,9 +50,10 @@ class MQTIRTest : public ::testing::Test { void SetUp() override { DialectRegistry registry; - registry.insert(); + registry + .insert(); context = std::make_unique(registry); context->loadAllAvailableDialects(); } @@ -380,6 +384,107 @@ 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 {\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( @@ -749,8 +854,8 @@ TEST_F(MQTIRTest, RoundTripsTypedTargetEnvironment) { )mlir"); ASSERT_TRUE(moduleOp); - const auto targetEnv = - (*moduleOp)->getAttrOfType(mqt::TargetEnvAttr::name); + const auto targetEnv = moduleOp.get()->getAttrOfType( + mqt::TargetEnvAttr::name); ASSERT_TRUE(targetEnv); const auto compilationTarget = targetEnv.getCompilationTarget(); EXPECT_EQ(compilationTarget.getName().getValue(), "device"); 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 0e8e51081c..afed1e621e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -358,6 +358,47 @@ class MappingPassTest : public MappingPassFixture, }; // namespace +TEST_F(MappingPassFixture, StandalonePassesUseSharedAllocationVerifier) { + const auto target = llvm::cantFail( + CompilerTarget::create(1, Connectivity::fromCouplings({}), + NativeOperations::fromOperations({}))); + for (const bool placement : {false, true}) { + SCOPED_TRACE(placement); + MLIRContext rawContext; + rawContext.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()); + } + EXPECT_TRUE(failed(pm.run(*module))); + EXPECT_TRUE(diagnosed); + } +} + TEST_F(MappingPassFixture, RequiresTypedTargetEnvironment) { QCOProgramBuilder builder(context.get()); builder.initialize(); @@ -922,81 +963,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/Dialect/QIR/IR/test_qir_ir.cpp b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp index 226ad685cd..f2294a4bc4 100644 --- a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp +++ b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp @@ -160,6 +160,63 @@ 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 +374,7 @@ TEST_F(QIRTest, PreservesUnrelatedMetadataIdempotently) { OperationEquivalence::Flags::None)); } -TEST_F(QIRTest, MetadataDeclaresCapacityForStaticResourceIds) { +TEST_F(QIRTest, MetadataDeclaresResourceCapacities) { struct CapacityCase { SmallVector indices; StringRef requiredCapacity; @@ -360,7 +417,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}))); } } } diff --git a/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp b/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp index d889b4a2d5..192ea5be0e 100644 --- a/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp +++ b/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp @@ -2235,6 +2235,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; }", diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index a7bb3e5597..1f5720d471 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -137,7 +137,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 8614dd9a1b..d2a8971ad7 100644 --- a/test/python/test_mlir_loops.py +++ b/test/python/test_mlir_loops.py @@ -484,9 +484,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 @@ -502,12 +503,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: