From 33dbc843e589d9e9166308084e825c9f8b2ff89d Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 4 Sep 2026 21:58:57 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=B7=20Lower=20reusable=20functions=20w?= =?UTF-8?q?ith=20MLIR=20passes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Require a public, defined func.func program entry so symbol DCE can prune unreachable private helpers during QC export and QC/QCO cleanup. Let structured exporters omit unreachable gate declarations. Use MLIR dead-value removal in explicit cleanup so unused unitary parameters and qubits are removed with their matching call operands. Inline reusable calls once at the target-compilation boundary before mapping, synthesis, and conformance checks. Avoid redundant inlining in typed and CLI QIR paths. Assisted-by: GPT-5.6 via Codex --- .agent/plans/reusable-function-lowering.md | 58 ++++++++++++++++ CHANGELOG.md | 3 +- bindings/mlir/qiskit/QiskitExport.cpp | 7 -- docs/mlir/target_compilation.md | 11 +-- .../include/mlir/Compiler/TargetCompilation.h | 15 ++--- .../include/mlir/Dialect/MQT/IR/MQTDialect.td | 4 +- mlir/include/mlir/Support/Passes.h | 6 +- mlir/lib/Compiler/Pipeline.cpp | 3 +- mlir/lib/Compiler/TargetCompilation.cpp | 1 + mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 9 ++- .../QC/Translation/TranslateQCToOpenQASM3.cpp | 54 +++++++++------ mlir/lib/Support/Passes.cpp | 4 ++ mlir/tools/mqt-cc/mqt-cc.cpp | 5 +- .../Compiler/test_compiler_pipeline.cpp | 56 +++++++++++++++- mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 5 ++ mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp | 57 ++++++++++++++-- .../Translation/test_openqasm3_emission.cpp | 48 +++++++++++-- mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp | 67 ++++++++++++++++--- test/python/test_mlir_qiskit_translation.py | 11 +-- 19 files changed, 340 insertions(+), 84 deletions(-) create mode 100644 .agent/plans/reusable-function-lowering.md diff --git a/.agent/plans/reusable-function-lowering.md b/.agent/plans/reusable-function-lowering.md new file mode 100644 index 0000000000..5ca3fa1587 --- /dev/null +++ b/.agent/plans/reusable-function-lowering.md @@ -0,0 +1,58 @@ +# Lower reusable functions with standard MLIR passes + +Status: complete. + +## Goal + +Keep reusable QC and QCO functions visible for structured format export, but +inline calls before target-specific decomposition, mapping, synthesis, and +conformance checks. Cleanup and export should remove unreachable private +functions, and cleanup may shrink live unitary signatures. + +## Design + +- Require `mqt.entry_point` on one public, defined, module-level `func.func` so + MLIR symbol DCE has a durable program root. +- Use MLIR's standard inliner at the target-compilation boundary. The typed API + and `mqt-cc` skip their QIR-preparation inliner when a target pipeline owns + inlining. +- Run symbol DCE after local canonicalization in QC export and QCO cleanup so + every export can omit unused gate declarations. +- Use `RemoveDeadValues` in QC and QCO cleanup. Unused unitary parameters and + qubits need not remain in live function and call signatures. +- Keep `WireIterator` unchanged. `qco.call` already implements + `UnitaryOpInterface` with positional input/output correspondence, while + generic `func.call` remains an intentional wire boundary. +- Do not add custom specialization, tensor promotion, auxiliary-qubit hoisting, + call-graph infrastructure, or IPO passes without a measured workload. + +## Implementation + +`populateTargetCompilationPipeline` starts with the standard MLIR inliner. QC +export and both cleanup pipelines remove unreachable symbols; explicit QC and +QCO cleanup additionally remove dead values. The typed compiler pipeline and CLI +avoid duplicate inlining for targeted QIR output. + +Tests cover the entry-point visibility contract, unreachable gate removal, +unitary-signature shrinking, native jeff calls through target compilation, and +target compilation from a caller-owned context. + +## Validation + +Run from the repository root: + + cmake --preset release + cmake --build --preset release + ctest --preset release + uvx nox -s cpp-lint -- 6328d48c77370cc99e089ce38e57bcd9053e48c6 + uvx nox -s lint + +The release build and all 3,918 registered tests pass, with one expected skip. +All 294 Qiskit translation tests, repository lint, and all-file C++ lint pass. +Hosted CI is separate evidence and must run on the published commit. + +## Outcome + +The implementation uses standard MLIR infrastructure and adds no custom pass or +framework. Exporters omit unreachable functions, and standard dead-value removal +owns cleanup-time unitary-signature shrinking. diff --git a/CHANGELOG.md b/CHANGELOG.md index 63df8dfd27..c85b8b3f07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ releases may include breaking changes. [#1927], [#1935], [#1936], [#1938], [#1975], [#1976], [#2006], [#2014], [#2015], [#2017], [#2026], [#2028], [#2054], [#2058], [#2125], [#2136], [#2149], [#2150], [#2158], [#2194], [#2210], [#2211], [#2215], [#2218], - [#2220], [#2323], [#2336]) ([**@burgholzer**], [**@denialhaag**], + [#2220], [#2323], [#2336], [#2344]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**], [**@J4MMlE**]) - ✨ Add a library for typed structured quantum benchmarks with versioned @@ -932,6 +932,7 @@ for previous changelogs._ [#2368]: https://github.com/munich-quantum-toolkit/core/pull/2368 [#2358]: https://github.com/munich-quantum-toolkit/core/pull/2358 [#2349]: https://github.com/munich-quantum-toolkit/core/pull/2349 +[#2344]: https://github.com/munich-quantum-toolkit/core/pull/2344 [#2342]: https://github.com/munich-quantum-toolkit/core/pull/2342 [#2340]: https://github.com/munich-quantum-toolkit/core/pull/2340 [#2339]: https://github.com/munich-quantum-toolkit/core/pull/2339 diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index b8508c14c8..c92fa64ec4 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -2816,13 +2816,6 @@ collectGateFunctions(mlir::ModuleOp moduleOp, mlir::func::FuncOp entryPoint) { ordered.push_back(function); } } - for (auto function : moduleOp.getOps()) { - if (!gateCallDepths.contains(graph.lookupNode(&function.getBody()))) { - throw std::runtime_error("Qiskit circuit export cannot preserve " - "function '" + - function.getName().str() + "'"); - } - } return ordered; } diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index c50244c583..cce96e02c8 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -2,9 +2,10 @@ An MLIR {code}`mlir::CompilerTarget` is an immutable snapshot of a circuit-model device. It contains the device sites, topology, native operations, and available -calibration and ordered-applicability data. Compilation decomposes supported -multi-qubit operations, optimizes and maps the program, synthesizes native -gates, and verifies that the result conforms to the target. +calibration and ordered-applicability data. Compilation inlines reusable +functions, decomposes supported multi-qubit operations, optimizes and maps the +program, synthesizes native gates, and verifies that the result conforms to the +target. The snapshot is independent of its originating QDMI session. It can therefore be stored, copied cheaply, and reused for multiple compilations. @@ -95,7 +96,9 @@ benchmarking, the C++ API exposes separate factories for pre-routing optimization, deterministic placement, topology-aware mapping, native synthesis, and conformance verification. Target compilation uses compact placement on all-to-all targets and the mapper only when the target has an explicit coupling -graph. +graph. The high-level program API registers the required inliner extensions; +callers that populate the low-level target pipeline directly must register +inliner extensions for every callable dialect in their context. Target compilation preserves quantum operations even when their final qubit values are not measured or returned. This supports measurement-free programs, diff --git a/mlir/include/mlir/Compiler/TargetCompilation.h b/mlir/include/mlir/Compiler/TargetCompilation.h index 4a03c81fb2..10d4cd6233 100644 --- a/mlir/include/mlir/Compiler/TargetCompilation.h +++ b/mlir/include/mlir/Compiler/TargetCompilation.h @@ -15,14 +15,13 @@ namespace mlir { class CompilerTarget; class OpPassManager; -/** - * @brief Populate the canonical compiler-target pipeline. - * - * @details Decomposes supported multi-controlled gates, performs - * target-independent optimization, maps to the target topology, synthesizes - * native operations, performs a final local cleanup, and verifies target - * conformance. - */ +/// Populate the canonical compiler-target pipeline. +/// +/// Inlines reusable functions, decomposes supported multi-controlled gates, +/// performs target-independent optimization, maps to the target topology, +/// synthesizes native operations, performs a final local cleanup, and verifies +/// target conformance. The context that runs this low-level pipeline must +/// register inliner extensions for its callable dialects. void populateTargetCompilationPipeline(OpPassManager& pm, const CompilerTarget& target); diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td index 03083a2aa6..f5e607129c 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -32,8 +32,8 @@ def MQTDialect : Dialect { `mqt.register_name` records the source-level name of a quantum or classical register allocation. Input and register names share one function-wide namespace. - `mqt.entry_point` marks the single defined program entry function in a - module. + `mqt.entry_point` marks the single public, defined `func.func` program entry + in a module. `mqt.unitary` marks a private function that defines a unitary operation. `#mqt.compilation_target` records compiler-target facts as typed IR. }]; diff --git a/mlir/include/mlir/Support/Passes.h b/mlir/include/mlir/Support/Passes.h index 1bdededd32..b834da0065 100644 --- a/mlir/include/mlir/Support/Passes.h +++ b/mlir/include/mlir/Support/Passes.h @@ -52,8 +52,7 @@ runPassPipeline(mlir::ModuleOp moduleOp, mlir::StringRef pipeline, /// Populate a QC-oriented cleanup pipeline on the given pass manager. /// -/// Adds generic cleanup and QC qubit-register shrinking while -/// preserving live function signatures. +/// Adds generic cleanup, QC qubit-register shrinking, and dead-value removal. void populateQCCleanupPipeline(mlir::OpPassManager& pm); /// Run QC cleanup that preserves defined values on every syntactic loop edge. @@ -63,8 +62,7 @@ void populateQCExportPipeline(mlir::OpPassManager& pm); /// Populate a QCO-oriented cleanup pipeline on the given pass manager. /// -/// Adds generic cleanup and qtensor shrink-to-fit while -/// preserving live function signatures. +/// Adds generic cleanup, qtensor shrink-to-fit, and dead-value removal. void populateQCOCleanupPipeline(mlir::OpPassManager& pm); /// Populate a QIR-oriented cleanup pipeline on the given pass manager. diff --git a/mlir/lib/Compiler/Pipeline.cpp b/mlir/lib/Compiler/Pipeline.cpp index 53da9b3755..a3ae3a98e7 100644 --- a/mlir/lib/Compiler/Pipeline.cpp +++ b/mlir/lib/Compiler/Pipeline.cpp @@ -482,7 +482,8 @@ runDefaultPipeline(CompilerInput&& program, ProgramFormat output, return CompilerProgram(std::move(*qco)); } - if ((output == ProgramFormat::QIRBase || + if (target == nullptr && + (output == ProgramFormat::QIRBase || output == ProgramFormat::QIRAdaptive) && failed(runQCOTransformPasses( qco->module(), diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index e30aa03ed2..1895da149d 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -22,6 +22,7 @@ namespace mlir { void populateTargetCompilationPipeline(OpPassManager& pm, const CompilerTarget& target) { + pm.addPass(createInlinerPass()); populateQCOCleanupPipeline(pm); pm.addPass(qco::createDecomposeMultiControlled(target)); populateDefaultQCOOptimizationPipeline(pm); diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 48bd9a3ee5..804575a7df 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -293,14 +293,13 @@ verifyEntryPoint(Operation* operation, const NamedAttribute attribute) { << "' must be a unit attribute"; } - auto function = dyn_cast(operation); + auto function = dyn_cast(operation); auto moduleOp = operation->getParentOfType(); - if (!function || !moduleOp || - operation->getParentOp() != moduleOp.getOperation() || - function.getFunctionBody().empty()) { + if (!function || !function.isPublic() || function.isExternal() || !moduleOp || + operation->getParentOp() != moduleOp.getOperation()) { return operation->emitError() << "attribute '" << attribute.getName().getValue() - << "' requires a defined module-level function"; + << "' requires a public, defined module-level function"; } for (Operation& candidate : moduleOp.getBody()->getOperations()) { diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp index b6ec74f620..623134be4a 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp @@ -261,12 +261,12 @@ class OpenQASMEmitter { } [[nodiscard]] LogicalResult orderGateFunctions() { - if (gateNames_.empty()) { - return success(); - } const CallGraph callGraph(moduleOp); - for (auto component = llvm::scc_begin(&callGraph); !component.isAtEnd(); - ++component) { + for (auto component = + llvm::scc_iterator>:: + begin(callGraph.lookupNode(&function.getBody())); + !component.isAtEnd(); ++component) { auto* node = component->front(); if (node->isExternal()) { continue; @@ -275,8 +275,12 @@ class OpenQASMEmitter { if (component.hasCycle()) { return fail(current, "recursive gate function calls are not supported"); } - if (gateNames_.contains(current)) { - gateFunctions_.push_back(cast(current)); + if (current != function) { + auto gate = dyn_cast(current); + if (!gate) { + return fail(current, "gate calls must target func.func definitions"); + } + gateFunctions_.push_back(gate); } } return success(); @@ -316,10 +320,10 @@ class OpenQASMEmitter { return failure(); } - for (auto current : functions) { - if (current == function) { - continue; - } + if (failed(orderGateFunctions())) { + return failure(); + } + for (auto current : gateFunctions_) { if (!current.isPrivate() || current.isExternal() || !current.getBody().hasOneBlock() || current.getNumResults() != 0) { return fail(current, "gate functions must be private, defined, " @@ -348,16 +352,22 @@ class OpenQASMEmitter { ? requested.str() : uniqueName("gate", nextHelper)); } - const auto walkResult = moduleOp.walk([&](Operation* operation) { - if (isa(operation) && - resolveGateCallee(operation) == nullptr) { - std::ignore = fail(operation, "call does not target an exportable gate " - "function"); - return WalkResult::interrupt(); - } - return WalkResult::advance(); - }); - if (walkResult.wasInterrupted()) { + const auto hasInvalidCall = [&](func::FuncOp current) { + return current + .walk([&](Operation* operation) { + if (isa(operation) && + resolveGateCallee(operation) == nullptr) { + std::ignore = + fail(operation, + "call does not target an exportable gate function"); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }) + .wasInterrupted(); + }; + if (hasInvalidCall(function) || + llvm::any_of(gateFunctions_, hasInvalidCall)) { return failure(); } for (Operation& operation : moduleOp.getBody()->getOperations()) { @@ -365,7 +375,7 @@ class OpenQASMEmitter { return fail(&operation, "only functions may appear at module scope"); } } - return orderGateFunctions(); + return success(); } [[nodiscard]] LogicalResult collectProgramShape() { diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index fcfcac0749..76c78d8a33 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -128,10 +128,12 @@ void populateQCExportPipeline(OpPassManager& pm) { pm.addPass(mlir::mqt::createNormalizeGlobalPhases()); pm.addPass(createCSEPass()); pm.addPass(qc::createShrinkQubitRegistersPass()); + pm.addPass(createSymbolDCEPass()); } void populateQCCleanupPipeline(OpPassManager& pm) { populateQCExportPipeline(pm); + pm.addPass(createRemoveDeadValuesPass()); } void populateQCOCleanupPipeline(OpPassManager& pm) { @@ -140,6 +142,8 @@ void populateQCOCleanupPipeline(OpPassManager& pm) { pm.addPass(mlir::mqt::createNormalizeGlobalPhases()); pm.addPass(createCSEPass()); pm.addPass(qtensor::createShrinkQTensorToFitPass()); + pm.addPass(createSymbolDCEPass()); + pm.addPass(createRemoveDeadValuesPass()); } void populateQIRCleanupPipeline(OpPassManager& pm, bool useAdaptive) { diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 3e5d394e43..e2bf1127b0 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -539,8 +539,9 @@ static int runCompiler(int argc, char** argv) { *parsedOutputFormat != OutputFormat::QCImport && *parsedOutputFormat != OutputFormat::QCO; if (requiresPostQcoPasses && failed(runPasses([&](OpPassManager& pm) { - if (*parsedOutputFormat == OutputFormat::QIRBase || - *parsedOutputFormat == OutputFormat::QIRAdaptive) { + if (!compilerTarget && + (*parsedOutputFormat == OutputFormat::QIRBase || + *parsedOutputFormat == OutputFormat::QIRAdaptive)) { pm.addPass(createInlinerPass()); } if (compilerTarget) { diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 10cdc07572..25c2a3454a 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -1155,6 +1155,23 @@ bit[2] c = measure q; EXPECT_TRUE(adaptiveQIR); } +TEST_F(CompilerPipelineTest, TypedOpenQASMExportDropsUnusedGates) { + constexpr llvm::StringLiteral source = R"mlir(module { + func.func private @unused(%q: !qc.qubit) attributes {mqt.unitary} { + qc.x %q : !qc.qubit + return + } + func.func @main() attributes {mqt.entry_point} { return } + })mlir"; + auto program = QCProgram::fromMLIRString(source.str()); + ASSERT_TRUE(program); + + auto exported = program->toOpenQASM3(); + + ASSERT_TRUE(exported); + EXPECT_EQ(exported->source().find("gate unused"), std::string::npos); +} + TEST_F(CompilerPipelineTest, TypedOpenQASMExportReportsUnsupportedQC) { constexpr llvm::StringLiteral source = R"mlir(module { func.func @main(%value: i64) { @@ -1438,6 +1455,9 @@ TEST_F(CompilerPipelineTest, JeffBinaryRoundTripPreservesReusableFunctions) { ASSERT_TRUE(output); EXPECT_TRUE(std::get(*output).llvmIR()); } + auto targeted = restored->copy(); + ASSERT_TRUE(targeted.compileForTarget(makeSparseUCZTarget(true))); + EXPECT_EQ(llvm::range_size(targeted.module().getOps()), 1); auto qc = std::move(*restored).intoQC(); ASSERT_TRUE(qc); EXPECT_TRUE(succeeded(verify(qc->module()))); @@ -1635,6 +1655,39 @@ TEST_F(CompilerPipelineTest, QCOProgramCompilesForTarget) { EXPECT_FALSE(unsupportedQCO->compileForTarget(makeSparseUCZTarget(false))); } +TEST_F(CompilerPipelineTest, TargetCompilationInlinesReusableFunctions) { + constexpr llvm::StringLiteral source = R"mlir(module { + func.func private @flip(%q: !qco.qubit) -> !qco.qubit + attributes {mqt.unitary} { + %out = qco.x %q : !qco.qubit -> !qco.qubit + return %out : !qco.qubit + } + func.func @main() attributes {mqt.entry_point} { + %q = qco.alloc : !qco.qubit + %out = qco.call @flip(%q) : (!qco.qubit) -> !qco.qubit + qco.sink %out : !qco.qubit + return + } + })mlir"; + + DialectRegistry registry; + registry.insert(); + auto ownedContext = std::make_shared(registry); + ownedContext->loadAllAvailableDialects(); + auto moduleOp = parseSourceString(source, ownedContext.get()); + ASSERT_TRUE(moduleOp); + auto program = QCOProgram::fromModule(ownedContext, std::move(moduleOp)); + ASSERT_TRUE(program); + + ASSERT_TRUE(program->compileForTarget(makeSparseUCZTarget(false))); + EXPECT_FALSE(program->module().lookupSymbol("flip")); + size_t calls = 0; + program->module().walk([&](qco::CallOp) { ++calls; }); + EXPECT_EQ(calls, 0U); + EXPECT_TRUE(verify(program->module()).succeeded()); +} + // Test that target compilation leaves dead-value cleanup at a fixed point. TEST_F(CompilerPipelineTest, TargetCompilationLeavesDeadValueCleanupAtFixedPoint) { @@ -1684,8 +1737,7 @@ TEST_F(CompilerPipelineTest, ASSERT_TRUE(program->compileForTarget(target)); const std::string before = program->str(); - EXPECT_NE(before.find("func.func private @forward"), std::string::npos); - EXPECT_NE(before.find("call @forward"), std::string::npos); + EXPECT_EQ(before.find("func.func private @forward"), std::string::npos); EXPECT_NE(before.find("qco.if"), std::string::npos); EXPECT_NE(before.find("qco.u"), std::string::npos); EXPECT_NE(before.find("qco.ctrl"), std::string::npos); diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index 570887e28a..ba42ba98b8 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -351,6 +351,11 @@ TEST_F(MQTIRTest, RejectsInvalidEntryPoints) { func.func private @main() attributes {mqt.entry_point} } )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + func.func private @main() attributes {mqt.entry_point} { return } + } + )mlir")); EXPECT_FALSE(parse(R"mlir( module { func.func @first() attributes {mqt.entry_point} { return } diff --git a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp index 0a698a5814..7fffaeb22e 100644 --- a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp +++ b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp @@ -89,8 +89,8 @@ class QCTest : public testing::TestWithParam { void QCTest::SetUp() { // Register all necessary dialects DialectRegistry registry; - registry.insert(); + registry.insert(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -542,11 +542,6 @@ TEST_F(QCTest, UnitaryFunctionMarkerRequiresFunctionReturn) { } TEST_F(QCTest, UnitaryVerifierRejectsInvalidFunctionAndCallContracts) { - DialectRegistry registry; - registry.insert(); - context->appendDialectRegistry(registry); - context->getOrLoadDialect(); - constexpr std::array invalidPrograms{ R"mlir(module { func.func private @bad(%q: !qc.qubit) @@ -625,6 +620,54 @@ TEST_F(QCTest, UnitaryVerifierRejectsInvalidFunctionAndCallContracts) { EXPECT_TRUE(failed(call.verifySymbolUses(symbols))); } +TEST_F(QCTest, CleanupPrunesUnitaryFunctionsAndSignatures) { + auto moduleOp = parseSourceString(R"mlir(module { + func.func private @used(%theta: f64, %unusedTheta: f64, + %q: !qc.qubit, %unusedQubit: !qc.qubit) + attributes {mqt.unitary} { + qc.rz(%theta) %q : !qc.qubit + return + } + func.func private @unused(%q: !qc.qubit) attributes {mqt.unitary} { + qc.h %q : !qc.qubit + return + } + func.func private @conditional(%q: !qc.qubit) attributes {mqt.unitary} { + qc.z %q : !qc.qubit + return + } + func.func @main() attributes {mqt.entry_point} { + %theta = arith.constant 1.0 : f64 + %unusedTheta = arith.constant 2.0 : f64 + %q = qc.alloc : !qc.qubit + %unusedQubit = qc.alloc : !qc.qubit + %false = arith.constant false + scf.if %false { + qc.call @conditional(%q) : !qc.qubit + } + qc.call @used(%theta, %unusedTheta, %q, %unusedQubit) + : f64, f64, !qc.qubit, !qc.qubit + qc.dealloc %q : !qc.qubit + qc.dealloc %unusedQubit : !qc.qubit + return + } + })mlir", + context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(runQCCleanupPipeline(*moduleOp))); + EXPECT_TRUE(succeeded(verify(*moduleOp))); + auto used = moduleOp->lookupSymbol("used"); + ASSERT_TRUE(used); + EXPECT_EQ(used.getNumArguments(), 2); + auto call = + *mlir::mqt::getEntryPoint(*moduleOp).getBody().getOps().begin(); + EXPECT_EQ(call.getNumOperands(), 2); + EXPECT_FALSE(moduleOp->lookupSymbol("unused")); + EXPECT_FALSE(moduleOp->lookupSymbol("conditional")); + EXPECT_TRUE(mlir::mqt::getEntryPoint(*moduleOp)); +} + TEST_F(QCTest, DirectSingleQubitPowBuilder) { QCProgramBuilder builder(context.get()); builder.initialize(); diff --git a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp index ea1fd439cf..4338dd1a47 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -891,7 +891,11 @@ TEST(OpenQASM3EmissionTest, OrdersLongReverseDeclaredGateGraph) { } stream << "return\n}\n"; } - stream << "func.func @entry() attributes {mqt.entry_point} { return }\n}\n"; + stream << "func.func @entry() attributes {mqt.entry_point} {\n" + "%qubit = qc.alloc : !qc.qubit\n" + "func.call @gate0(%qubit) : (!qc.qubit) -> ()\n" + "qc.dealloc %qubit : !qc.qubit\n" + "return\n}\n}\n"; stream.flush(); DialectRegistry registry = emissionDialects(); MLIRContext context(registry); @@ -911,7 +915,26 @@ TEST(OpenQASM3EmissionTest, OrdersLongReverseDeclaredGateGraph) { 101); size_t calls = 0; roundTripped->walk([&](qc::CallOp) { ++calls; }); - EXPECT_EQ(calls, 99); + EXPECT_EQ(calls, 100); +} + +TEST(OpenQASM3EmissionTest, DropsUnreachableGateFunctions) { + constexpr llvm::StringLiteral source = R"mlir(module { + func.func private @unused(%qubit: !qc.qubit) attributes {mqt.unitary} { + qc.x %qubit : !qc.qubit + return + } + func.func @entry() attributes {mqt.entry_point} { return } + })mlir"; + DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + EXPECT_EQ(emitted->find("gate unused"), std::string::npos) << *emitted; } TEST(OpenQASM3EmissionTest, RejectsInvalidGateFunctions) { @@ -921,14 +944,24 @@ TEST(OpenQASM3EmissionTest, RejectsInvalidGateFunctions) { qc.reset %qubit : !qc.qubit return } - func.func @entry() attributes {mqt.entry_point} { return } + func.func @entry() attributes {mqt.entry_point} { + %qubit = qc.alloc : !qc.qubit + func.call @resetter(%qubit) : (!qc.qubit) -> () + qc.dealloc %qubit : !qc.qubit + return + } })mlir"}, llvm::StringLiteral{R"mlir(module { func.func private @left(%qubit: !qc.qubit) { func.call @right(%qubit) : (!qc.qubit) -> () return } - func.func @entry() attributes {mqt.entry_point} { return } + func.func @entry() attributes {mqt.entry_point} { + %qubit = qc.alloc : !qc.qubit + func.call @left(%qubit) : (!qc.qubit) -> () + qc.dealloc %qubit : !qc.qubit + return + } func.func private @right(%qubit: !qc.qubit) { func.call @left(%qubit) : (!qc.qubit) -> () return @@ -941,7 +974,12 @@ TEST(OpenQASM3EmissionTest, RejectsInvalidGateFunctions) { qc.rx(%angle) %qubit : !qc.qubit return } - func.func @entry() attributes {mqt.entry_point} { return } + func.func @entry() attributes {mqt.entry_point} { + %qubit = qc.alloc : !qc.qubit + func.call @invalid(%qubit) : (!qc.qubit) -> () + qc.dealloc %qubit : !qc.qubit + return + } })mlir"}, llvm::StringLiteral{R"mlir(module { func.func private @pair(%left: !qc.qubit, %right: !qc.qubit) { diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp index b126148db7..b7cd558fc7 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp @@ -94,9 +94,9 @@ class QCOTest : public testing::TestWithParam { void QCOTest::SetUp() { // Register all necessary dialects DialectRegistry registry; - registry.insert(); + registry.insert(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -382,9 +382,12 @@ TEST_F(QCOTest, UnitaryVerifierDiagnosesMalformedCalls) { ParserConfig config(context.get(), false); auto moduleOp = parseSourceString(R"mlir( module { - func.func private @malformed(%q: !qco.qubit) -> !qco.qubit + func.func private @callee(%q: !qco.qubit) -> !qco.qubit attributes {mqt.unitary} { - %left, %right = qco.call @malformed(%q) + return %q : !qco.qubit + } + func.func private @caller(%q: !qco.qubit) -> !qco.qubit { + %left, %right = qco.call @callee(%q) : (!qco.qubit) -> (!qco.qubit, !qco.qubit) return %left : !qco.qubit } @@ -406,11 +409,6 @@ TEST_F(QCOTest, UnitaryVerifierDiagnosesMalformedCalls) { } TEST_F(QCOTest, UnitaryVerifierRejectsInvalidFunctionAndCallContracts) { - DialectRegistry registry; - registry.insert(); - context->appendDialectRegistry(registry); - context->getOrLoadDialect(); - constexpr std::array invalidPrograms{ R"mlir(module { func.func private @bad() attributes {mqt.unitary} { return } @@ -503,6 +501,55 @@ TEST_F(QCOTest, UnitaryVerifierRejectsInvalidFunctionAndCallContracts) { EXPECT_TRUE(failed(call.verifySymbolUses(symbols))); } +TEST_F(QCOTest, CleanupPrunesUnitaryFunctionsAndSignatures) { + auto moduleOp = parseSourceString(R"mlir(module { + func.func private @used(%unusedTheta: f64, %q: !qco.qubit) -> !qco.qubit + attributes {mqt.unitary} { + %out = qco.x %q : !qco.qubit -> !qco.qubit + return %out : !qco.qubit + } + func.func private @unused(%q: !qco.qubit) -> !qco.qubit + attributes {mqt.unitary} { + %out = qco.h %q : !qco.qubit -> !qco.qubit + return %out : !qco.qubit + } + func.func private @conditional(%q: !qco.qubit) -> !qco.qubit + attributes {mqt.unitary} { + %out = qco.z %q : !qco.qubit -> !qco.qubit + return %out : !qco.qubit + } + func.func @main() attributes {mqt.entry_point} { + %false = arith.constant false + scf.if %false { + %branchQ = qco.alloc : !qco.qubit + %branchOut = qco.call @conditional(%branchQ) + : (!qco.qubit) -> !qco.qubit + qco.sink %branchOut : !qco.qubit + } + %unusedTheta = arith.constant 2.0 : f64 + %q = qco.alloc : !qco.qubit + %out = qco.call @used(%unusedTheta, %q) + : (f64, !qco.qubit) -> !qco.qubit + qco.sink %out : !qco.qubit + return + } + })mlir", + context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(runQCOCleanupPipeline(*moduleOp))); + EXPECT_TRUE(succeeded(verify(*moduleOp))); + auto used = moduleOp->lookupSymbol("used"); + ASSERT_TRUE(used); + EXPECT_EQ(used.getNumArguments(), 1); + auto call = + *mlir::mqt::getEntryPoint(*moduleOp).getBody().getOps().begin(); + EXPECT_EQ(call.getNumOperands(), 1); + EXPECT_FALSE(moduleOp->lookupSymbol("unused")); + EXPECT_FALSE(moduleOp->lookupSymbol("conditional")); + EXPECT_TRUE(mlir::mqt::getEntryPoint(*moduleOp)); +} + TEST_F(QCOTest, TraceQubitArgumentRejectsUnsupportedSources) { ParserConfig config(context.get(), false); auto moduleOp = parseSourceString(R"mlir(module { diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 596fb7bb68..7f309e8907 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -1354,8 +1354,8 @@ def test_custom_gate_export_rejects_duplicate_qargs() -> None: program.to_qiskit() -def test_custom_gate_export_rejects_unreferenced_functions() -> None: - """Reject helper definitions that a Qiskit circuit cannot retain.""" +def test_custom_gate_export_drops_unreferenced_functions() -> None: + """Drop helper definitions that the exported circuit does not use.""" program = QCProgram.from_mlir_str( """module { func.func private @unused(%q: !qc.qubit) attributes {mqt.unitary} { @@ -1369,8 +1369,11 @@ def test_custom_gate_export_rejects_unreferenced_functions() -> None: """ ) - with pytest.raises(RuntimeError, match="cannot preserve function 'unused'"): - program.to_qiskit() + source_ir = program.ir + restored = program.to_qiskit() + + assert not restored.data + assert program.ir == source_ir def test_generic_instruction_with_clbits_remains_flattened() -> None: