diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td index 041dc983c3..ea633f76e7 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td @@ -23,10 +23,18 @@ def QCToQIRBase : Pass<"qc-to-qir-base", "mlir::ModuleOp"> { - The input entry function must consist of a single block. Multi-block input functions are currently not supported. - The program must have straight-line control flow (i.e., Base Profile QIR). + - A measured qubit must not be used by another quantum instruction, + including another measurement. Gates on independent qubits may follow + measurements in the input. + - Explicit static qubit IDs cannot be mixed with qubit allocations. + - Qubit-register loads require constant, in-bounds indices into statically + sized allocations. Register aliases must be resolved before conversion. Behavior: - - Each QC quantum operation is replaced by a call to the corresponding QIR function in the LLVM dialect. + - Each QC quantum operation is replaced in place by its QIR call. After + validating qubit usage in instruction order, the pass moves terminal + measurements to the irreversible operations block. - Required QIR module flags are attached as attributes to the entry function. - The pass transforms the single-block entry function into four blocks to satisfy QIR Base Profile constraints: 0. Initialization block: Sets up the execution environment and performs required runtime initialization. diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h b/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h index 9d96a83fe3..3b1cf25224 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h @@ -12,6 +12,7 @@ #include "mlir/Dialect/QIR/Utils/QIRUtils.h" +#include #include #include #include @@ -46,6 +47,9 @@ struct LoweringState { /// Cache static qubit pointers for reuse DenseMap staticQubits; + /// Canonical Base-profile pointers for constant qubit-register elements. + DenseMap, Value> staticRegisterQubits; + /// Cache qubit register sizes for reuse DenseMap qregSizes; diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index 1add8d0cb3..f77ae428c8 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -20,6 +20,7 @@ #include "mlir/Dialect/QIR/QIRDefinitions.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" +#include #include #include #include @@ -40,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +51,7 @@ #include #include #include +#include #include #include @@ -85,6 +88,39 @@ static FailureOr resolveRegisterMeasurement(LoweringState& state, return results[static_cast(*indexValue)]; } +/// Validates canonical qubit pointers before moving measurements out of order. +static LogicalResult moveTerminalMeasurements(Block& body, + Block& measurements) { + DenseSet measuredQubits; + SmallVector measurementCalls; + for (auto call : body.getOps()) { + if (!call.getCallee() || + !call.getCallee()->starts_with("__quantum__qis__")) { + continue; + } + const bool isMeasurement = call.getCallee() == QIR_MEASURE; + /// Measurement's second pointer identifies a result, not a qubit. + auto operands = call.getOperands(); + if (isMeasurement) { + operands = operands.take_front(1); + } + for (auto operand : operands) { + if (measuredQubits.contains(operand)) { + return call.emitError( + "QIR Base Profile forbids using a qubit after measurement"); + } + } + if (isMeasurement) { + measuredQubits.insert(call.getOperand(0)); + measurementCalls.push_back(call); + } + } + for (auto call : measurementCalls) { + call->moveBefore(measurements.getTerminator()); + } + return success(); +} + namespace { /** @@ -171,6 +207,9 @@ struct ConvertMemRefAllocOp final LogicalResult matchAndRewrite(memref::AllocOp op, OpAdaptor /*adaptor*/, ConversionPatternRewriter& rewriter) const override { + if (failed(getState().ensureAllocationMode(AllocationMode::Dynamic, op))) { + return failure(); + } rewriter.eraseOp(op); return success(); } @@ -206,16 +245,23 @@ struct ConvertMemRefLoadOp final : StatefulOpConversionPattern { return rewriter.notifyMatchFailure( op, "Only one-dimensional registers are supported"); } - // Save current insertion point - const OpBuilder::InsertionGuard guard(rewriter); - - // Switch to entry block - rewriter.setInsertionPoint(state.entryBlock->getTerminator()); - - auto nqubits = state.staticQubits.size(); - auto qubit = createPointerFromIndex(rewriter, op.getLoc(), - static_cast(nqubits)); - state.staticQubits.try_emplace(static_cast(nqubits), qubit); + const auto index = getConstantIntValue(op.getIndices().front()); + if (!index || ShapedType::isDynamic(shape.front()) || + !op.getMemref().getDefiningOp()) { + return op.emitError("QIR Base Profile requires constant indices into " + "statically allocated qubit registers"); + } + if (*index < 0 || *index >= shape.front()) { + return op.emitError("qubit-register index is out of bounds"); + } + auto& qubit = state.staticRegisterQubits[{op.getMemref(), *index}]; + if (!qubit) { + const OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(state.entryBlock->getTerminator()); + const auto id = static_cast(state.staticQubits.size()); + qubit = createPointerFromIndex(rewriter, op.getLoc(), id); + state.staticQubits.try_emplace(id, qubit); + } rewriter.replaceOp(op, qubit); return success(); @@ -262,6 +308,9 @@ struct ConvertQCAllocOp final : StatefulOpConversionPattern { matchAndRewrite(AllocOp op, OpAdaptor /*adaptor*/, ConversionPatternRewriter& rewriter) const override { auto& state = getState(); + if (failed(state.ensureAllocationMode(AllocationMode::Dynamic, op))) { + return failure(); + } const OpBuilder::InsertionGuard guard(rewriter); @@ -331,8 +380,8 @@ struct ConvertQCMeasureOp final : StatefulOpConversionPattern { result = getResultPtr(state, op.getOperation(), rewriter); } - // Emit the measurement in the measurements block - rewriter.setInsertionPoint(state.measurementsBlock->getTerminator()); + /// Preserve instruction order until terminal measurements are verified. + rewriter.setInsertionPoint(op); auto fnSig = LLVM::LLVMFunctionType::get(voidType, {ptrType, ptrType}); auto fnDec = getOrCreateFunctionDeclaration(rewriter, op, QIR_MEASURE, fnSig); @@ -468,8 +517,8 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { * Insert the `__quantum__rt__initialize` call. * * **Stage 4: QC to LLVM** - * Convert QC dialect operations to QIR calls and add output recording to the - * output block. + * Convert QC dialect operations in place, validate and move terminal + * measurements, and add output recording to the output block. * * **Stage 5: Standard dialects to LLVM** * Convert arith and control flow dialects to LLVM (for index arithmetic and @@ -554,6 +603,11 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { return; } + auto& body = *std::next(main.getBody().begin()); + if (failed(moveTerminalMeasurements(body, *state.measurementsBlock))) { + signalPassFailure(); + return; + } addOutputRecording(main, ctx, state); } diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 25c2a3454a..d3464b2413 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -834,6 +834,30 @@ if (flag) { EXPECT_TRUE(qir->llvmIR().has_value()); } +TEST_F(CompilerPipelineTest, BaseMeasurementMayBeInsertedIntoFreedQTensor) { + auto qco = QCOProgram::fromMLIRString(R"mlir(module { + func.func @main() -> i1 attributes {mqt.entry_point} { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %reg = qtensor.alloc(%c1) : tensor<1x!qco.qubit> + %rest, %qubit = qtensor.extract %reg[%c0] : tensor<1x!qco.qubit> + %out, %result = qco.measure %qubit : !qco.qubit + %final = qtensor.insert %out into %rest[%c0] : tensor<1x!qco.qubit> + qtensor.dealloc %final : tensor<1x!qco.qubit> + return %result : i1 + } + })mlir"); + ASSERT_TRUE(qco); + auto qc = std::move(*qco).intoQC(); + ASSERT_TRUE(qc); + auto qir = std::move(*qc).intoQIR(QIRProfile::Base); + ASSERT_TRUE(qir); + const auto llvmIR = qir->llvmIR(); + ASSERT_TRUE(llvmIR); + EXPECT_NE(llvmIR->find("call void @__quantum__qis__mz__body"), + std::string::npos); +} + TEST_F(CompilerPipelineTest, EmitsQIR21ProfileModuleFlags) { constexpr llvm::StringLiteral source = R"qasm( OPENQASM 3.0; 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 5b62f07fc5..dda279d4b0 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 @@ -16,6 +16,7 @@ #include "mlir/Dialect/MQT/Transforms/Passes.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" +#include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" #include "mlir/Support/Passes.h" @@ -159,6 +160,200 @@ TEST(QCToQIRBaseNativeTest, RejectsMultiBlockEntryFunctionWithoutMutation) { EXPECT_EQ(entryPoint.getBlocks().size(), 2); } +static void expectMeasurementOrderRejected( + function_ref buildProgram, + StringRef expectedDiagnostic = + "QIR Base Profile forbids using a qubit after measurement") { + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto result = buildProgram(builder); + builder.retype(result.getType()); + auto moduleOp = builder.finalize(result); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + std::string message; + llvm::raw_string_ostream stream(message); + diagnostic.print(stream); + sawExpectedDiagnostic |= StringRef(message).contains(expectedDiagnostic); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRBaseConversion(*moduleOp))); + EXPECT_TRUE(sawExpectedDiagnostic); +} + +TEST(QCToQIRBaseNativeTest, RejectsGateAfterMeasurementOnSameQubit) { + expectMeasurementOrderRejected([](qc::QCProgramBuilder& builder) { + auto qubit = builder.allocQubit(); + auto result = builder.measure(qubit); + builder.x(qubit); + return result; + }); +} + +TEST(QCToQIRBaseNativeTest, RejectsGateWithMeasuredControl) { + expectMeasurementOrderRejected([](qc::QCProgramBuilder& builder) { + auto control = builder.allocQubit(); + auto target = builder.allocQubit(); + auto result = builder.measure(control); + builder.cx(control, target); + return result; + }); +} + +TEST(QCToQIRBaseNativeTest, RejectsGateOnMeasuredStaticAlias) { + expectMeasurementOrderRejected([](qc::QCProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + auto result = builder.measure(qubit); + auto alias = qc::StaticOp::create(builder, 0).getQubit(); + builder.x(alias); + return result; + }); +} + +TEST(QCToQIRBaseNativeTest, RejectsGateOnMeasuredRegisterElement) { + expectMeasurementOrderRejected([](qc::QCProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(2); + auto result = builder.measure(qubits[0]); + auto alias = builder.loadQubit(qubits.value, builder.indexConstant(0)); + builder.x(alias); + return result; + }); +} + +TEST(QCToQIRBaseNativeTest, RejectsRepeatedMeasurement) { + expectMeasurementOrderRejected([](qc::QCProgramBuilder& builder) { + auto qubit = builder.allocQubit(); + builder.measure(qubit); + return builder.measure(qubit); + }); +} + +TEST(QCToQIRBaseNativeTest, RejectsRepeatedMeasurementToSameBit) { + expectMeasurementOrderRejected(qc::repeatedMeasurementToSameBit); +} + +TEST(QCToQIRBaseNativeTest, RejectsRepeatedMeasurementToDifferentBits) { + expectMeasurementOrderRejected([](qc::QCProgramBuilder& builder) { + return qc::repeatedMeasurementToDifferentBits(builder).front(); + }); +} + +TEST(QCToQIRBaseNativeTest, RejectsMixedQubitAllocationModes) { + for (const bool registerAllocation : {false, true}) { + SCOPED_TRACE(registerAllocation); + expectMeasurementOrderRejected( + [registerAllocation](qc::QCProgramBuilder& builder) { + auto qubit = builder.staticQubit(5); + if (registerAllocation) { + memref::AllocOp::create(builder, + MemRefType::get({1}, qubit.getType())); + } else { + qc::AllocOp::create(builder); + } + return builder.measure(qubit); + }, + "cannot mix static and dynamic qubit allocation modes"); + } +} + +TEST(QCToQIRBaseNativeTest, RejectsRuntimeQubitRegisterIndex) { + expectMeasurementOrderRejected( + [](qc::QCProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(2); + auto unknown = LLVM::UndefOp::create(builder, builder.getI64Type()); + auto index = arith::IndexCastOp::create(builder, builder.getIndexType(), + unknown); + return builder.measure(builder.loadQubit(qubits.value, index)); + }, + "QIR Base Profile requires constant indices"); +} + +TEST(QCToQIRBaseNativeTest, RejectsOutOfBoundsQubitRegisterIndex) { + for (const auto index : {-1, 2}) { + SCOPED_TRACE(index); + expectMeasurementOrderRejected( + [index](qc::QCProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(2); + return builder.measure( + builder.loadQubit(qubits.value, builder.indexConstant(index))); + }, + "qubit-register index is out of bounds"); + } +} + +TEST(QCToQIRBaseNativeTest, RejectsRepeatedMeasurementThroughRegisterAlias) { + expectMeasurementOrderRejected([](qc::QCProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(1); + builder.measure(qubits[0]); + return builder.measure( + builder.loadQubit(qubits.value, builder.indexConstant(0))); + }); +} + +TEST(QCToQIRBaseNativeTest, RegisterLoadsPreserveQubitIdentity) { + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto qubits = builder.allocQubitRegister(1); + builder.x(qubits[0]); + auto result = builder.measure( + builder.loadQubit(qubits.value, builder.indexConstant(0))); + builder.retype(result.getType()); + auto moduleOp = builder.finalize(result); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(runQCToQIRBaseConversion(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + Value gateQubit; + Value measuredQubit; + moduleOp->walk([&](LLVM::CallOp call) { + if (call.getCallee() == qir::QIR_X) { + gateQubit = call.getOperand(0); + } else if (call.getCallee() == qir::QIR_MEASURE) { + measuredQubit = call.getOperand(0); + } + }); + ASSERT_TRUE(gateQubit); + EXPECT_EQ(gateQubit, measuredQubit); +} + +TEST(QCToQIRBaseNativeTest, AllowsGateAfterMeasurementOnIndependentQubit) { + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto qubits = builder.allocQubitRegister(2); + auto result = builder.measure(qubits[0]); + builder.x(qubits[1]); + builder.retype(result.getType()); + auto moduleOp = builder.finalize(result); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(runQCToQIRBaseConversion(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + SmallVector quantumCalls; + moduleOp->walk([&](LLVM::CallOp call) { + if (call.getCallee() == qir::QIR_X || + call.getCallee() == qir::QIR_MEASURE) { + quantumCalls.push_back(call); + } + }); + ASSERT_EQ(quantumCalls.size(), 2); + EXPECT_EQ(quantumCalls[0].getCallee(), qir::QIR_X); + EXPECT_EQ(quantumCalls[1].getCallee(), qir::QIR_MEASURE); + EXPECT_NE(quantumCalls[0].getOperand(0), quantumCalls[1].getOperand(0)); +} + TEST(QCToQIRBaseNativeTest, ControlledBarrierDoesNotControlFollowingGate) { expectFollowingXIsUncontrolled( [](qc::QCProgramBuilder& builder, Value control, Value target) { @@ -947,14 +1142,6 @@ INSTANTIATE_TEST_SUITE_P( "SingleMeasurementToSingleBit", MQT_NAMED_BUILDER(qc::singleMeasurementToSingleBit), MQT_NAMED_BUILDER(qir::singleMeasurementToSingleBit)}, - QCToQIRBaseTestCase{ - "RepeatedMeasurementToSameBit", - MQT_NAMED_BUILDER(qc::repeatedMeasurementToSameBit), - MQT_NAMED_BUILDER(qir::repeatedMeasurementToSameBit)}, - QCToQIRBaseTestCase{ - "RepeatedMeasurementToDifferentBits", - MQT_NAMED_BUILDER(qc::repeatedMeasurementToDifferentBits), - MQT_NAMED_BUILDER(qir::repeatedMeasurementToDifferentBits)}, QCToQIRBaseTestCase{ "MultipleClassicalRegistersAndMeasurements", MQT_NAMED_BUILDER(qc::multipleClassicalRegistersAndMeasurements),