From a58e6da7007f051df56d22cb47af7bc7e299a5e2 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 16:22:10 +0200 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=90=9B=20Reject=20unsafe=20Base-profi?= =?UTF-8?q?le=20measurement=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reject gates on measured qubits before moving measurements to the final block. Track static and register aliases while allowing later gates on independent qubits. Assisted-by: GPT-6 via Codex --- .../Conversion/QCToQIR/QIRBase/QCToQIRBase.td | 5 + .../QCToQIR/QIRBase/QCToQIRBase.cpp | 52 ++++++++ .../QCToQIRBase/test_qc_to_qir_base.cpp | 114 ++++++++++++++++++ 3 files changed, 171 insertions(+) diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td index 041dc983c3..1e6371d384 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td @@ -23,6 +23,11 @@ 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 gate must not act on a qubit after it has been measured. The pass + rejects this order because moving the measurement after the gate can + change the result. Gates on independent qubits may follow measurements. + Dynamic register indices must be provably independent of measured + register elements. Behavior: diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index 1add8d0cb3..2c0020bd4a 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -16,10 +16,12 @@ #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/QC/IR/QCDialect.h" +#include "mlir/Dialect/QC/IR/QCInterfaces.h" #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QIR/QIRDefinitions.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" +#include #include #include #include @@ -40,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -87,6 +90,51 @@ static FailureOr resolveRegisterMeasurement(LoweringState& state, namespace { +/// Checks that moving measurements after all gates preserves qubit order. +static LogicalResult checkMeasurementOrder(func::FuncOp entryPoint) { + // Static indices and register elements can have multiple SSA references. + const auto qubitKey = [](Value qubit) -> std::pair { + if (auto staticQubit = qubit.getDefiningOp()) { + return {Value{}, static_cast(staticQubit.getIndex())}; + } + if (auto load = qubit.getDefiningOp()) { + // A dynamic index can refer to any element of its register. + const auto index = + load.getIndices().size() == 1 + ? getConstantIntValue(load.getIndices().front()).value_or(-1) + : -1; + return {load.getMemref(), index}; + } + return {qubit, 0}; + }; + + DenseSet> measuredQubits; + DenseSet measuredRegisters; + for (auto& operation : entryPoint.front()) { + if (auto measurement = dyn_cast(operation)) { + const auto key = qubitKey(measurement.getQubit()); + measuredQubits.insert(key); + measuredRegisters.insert(key.first); + continue; + } + auto unitary = dyn_cast(operation); + if (!unitary || isa(operation)) { + continue; + } + for (auto qubit : unitary.getQubits()) { + const auto key = qubitKey(qubit); + if (measuredQubits.contains(key) || + measuredQubits.contains({key.first, -1}) || + (key.second == -1 && measuredRegisters.contains(key.first))) { + return operation.emitError( + "QIR Base Profile requires gates to precede measurements on " + "the same qubit"); + } + } + } + return success(); +} + /** * @brief Converts `cbit.alloc` to static result * pointers represented by `llvm.inttoptr` operations @@ -493,6 +541,10 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { signalPassFailure(); return; } + if (failed(checkMeasurementOrder(entryPoint))) { + signalPassFailure(); + return; + } auto entryPointName = entryPoint.getSymNameAttr(); if (failed(mqt::normalizeGlobalPhases(moduleOp))) { signalPassFailure(); 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..fede0bc99f 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,119 @@ TEST(QCToQIRBaseNativeTest, RejectsMultiBlockEntryFunctionWithoutMutation) { EXPECT_EQ(entryPoint.getBlocks().size(), 2); } +static void expectMeasurementOrderRejected( + function_ref buildProgram) { + 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( + "QIR Base Profile requires gates to precede measurements on the " + "same qubit"); + 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, RejectsPossiblyMeasuredRegisterElement) { + for (const auto dynamicMeasurement : {false, true}) { + SCOPED_TRACE(dynamicMeasurement); + expectMeasurementOrderRejected([&](qc::QCProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(2); + auto condition = LLVM::UndefOp::create(builder, builder.getI1Type()); + auto index = + arith::SelectOp::create(builder, condition, builder.indexConstant(0), + builder.indexConstant(1)); + auto dynamicQubit = builder.loadQubit(qubits.value, index); + auto result = + builder.measure(dynamicMeasurement ? dynamicQubit : qubits[0]); + builder.x(dynamicMeasurement ? qubits[0] : dynamicQubit); + return result; + }); + } +} + +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) { From 1c68d2e05e753621d14079606707eb58aa9a4930 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 16:38:49 +0200 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=94=87=20Preserve=20the=20GoogleTest?= =?UTF-8?q?=20setup=20override=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-6 via Codex --- .../Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp | 2 ++ 1 file changed, 2 insertions(+) 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 fede0bc99f..56c23d4456 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 @@ -74,6 +74,8 @@ class QCToQIRBaseTest : public testing::TestWithParam { protected: std::unique_ptr context; + // GoogleTest requires this override name. + // NOLINTNEXTLINE(readability-identifier-naming) void SetUp() override { DialectRegistry registry; registry.insert Date: Mon, 7 Sep 2026 17:10:22 +0200 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=94=87=20Fix=20Base=20QIR=20helper=20?= =?UTF-8?q?namespace=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the static measurement-order helper outside the anonymous namespace without changing its internal linkage or behavior. Assisted-by: GPT-6 via Codex --- mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index 2c0020bd4a..a3ec249fb3 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -88,8 +88,6 @@ static FailureOr resolveRegisterMeasurement(LoweringState& state, return results[static_cast(*indexValue)]; } -namespace { - /// Checks that moving measurements after all gates preserves qubit order. static LogicalResult checkMeasurementOrder(func::FuncOp entryPoint) { // Static indices and register elements can have multiple SSA references. @@ -135,6 +133,8 @@ static LogicalResult checkMeasurementOrder(func::FuncOp entryPoint) { return success(); } +namespace { + /** * @brief Converts `cbit.alloc` to static result * pointers represented by `llvm.inttoptr` operations From ded308ce429243fc6f4d8a31a42bc3b9a48142cd Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 17:50:05 +0200 Subject: [PATCH 4/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Narrow=20Base=20measur?= =?UTF-8?q?ement-order=20alias=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep constant-index register aliases, which Base lowering already accepts. Remove runtime-index alias tracking and its test and docs from this static-qubit ordering fix. Assisted-by: GPT-6 via Codex --- .../Conversion/QCToQIR/QIRBase/QCToQIRBase.td | 2 -- .../QCToQIR/QIRBase/QCToQIRBase.cpp | 24 +++++++------------ .../QCToQIRBase/test_qc_to_qir_base.cpp | 18 -------------- 3 files changed, 8 insertions(+), 36 deletions(-) diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td index 1e6371d384..10a052d112 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td @@ -26,8 +26,6 @@ def QCToQIRBase : Pass<"qc-to-qir-base", "mlir::ModuleOp"> { - A gate must not act on a qubit after it has been measured. The pass rejects this order because moving the measurement after the gate can change the result. Gates on independent qubits may follow measurements. - Dynamic register indices must be provably independent of measured - register elements. Behavior: diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index a3ec249fb3..631d42d661 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -90,29 +90,24 @@ static FailureOr resolveRegisterMeasurement(LoweringState& state, /// Checks that moving measurements after all gates preserves qubit order. static LogicalResult checkMeasurementOrder(func::FuncOp entryPoint) { - // Static indices and register elements can have multiple SSA references. + // Static qubit IDs and constant register indices can have SSA aliases. const auto qubitKey = [](Value qubit) -> std::pair { if (auto staticQubit = qubit.getDefiningOp()) { return {Value{}, static_cast(staticQubit.getIndex())}; } - if (auto load = qubit.getDefiningOp()) { - // A dynamic index can refer to any element of its register. - const auto index = - load.getIndices().size() == 1 - ? getConstantIntValue(load.getIndices().front()).value_or(-1) - : -1; - return {load.getMemref(), index}; + if (auto load = qubit.getDefiningOp(); + load && load.getIndices().size() == 1) { + if (const auto index = getConstantIntValue(load.getIndices().front())) { + return {load.getMemref(), *index}; + } } return {qubit, 0}; }; DenseSet> measuredQubits; - DenseSet measuredRegisters; for (auto& operation : entryPoint.front()) { if (auto measurement = dyn_cast(operation)) { - const auto key = qubitKey(measurement.getQubit()); - measuredQubits.insert(key); - measuredRegisters.insert(key.first); + measuredQubits.insert(qubitKey(measurement.getQubit())); continue; } auto unitary = dyn_cast(operation); @@ -120,10 +115,7 @@ static LogicalResult checkMeasurementOrder(func::FuncOp entryPoint) { continue; } for (auto qubit : unitary.getQubits()) { - const auto key = qubitKey(qubit); - if (measuredQubits.contains(key) || - measuredQubits.contains({key.first, -1}) || - (key.second == -1 && measuredRegisters.contains(key.first))) { + if (measuredQubits.contains(qubitKey(qubit))) { return operation.emitError( "QIR Base Profile requires gates to precede measurements on " "the same qubit"); 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 56c23d4456..dab234f6a1 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 @@ -228,24 +228,6 @@ TEST(QCToQIRBaseNativeTest, RejectsGateOnMeasuredRegisterElement) { }); } -TEST(QCToQIRBaseNativeTest, RejectsPossiblyMeasuredRegisterElement) { - for (const auto dynamicMeasurement : {false, true}) { - SCOPED_TRACE(dynamicMeasurement); - expectMeasurementOrderRejected([&](qc::QCProgramBuilder& builder) { - auto qubits = builder.allocQubitRegister(2); - auto condition = LLVM::UndefOp::create(builder, builder.getI1Type()); - auto index = - arith::SelectOp::create(builder, condition, builder.indexConstant(0), - builder.indexConstant(1)); - auto dynamicQubit = builder.loadQubit(qubits.value, index); - auto result = - builder.measure(dynamicMeasurement ? dynamicQubit : qubits[0]); - builder.x(dynamicMeasurement ? qubits[0] : dynamicQubit); - return result; - }); - } -} - TEST(QCToQIRBaseNativeTest, AllowsGateAfterMeasurementOnIndependentQubit) { MLIRContext context; context.loadDialect Date: Mon, 7 Sep 2026 18:31:36 +0200 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=94=87=20Remove=20redundant=20SetUp?= =?UTF-8?q?=20naming=20suppression?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-6 via Codex --- .../Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp | 2 -- 1 file changed, 2 deletions(-) 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 dab234f6a1..3db1eb5363 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 @@ -74,8 +74,6 @@ class QCToQIRBaseTest : public testing::TestWithParam { protected: std::unique_ptr context; - // GoogleTest requires this override name. - // NOLINTNEXTLINE(readability-identifier-naming) void SetUp() override { DialectRegistry registry; registry.insert Date: Mon, 7 Sep 2026 21:06:20 +0000 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=90=9B=20Validate=20terminal=20measur?= =?UTF-8?q?ements=20after=20QIR=20lowering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserve quantum instruction order until canonical qubit pointers are checked, then move measurements into the Base measurement block. Resolve repeated register loads once during lowering and reject unresolved or out-of-bounds indices and mixed allocation modes. Reject repeated Base measurements while retaining Adaptive support and QCO measurements whose qubits are inserted into a freed tensor. Assisted-by: GPT-6 via Codex --- .../Conversion/QCToQIR/QIRBase/QCToQIRBase.td | 13 +- .../Conversion/QCToQIR/QIRCommon/QIRCommon.h | 4 + .../QCToQIR/QIRBase/QCToQIRBase.cpp | 104 +++++++++------- .../Compiler/test_compiler_pipeline.cpp | 24 ++++ .../QCToQIRBase/test_qc_to_qir_base.cpp | 115 ++++++++++++++++-- 5 files changed, 197 insertions(+), 63 deletions(-) diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td index 10a052d112..ea633f76e7 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td @@ -23,13 +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 gate must not act on a qubit after it has been measured. The pass - rejects this order because moving the measurement after the gate can - change the result. Gates on independent qubits may follow measurements. + - 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 631d42d661..f77ae428c8 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -16,7 +16,6 @@ #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/QC/IR/QCDialect.h" -#include "mlir/Dialect/QC/IR/QCInterfaces.h" #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QIR/QIRDefinitions.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" @@ -52,6 +51,7 @@ #include #include #include +#include #include #include @@ -88,39 +88,35 @@ static FailureOr resolveRegisterMeasurement(LoweringState& state, return results[static_cast(*indexValue)]; } -/// Checks that moving measurements after all gates preserves qubit order. -static LogicalResult checkMeasurementOrder(func::FuncOp entryPoint) { - // Static qubit IDs and constant register indices can have SSA aliases. - const auto qubitKey = [](Value qubit) -> std::pair { - if (auto staticQubit = qubit.getDefiningOp()) { - return {Value{}, static_cast(staticQubit.getIndex())}; - } - if (auto load = qubit.getDefiningOp(); - load && load.getIndices().size() == 1) { - if (const auto index = getConstantIntValue(load.getIndices().front())) { - return {load.getMemref(), *index}; - } - } - return {qubit, 0}; - }; - - DenseSet> measuredQubits; - for (auto& operation : entryPoint.front()) { - if (auto measurement = dyn_cast(operation)) { - measuredQubits.insert(qubitKey(measurement.getQubit())); +/// 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; } - auto unitary = dyn_cast(operation); - if (!unitary || isa(operation)) { - 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 qubit : unitary.getQubits()) { - if (measuredQubits.contains(qubitKey(qubit))) { - return operation.emitError( - "QIR Base Profile requires gates to precede measurements on " - "the same qubit"); + 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(); } @@ -211,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(); } @@ -246,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(); @@ -302,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); @@ -371,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); @@ -508,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 @@ -533,10 +542,6 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { signalPassFailure(); return; } - if (failed(checkMeasurementOrder(entryPoint))) { - signalPassFailure(); - return; - } auto entryPointName = entryPoint.getSymNameAttr(); if (failed(mqt::normalizeGlobalPhases(moduleOp))) { signalPassFailure(); @@ -598,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 3db1eb5363..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 @@ -161,7 +161,9 @@ TEST(QCToQIRBaseNativeTest, RejectsMultiBlockEntryFunctionWithoutMutation) { } static void expectMeasurementOrderRejected( - function_ref buildProgram) { + function_ref buildProgram, + StringRef expectedDiagnostic = + "QIR Base Profile forbids using a qubit after measurement") { MLIRContext context; context.loadDialect(); @@ -178,9 +180,7 @@ static void expectMeasurementOrderRejected( std::string message; llvm::raw_string_ostream stream(message); diagnostic.print(stream); - sawExpectedDiagnostic |= StringRef(message).contains( - "QIR Base Profile requires gates to precede measurements on the " - "same qubit"); + sawExpectedDiagnostic |= StringRef(message).contains(expectedDiagnostic); return success(); }); EXPECT_TRUE(failed(runQCToQIRBaseConversion(*moduleOp))); @@ -226,6 +226,105 @@ TEST(QCToQIRBaseNativeTest, RejectsGateOnMeasuredRegisterElement) { }); } +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