From 56c3e10f703b923e9bd325a362bce7dcafc5242e Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sun, 23 Aug 2026 22:00:39 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Reject=20nonlinear=20QCO=20progr?= =?UTF-8?q?ams=20at=20checked=20boundaries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verify that every scalar qubit, rank-one qubit tensor, and qubit vector result or block argument has exactly one use at checked C++ and CLI ownership boundaries. Keep transformation passes built on the linear IR invariant. Assisted-by: GPT-5.6 Sol via Codex --- CHANGELOG.md | 3 +- mlir/include/mlir/Compiler/Programs.h | 20 ++- mlir/include/mlir/Dialect/QCO/QCOUtils.h | 9 +- mlir/lib/Compiler/Programs.cpp | 136 +++++++++++++----- mlir/lib/Dialect/QCO/IR/QCOUtils.cpp | 33 +++++ .../QTensor/Transforms/ShrinkRegisters.cpp | 12 -- mlir/tools/mqt-cc/mqt-cc.cpp | 5 + mlir/unittests/Compiler/mqt-cc/CMakeLists.txt | 1 + .../Compiler/mqt-cc/nonlinear.qco.mlir | 14 ++ .../Compiler/mqt-cc/verify_invalid_mlir.cmake | 24 ++-- .../Compiler/test_compiler_pipeline.cpp | 115 +++++++++++++++ 11 files changed, 305 insertions(+), 67 deletions(-) create mode 100644 mlir/unittests/Compiler/mqt-cc/nonlinear.qco.mlir diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bff080e99..0e3a1500a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ releases may include breaking changes. [#1807], [#1808], [#1815], [#1824], [#1869], [#1872], [#1914], [#1925], [#1927], [#1935], [#1936], [#1938], [#1975], [#1976], [#2006], [#2014], [#2015], [#2017], [#2026], [#2028], [#2054], [#2058], [#2125], [#2136], - [#2149], [#2150], [#2158], [#2210], [#2211]) ([**@burgholzer**], + [#2149], [#2150], [#2158], [#2210], [#2211], [#2220]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**], [**@J4MMlE**]) - ✨ Add decision diagram-based construction, simulation, and sampling for QCO @@ -851,6 +851,7 @@ for previous changelogs._ [#2257]: https://github.com/munich-quantum-toolkit/core/pull/2257 [#2249]: https://github.com/munich-quantum-toolkit/core/pull/2249 [#2246]: https://github.com/munich-quantum-toolkit/core/pull/2246 +[#2220]: https://github.com/munich-quantum-toolkit/core/pull/2220 [#2232]: https://github.com/munich-quantum-toolkit/core/pull/2232 [#2224]: https://github.com/munich-quantum-toolkit/core/pull/2224 [#2209]: https://github.com/munich-quantum-toolkit/core/pull/2209 diff --git a/mlir/include/mlir/Compiler/Programs.h b/mlir/include/mlir/Compiler/Programs.h index 8b5f725ba5..95e4bc7efd 100644 --- a/mlir/include/mlir/Compiler/Programs.h +++ b/mlir/include/mlir/Compiler/Programs.h @@ -226,8 +226,6 @@ class QCProgram final : public Program { */ class QCOProgram final : public Program { public: - explicit QCOProgram(Storage storage) : Program(std::move(storage)) {} - /// Parse QCO MLIR assembly. [[nodiscard]] static std::optional fromMLIRString(std::string_view source); @@ -236,6 +234,17 @@ class QCOProgram final : public Program { [[nodiscard]] static std::optional fromMLIRFile(const std::filesystem::path& path); + /** + * @brief Take ownership of an MLIR module that contains a QCO program. + * + * @details The context must own every dialect referenced by the module and + * must remain the module's context. The factory verifies the module, requires + * at least one operation from the QCO dialect, and verifies QCO linearity. + */ + [[nodiscard]] static std::optional + fromModule(std::shared_ptr context, + OwningOpRef moduleOp); + /// Create an independent QCO program copy. [[nodiscard]] QCOProgram copy() const; @@ -285,6 +294,13 @@ class QCOProgram final : public Program { /// Consume this program and convert it to `jeff` MLIR. [[nodiscard]] std::optional intoJeff() &&; + +private: + friend class QCProgram; + friend class JeffProgram; + + explicit QCOProgram(Storage storage) : Program(std::move(storage)) {} + [[nodiscard]] bool hasValidLinearity() const; }; /** diff --git a/mlir/include/mlir/Dialect/QCO/QCOUtils.h b/mlir/include/mlir/Dialect/QCO/QCOUtils.h index 412e2ca691..257a9d7501 100644 --- a/mlir/include/mlir/Dialect/QCO/QCOUtils.h +++ b/mlir/include/mlir/Dialect/QCO/QCOUtils.h @@ -50,11 +50,14 @@ inline bool checkDeadGate(Operation* op) { if (isa(type)) { return true; } - const auto tensorType = dyn_cast(type); - return tensorType && tensorType.getRank() == 1 && - isa(tensorType.getElementType()); + const auto shapedType = dyn_cast(type); + return isa(type) && shapedType.getRank() == 1 && + isa(shapedType.getElementType()); } +/// Verify that every linear QCO value under @p root has exactly one use. +[[nodiscard]] LogicalResult verifyLinearity(Operation* root); + /// Maximum number of modifier targets supported by @ref /// composeBodyMatrix. inline constexpr size_t kMaxModifierTargetQubits = 10; diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index dbbde4725a..d607b7aa85 100644 --- a/mlir/lib/Compiler/Programs.cpp +++ b/mlir/lib/Compiler/Programs.cpp @@ -27,6 +27,7 @@ #include "mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h" #include "mlir/Dialect/QC/Translation/TranslateQCToOpenQASM3.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" @@ -153,18 +154,13 @@ parseMLIRFile(MLIRContext* context, const std::filesystem::path& path) { template [[nodiscard]] static std::optional -parseTypedProgram(const StringRef dialect, Parse&& parse) { +parseTypedProgram(Parse&& parse) { auto context = createCompilerContext(); auto mod = std::forward(parse)(context.get()); if (failed(mod)) { return std::nullopt; } - if (!moduleUsesDialect(**mod, dialect)) { - (**mod)->emitError() << "expected a module using the '" << dialect - << "' dialect"; - return std::nullopt; - } - return ProgramType({.context = std::move(context), .mod = std::move(*mod)}); + return ProgramType::fromModule(std::move(context), std::move(*mod)); } [[nodiscard]] static LogicalResult @@ -186,6 +182,17 @@ runPasses(ModuleOp mod, return success(); } +[[nodiscard]] static LogicalResult runQCOTransformPasses( + ModuleOp mod, const llvm::function_ref populatePasses, + const StringRef failureMessage, const bool enableTiming = false, + const bool enableStatistics = false) { + if (failed(qco::verifyLinearity(mod))) { + return failure(); + } + return runPasses(mod, populatePasses, failureMessage, enableTiming, + enableStatistics); +} + //===----------------------------------------------------------------------===// // Program //===----------------------------------------------------------------------===// @@ -252,16 +259,15 @@ bool OpenQASMProgram::write(const std::filesystem::path& path) const { std::optional QCProgram::fromMLIRString(const std::string_view source) { - return parseTypedProgram("qc", [source](MLIRContext* context) { + return parseTypedProgram([source](MLIRContext* context) { return parseMLIRString(context, source); }); } std::optional QCProgram::fromMLIRFile(const std::filesystem::path& path) { - return parseTypedProgram("qc", [&path](MLIRContext* context) { - return parseMLIRFile(context, path); - }); + return parseTypedProgram( + [&path](MLIRContext* context) { return parseMLIRFile(context, path); }); } std::optional @@ -296,31 +302,32 @@ QCProgram::fromQASMFile(const std::filesystem::path& path) { std::optional QCProgram::fromModule(std::shared_ptr context, OwningOpRef moduleOp) { - if (!moduleOp) { - if (context) { - emitError(UnknownLoc::get(context.get()), + Storage storage{.context = std::move(context), .mod = std::move(moduleOp)}; + if (!storage.mod) { + if (storage.context) { + emitError(UnknownLoc::get(storage.context.get()), "cannot construct a QC program from a null module"); } return std::nullopt; } - if (!context) { - moduleOp->emitError( + if (!storage.context) { + storage.mod->emitError( "cannot construct a QC program without its owning context"); return std::nullopt; } - if (moduleOp->getContext() != context.get()) { - moduleOp->emitError( + if (storage.mod->getContext() != storage.context.get()) { + storage.mod->emitError( "cannot construct a QC program with a different MLIR context"); return std::nullopt; } - if (failed(verify(*moduleOp))) { + if (failed(verify(*storage.mod))) { return std::nullopt; } - if (!moduleUsesDialect(*moduleOp, "qc")) { - moduleOp->emitError("expected a module using the 'qc' dialect"); + if (!moduleUsesDialect(*storage.mod, "qc")) { + storage.mod->emitError("expected a module using the 'qc' dialect"); return std::nullopt; } - return QCProgram({.context = std::move(context), .mod = std::move(moduleOp)}); + return QCProgram(std::move(storage)); } QCProgram QCProgram::copy() const { return QCProgram(cloneStorage()); } @@ -352,6 +359,9 @@ std::optional QCProgram::intoQCO() && { "failed to convert QC to QCO"))) { return std::nullopt; } + if (failed(qco::verifyLinearity(mod()))) { + return std::nullopt; + } return QCOProgram(std::move(*this).releaseStorage()); } @@ -409,38 +419,82 @@ size_t QCProgram::numTwoQubitGates() const { std::optional QCOProgram::fromMLIRString(const std::string_view source) { - return parseTypedProgram("qco", [source](MLIRContext* context) { + return parseTypedProgram([source](MLIRContext* context) { return parseMLIRString(context, source); }); } std::optional QCOProgram::fromMLIRFile(const std::filesystem::path& path) { - return parseTypedProgram("qco", [&path](MLIRContext* context) { - return parseMLIRFile(context, path); - }); + return parseTypedProgram( + [&path](MLIRContext* context) { return parseMLIRFile(context, path); }); +} + +std::optional +QCOProgram::fromModule(std::shared_ptr context, + OwningOpRef moduleOp) { + Storage storage{.context = std::move(context), .mod = std::move(moduleOp)}; + if (!storage.mod) { + if (storage.context) { + emitError(UnknownLoc::get(storage.context.get()), + "cannot construct a QCO program from a null module"); + } + return std::nullopt; + } + if (!storage.context) { + storage.mod->emitError( + "cannot construct a QCO program without its owning context"); + return std::nullopt; + } + if (storage.mod->getContext() != storage.context.get()) { + storage.mod->emitError( + "cannot construct a QCO program with a different MLIR context"); + return std::nullopt; + } + if (failed(verify(*storage.mod))) { + return std::nullopt; + } + if (!moduleUsesDialect(*storage.mod, "qco")) { + storage.mod->emitError("expected a module using the 'qco' dialect"); + return std::nullopt; + } + if (failed(qco::verifyLinearity(*storage.mod))) { + return std::nullopt; + } + return QCOProgram(std::move(storage)); } QCOProgram QCOProgram::copy() const { return QCOProgram(cloneStorage()); } +bool QCOProgram::hasValidLinearity() const { + return succeeded(qco::verifyLinearity(mod())); +} + bool QCOProgram::cleanup() { - return succeeded(runPasses(mod(), populateQCOCleanupPipeline, - "failed to run the QCO cleanup pipeline")); + return succeeded( + runQCOTransformPasses(mod(), populateQCOCleanupPipeline, + "failed to run the QCO cleanup pipeline")); } bool QCOProgram::normalizeGlobalPhases() { + if (!hasValidLinearity()) { + return false; + } return succeeded(mqt::normalizeGlobalPhases(mod())); } bool QCOProgram::runPassPipeline(const std::string_view pipeline, const bool enableTiming, const bool enableStatistics) { + if (!hasValidLinearity()) { + return false; + } return succeeded( ::runPassPipeline(mod(), pipeline, enableTiming, enableStatistics)); } bool QCOProgram::mergeSingleQubitRotationGates() { - return succeeded(runPasses( + return succeeded(runQCOTransformPasses( mod(), [](OpPassManager& pm) { pm.addPass(qco::createMergeSingleQubitRotationGates()); @@ -451,7 +505,7 @@ bool QCOProgram::mergeSingleQubitRotationGates() { bool QCOProgram::fuseSingleQubitUnitaryRuns(const std::string_view basis) { qco::FuseSingleQubitUnitaryRunsOptions options; options.basis = basis; - return succeeded(runPasses( + return succeeded(runQCOTransformPasses( mod(), [&options](OpPassManager& pm) { pm.addPass(qco::createFuseSingleQubitUnitaryRuns(options)); @@ -462,7 +516,7 @@ bool QCOProgram::fuseSingleQubitUnitaryRuns(const std::string_view basis) { bool QCOProgram::unrollQuantumLoops(const int64_t factor) { qco::QuantumLoopUnrollOptions options; options.unrollFactor = factor; - return succeeded(runPasses( + return succeeded(runQCOTransformPasses( mod(), [&options](OpPassManager& pm) { pm.addNestedPass(qco::createQuantumLoopUnroll(options)); @@ -471,26 +525,26 @@ bool QCOProgram::unrollQuantumLoops(const int64_t factor) { } bool QCOProgram::liftHadamards() { - return succeeded(runPasses( + return succeeded(runQCOTransformPasses( mod(), [](OpPassManager& pm) { pm.addPass(qco::createHadamardLifting()); }, "failed to lift Hadamard gates")); } bool QCOProgram::reuseQubits() { - return succeeded(runPasses( + return succeeded(runQCOTransformPasses( mod(), [](OpPassManager& pm) { pm.addPass(qco::createReuseQubits()); }, "failed to reuse qubits")); } bool QCOProgram::runQubitReusePipeline() { - return succeeded(runPasses( + return succeeded(runQCOTransformPasses( mod(), [](OpPassManager& pm) { populateQubitReusePipeline(pm); }, "failed to run the qubit reuse pipeline")); } bool QCOProgram::decomposeMultiControlled(const uint64_t minQubits) { - return succeeded(runPasses( + return succeeded(runQCOTransformPasses( mod(), [minQubits](OpPassManager& pm) { populateDecomposeMultiControlledPipeline(pm, minQubits); @@ -501,6 +555,9 @@ bool QCOProgram::decomposeMultiControlled(const uint64_t minQubits) { bool QCOProgram::compileForTarget(const CompilerTarget& target, const bool enableTiming, const bool enableStatistics) { + if (!hasValidLinearity()) { + return false; + } return succeeded(runPasses( mod(), [&target](OpPassManager& pm) { @@ -511,7 +568,7 @@ bool QCOProgram::compileForTarget(const CompilerTarget& target, } std::optional QCOProgram::intoQC() && { - if (failed(runPasses( + if (failed(runQCOTransformPasses( mod(), [](OpPassManager& pm) { pm.addPass(createQCOToQC()); }, "failed to convert QCO to QC"))) { return std::nullopt; @@ -520,7 +577,7 @@ std::optional QCOProgram::intoQC() && { } std::optional QCOProgram::intoJeff() && { - if (failed(runPasses( + if (failed(runQCOTransformPasses( mod(), [](OpPassManager& pm) { pm.addPass(mqt::createUnrollModifiers()); @@ -599,6 +656,9 @@ std::optional JeffProgram::intoQCO() && { "failed to convert jeff to QCO"))) { return std::nullopt; } + if (failed(qco::verifyLinearity(mod()))) { + return std::nullopt; + } return QCOProgram(std::move(*this).releaseStorage()); } @@ -744,7 +804,7 @@ runDefaultPipeline(CompilerInput&& program, const ProgramFormat output, } }, std::move(program)); - if (!qco) { + if (!qco || failed(qco::verifyLinearity(qco->module()))) { return std::nullopt; } if (output == ProgramFormat::QCO) { diff --git a/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp b/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp index 5339fe0e0f..842a0cb49c 100644 --- a/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp +++ b/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp @@ -19,9 +19,12 @@ #include #include #include +#include #include #include +#include #include +#include #include #include @@ -29,6 +32,36 @@ namespace mlir::qco { +[[nodiscard]] static LogicalResult verifyLinearValue(Value value) { + if (!isLinearQubitType(value.getType()) || value.hasOneUse()) { + return success(); + } + return emitError(value.getLoc()) + << "expected linear QCO value to have exactly one use, but found " + << value.getNumUses(); +} + +LogicalResult verifyLinearity(Operation* root) { + const auto walkResult = root->walk([&](Operation* op) { + for (auto result : op->getResults()) { + if (failed(verifyLinearValue(result))) { + return WalkResult::interrupt(); + } + } + for (Region& region : op->getRegions()) { + for (Block& block : region) { + for (auto argument : block.getArguments()) { + if (failed(verifyLinearValue(argument))) { + return WalkResult::interrupt(); + } + } + } + } + return WalkResult::advance(); + }); + return walkResult.wasInterrupted() ? failure() : success(); +} + /// Returns the wire index for @p wire in @p wireIds, or `std::nullopt` if /// untracked. [[nodiscard]] static std::optional diff --git a/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp b/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp index 6874a9a1c4..cba3aba988 100644 --- a/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp +++ b/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp @@ -88,9 +88,6 @@ collectLiveIndices(AllocOp allocOp, BitVector& live, DeallocOp& deallocOp) { auto tensor = allocOp.getResult(); while (true) { auto* user = getLinearTensorUser(tensor); - if (user == nullptr) { - return failure(); - } if (auto currentDealloc = dyn_cast(user)) { if (currentDealloc.getTensor() != tensor) { @@ -177,9 +174,6 @@ struct ShrinkStaticQTensor final : OpRewritePattern { auto currentTensor = newAlloc.getResult(); while (true) { Operation* currentOp = getLinearTensorUser(oldTensor); - if (currentOp == nullptr) { - return failure(); - } if (auto deallocOp = dyn_cast(currentOp)) { if (deallocOp != oldDeallocOp || deallocOp.getTensor() != oldTensor) { @@ -207,9 +201,6 @@ struct ShrinkStaticQTensor final : OpRewritePattern { } auto oldOutTensor = extractOp.getOutTensor(); auto* nextOp = getLinearTensorUser(oldOutTensor); - if (nextOp == nullptr) { - return failure(); - } rewriter.setInsertionPoint(extractOp); auto index = arith::ConstantIndexOp::create( @@ -243,9 +234,6 @@ struct ShrinkStaticQTensor final : OpRewritePattern { } auto oldResultTensor = insertOp.getResult(); auto* nextOp = getLinearTensorUser(oldResultTensor); - if (nextOp == nullptr) { - return failure(); - } rewriter.setInsertionPoint(insertOp); auto index = arith::ConstantIndexOp::create(rewriter, insertOp.getLoc(), diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index d92687679a..fb88e19425 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -23,6 +23,7 @@ #include "mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h" #include "mlir/Dialect/QC/Translation/TranslateQCToOpenQASM3.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Support/Passes.h" @@ -524,6 +525,10 @@ static int runCompiler(int argc, char** argv) { }))) { return 1; } + if (*parsedOutputFormat != OutputFormat::QCImport && + failed(qco::verifyLinearity(*program.mod))) { + return 1; + } if (*parsedOutputFormat != OutputFormat::QCImport && *parsedOutputFormat != OutputFormat::QCO) { diff --git a/mlir/unittests/Compiler/mqt-cc/CMakeLists.txt b/mlir/unittests/Compiler/mqt-cc/CMakeLists.txt index 35c2a369be..6777382400 100644 --- a/mlir/unittests/Compiler/mqt-cc/CMakeLists.txt +++ b/mlir/unittests/Compiler/mqt-cc/CMakeLists.txt @@ -18,5 +18,6 @@ add_test( NAME mqt-core-mqt-cc-invalid-mlir-test COMMAND ${CMAKE_COMMAND} "-DMQT_CC=$" + "-DNONLINEAR_QCO_INPUT=${CMAKE_CURRENT_SOURCE_DIR}/nonlinear.qco.mlir" "-DOUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/output-$" -P "${CMAKE_CURRENT_SOURCE_DIR}/verify_invalid_mlir.cmake") diff --git a/mlir/unittests/Compiler/mqt-cc/nonlinear.qco.mlir b/mlir/unittests/Compiler/mqt-cc/nonlinear.qco.mlir new file mode 100644 index 0000000000..c23b03bc8d --- /dev/null +++ b/mlir/unittests/Compiler/mqt-cc/nonlinear.qco.mlir @@ -0,0 +1,14 @@ +// Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +// Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +// All rights reserved. +// +// SPDX-License-Identifier: MIT +// +// Licensed under the MIT License + +module { + func.func @main() { + %qubit = qco.alloc : !qco.qubit + return + } +} diff --git a/mlir/unittests/Compiler/mqt-cc/verify_invalid_mlir.cmake b/mlir/unittests/Compiler/mqt-cc/verify_invalid_mlir.cmake index ba0c8633fc..06e577ae1c 100644 --- a/mlir/unittests/Compiler/mqt-cc/verify_invalid_mlir.cmake +++ b/mlir/unittests/Compiler/mqt-cc/verify_invalid_mlir.cmake @@ -6,18 +6,20 @@ # # Licensed under the MIT License +function(require_failure description expected) + execute_process( + COMMAND ${ARGN} + RESULT_VARIABLE result + ERROR_VARIABLE error) + if(NOT result EQUAL 1 OR NOT error MATCHES "${expected}") + message(FATAL_ERROR "${description} did not fail as expected:\n${error}") + endif() +endfunction() + file(MAKE_DIRECTORY "${OUTPUT_DIR}") set(input_file "${OUTPUT_DIR}/invalid.mlir") file(WRITE "${input_file}" "module {\n") -execute_process( - COMMAND "${MQT_CC}" "${input_file}" - RESULT_VARIABLE result - ERROR_VARIABLE error) - -if(NOT result EQUAL 1) - message(FATAL_ERROR "mqt-cc returned ${result} for invalid MLIR:\n${error}") -endif() -if(NOT error MATCHES "expected operation name") - message(FATAL_ERROR "mqt-cc did not report the invalid MLIR:\n${error}") -endif() +require_failure("invalid MLIR" "expected operation name" "${MQT_CC}" "${input_file}") +require_failure("nonlinear QCO" "expected linear QCO value to have exactly one use" "${MQT_CC}" + "${NONLINEAR_QCO_INPUT}" "--emit=qco") diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index e3c88e8c14..1fc5a8ca17 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -283,6 +283,72 @@ TEST(CompilerProgramOwnershipTest, ValidatesAndOwnsExistingQCModules) { EXPECT_FALSE( QCProgram::fromModule(otherContext, std::move(mismatchedModule))); } +TEST(CompilerProgramOwnershipTest, EnforcesQCOLinearityAtPublicBoundaries) { + DialectRegistry registry; + registry.insert(); + auto context = std::make_shared(registry); + context->loadAllAvailableDialects(); + + constexpr llvm::StringLiteral validSource = R"mlir(module { + func.func @main() { + %qubit = qco.alloc : !qco.qubit + qco.sink %qubit : !qco.qubit + return + } + })mlir"; + constexpr llvm::StringLiteral nonlinearSource = R"mlir(module { + func.func @main() { + %qubit = qco.alloc : !qco.qubit + return + } + })mlir"; + + auto moduleOp = parseSourceString(validSource, context.get()); + ASSERT_TRUE(moduleOp); + auto program = QCOProgram::fromModule(context, std::move(moduleOp)); + ASSERT_TRUE(program); + + EXPECT_FALSE(QCOProgram::fromModule(context, {})); + + auto contextlessModule = + parseSourceString(validSource, context.get()); + ASSERT_TRUE(contextlessModule); + EXPECT_FALSE(QCOProgram::fromModule({}, std::move(contextlessModule))); + + auto mismatchedModule = + parseSourceString(validSource, context.get()); + ASSERT_TRUE(mismatchedModule); + auto otherContext = std::make_shared(registry); + EXPECT_FALSE( + QCOProgram::fromModule(otherContext, std::move(mismatchedModule))); + + auto invalidModule = parseSourceString(validSource, context.get()); + ASSERT_TRUE(invalidModule); + auto main = invalidModule->lookupSymbol("main"); + ASSERT_TRUE(main); + main.getBody().front().getTerminator()->erase(); + EXPECT_FALSE(QCOProgram::fromModule(context, std::move(invalidModule))); + + auto nonlinearModule = + parseSourceString(nonlinearSource, context.get()); + ASSERT_TRUE(nonlinearModule); + EXPECT_FALSE(QCOProgram::fromModule(context, std::move(nonlinearModule))); + + auto transformInput = program->copy(); + auto pipelineInput = program->copy(); + const auto eraseSink = [](QCOProgram& input) { + Operation* sink = nullptr; + input.module().walk([&sink](SinkOp op) { sink = op.getOperation(); }); + ASSERT_NE(sink, nullptr); + sink->erase(); + }; + eraseSink(transformInput); + eraseSink(pipelineInput); + + EXPECT_FALSE(transformInput.cleanup()); + EXPECT_FALSE(runDefaultPipeline(CompilerInput{std::move(pipelineInput)}, + ProgramFormat::QCO)); +} /** @brief Raw QCO stops before the registered default optimization pipeline. */ TEST_F(CompilerPipelineTest, RawAndOptimizedQCOAreDistinctCheckpoints) { @@ -883,6 +949,55 @@ h q; EXPECT_FALSE(QCOProgram::fromMLIRString(mlir)); } +/** + * @brief 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() { + %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> + qco.sink %qubit : !qco.qubit + qtensor.dealloc %rest : tensor<1x!qco.qubit> + return + } + })mlir"; + const std::string unusedResult = R"mlir(module { + func.func @main() { + %qubit = qco.alloc : !qco.qubit + return + } + })mlir"; + const std::string reusedBlockArgument = R"mlir(module { + func.func @main(%reg: tensor<1x!qco.qubit>) { + qtensor.dealloc %reg : tensor<1x!qco.qubit> + qtensor.dealloc %reg : tensor<1x!qco.qubit> + %qubit = qco.alloc : !qco.qubit + qco.sink %qubit : !qco.qubit + return + } + })mlir"; + const std::string unusedVectorArgument = R"mlir(module { + func.func @main(%qubits: vector<2x!qco.qubit>) { + %qubit = qco.alloc : !qco.qubit + qco.sink %qubit : !qco.qubit + return + } + })mlir"; + + EXPECT_TRUE(QCOProgram::fromMLIRString(valid)); + EXPECT_FALSE(QCOProgram::fromMLIRString(unusedResult)); + EXPECT_FALSE(QCOProgram::fromMLIRString(unusedVectorArgument)); + + const auto path = std::filesystem::path(testing::TempDir()) / + "nonlinear_block_argument.qco.mlir"; + std::ofstream(path) << reusedBlockArgument; + EXPECT_FALSE(QCOProgram::fromMLIRFile(path)); +} + /** * @brief Test: typed programs emit OpenQASM directly and through the pipeline. */