diff --git a/.agent/plans/qir-reusable-functions.md b/.agent/plans/qir-reusable-functions.md new file mode 100644 index 0000000000..58004a3e0d --- /dev/null +++ b/.agent/plans/qir-reusable-functions.md @@ -0,0 +1,116 @@ +# Flatten reusable quantum functions for QIR output + +This ExecPlan follows `.agent/PLANS.md`. Keep its progress, discoveries, +decisions, and outcome current. Run commands from the repository root. + +## Purpose + +QIR is a flat output format in the compiler: its lowering does not preserve QC +or QCO function calls. Make Base and Adaptive QIR generation accept reusable +unitary helpers without changing the compact QC/QCO representation used by other +outputs. Calls nested in quantum modifiers must retain their phase and modifier +semantics. + +## Progress + +- [x] (2026-09-04) Rebase onto current `main` and the finalized jeff function + layer, adapting the implementation to the split Programs/Pipeline + libraries. +- [x] (2026-09-04) Reuse MLIR's inliner and existing phase, modifier, and + canonicalization passes for flat QIR preparation. +- [x] (2026-09-04) Register Func and LLVM inliner extensions for compiler-owned + and caller-owned contexts. +- [x] (2026-09-04) Keep early QCO inlining exclusive to coordinated QIR output. +- [x] (2026-09-04) Preserve direct, CLI, jeff-imported, and both-profile tests. +- [x] (2026-09-04) Apply the independent QIR/MLIR specialist review: remove a + duplicate textual-inliner check and defer an inert OpenQASM test switch. +- [x] (2026-09-04) Build the compiler and CLI and pass all 161 compiler tests. +- [ ] Run repository lint on the final stack. + +## Discoveries + +Plain reusable calls are illegal at the QC-to-QIR boundary. A helper containing +a global phase under an integral power needs more than inlining: phase +normalization extracts and scales the phase, modifier unrolling distributes the +supported operations, and canonicalization folds the remaining one-operation +modifier. + +A same-wire composite helper under a power must be exposed while it is still QCO +so existing QCO synthesis can reduce it safely. Distributing a power over a +general noncommuting sequence would be incorrect. Early QCO inlining therefore +belongs only to QIR-bound coordinated pipelines, before target compilation or +the default QCO optimization pipeline. + +MLIR's stock inliner depends on promised Func and LLVM interfaces. Compiler +contexts and contexts adopted by typed programs must install those extensions; +otherwise the public textual `inline` pipeline can abort before examining QC or +QCO operations. + +Current `main` separates context/parsing code in `Programs.cpp` from conversion +and coordinated compilation in `Pipeline.cpp`. Inliner-extension registration +stays with context ownership; QIR preparation and output routing stay in the +pipeline library. + +## Decisions + +Use `createInlinerPass`, `NormalizeGlobalPhases`, `UnrollModifiers`, and the +canonicalizer. Do not add a QIR-specific call lowering, custom call graph, or +new synthesis pass. + +Install standard inliner extensions in `createCompilerContext` and when a typed +program adopts a caller-owned context. This makes the public compiler program +contract independent of who constructed the context. + +Inline all QCO calls early only for Base or Adaptive QIR output. Preserve calls +for QCO, QC, OpenQASM, and jeff output. The direct `QCProgram::intoQIR` path +uses the common QC preparation pipeline immediately before profile lowering. + +Keep unsupported composite powers fail-closed. The compiler need not invent an +unsafe algebraic rewrite merely to accept a hypothetical program. + +## Scope + +`mlir/lib/Compiler/Programs.cpp` owns compiler-context extension registration. +`mlir/lib/Compiler/Pipeline.cpp` owns typed and coordinated QIR preparation. +`mlir/lib/Support/Passes.cpp` exposes the shared QC-to-QIR preparation sequence. +`mlir/tools/mqt-cc/mqt-cc.cpp` mirrors the same boundaries for its direct MLIR +pipeline. + +The compiler regression covers a phase-bearing unitary helper under nested power +and control, caller-owned textual inlining, early QCO exposure, and a +binary-restored jeff helper through both QIR profiles. The existing OpenQASM +program matrix continues to exercise flat QIR generation from production +frontend inputs. + +## Validation + +Configure and build the compiler and CLI: + + cmake --preset release + cmake --build --preset release --target mqt-core-mlir-unittests-compiler mqt-cc -j4 + +Run the reusable-function regressions and full compiler test binary: + + build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler --gtest_filter='CompilerPipelineTest.*QIR*:*OpenQASM*' + build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler + +The nested helper must produce Base and Adaptive QIR with one correctly scaled +relative-phase call. Caller-owned contexts must run the textual inliner without +an abort. Coordinated QIR output must expose QCO helpers before synthesis, while +non-QIR outputs retain them. + +Finish once on the final stack with: + + uvx nox -s cpp-lint + uvx nox -s lint + +Hosted CI is separate evidence and counts only after the final rewritten branch +is pushed. Builds, tests, and extension registration are repeatable. This plan +does not authorize unrelated remote changes. + +## Outcome + +Focused validation passes; final stack lint remains. The rebased implementation +uses native MLIR infrastructure and adds no call-specific QIR representation or +analysis. An independent specialist found the production implementation +idiomatic and removed only redundant or premature test scaffolding. diff --git a/CHANGELOG.md b/CHANGELOG.md index 147f0a38e1..63d6eb8d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,7 +55,7 @@ releases may include breaking changes. [#1446], [#1513], [#1521], [#1548], [#1567], [#1569], [#1570], [#1572], [#1580], [#1620], [#1624], [#1626], [#1648], [#1710], [#1751], [#1755], [#1787], [#1815], [#1823], [#1933], [#1978], [#1979], [#2007], [#2026], - [#2030], [#2066], [#2217]) ([**@burgholzer**], [**@denialhaag**], + [#2030], [#2066], [#2217], [#2340]) ([**@burgholzer**], [**@denialhaag**], [**@simon1hofmann**], [**@li-mingbao**], [**@DRovara**], [**@MatthiasReumann**]) - ✨ Add OpenQASM import and export to the MQT Compiler Collection, including @@ -928,6 +928,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 +[#2340]: https://github.com/munich-quantum-toolkit/core/pull/2340 [#2339]: https://github.com/munich-quantum-toolkit/core/pull/2339 [#2337]: https://github.com/munich-quantum-toolkit/core/pull/2337 [#2336]: https://github.com/munich-quantum-toolkit/core/pull/2336 diff --git a/mlir/include/mlir/Support/Passes.h b/mlir/include/mlir/Support/Passes.h index 5ca5126ce0..1bdededd32 100644 --- a/mlir/include/mlir/Support/Passes.h +++ b/mlir/include/mlir/Support/Passes.h @@ -32,6 +32,9 @@ void registerMQTCompilerPasses(); /// Populate the default QCO optimization pipeline. void populateDefaultQCOOptimizationPipeline(mlir::OpPassManager& pm); +/// Prepare reusable QC functions and modifiers for QIR conversion. +void populateQIRPreparationPipeline(mlir::OpPassManager& pm); + /// Populate the qubit reuse pipeline including its preparation passes. void populateQubitReusePipeline(mlir::OpPassManager& pm); diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 76656c6329..bd592f2b5b 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -61,9 +61,11 @@ add_mlir_library( MLIRCBitDialect MLIRControlFlowDialect MLIRFuncDialect + MLIRFuncInlinerExtension MLIRIR MLIRJeff MLIRLLVMDialect + MLIRLLVMIRTransforms MLIRLLVMToLLVMIRTranslation MLIRMathDialect MLIRMemRefDialect diff --git a/mlir/lib/Compiler/Pipeline.cpp b/mlir/lib/Compiler/Pipeline.cpp index c36466b3a3..53da9b3755 100644 --- a/mlir/lib/Compiler/Pipeline.cpp +++ b/mlir/lib/Compiler/Pipeline.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include @@ -122,7 +123,7 @@ std::optional QCProgram::intoQIR(QIRProfile profile) && { if (failed(runPasses( mod(), [profile](OpPassManager& pm) { - pm.addPass(mqt::createUnrollModifiers()); + populateQIRPreparationPipeline(pm); if (profile == QIRProfile::Adaptive) { pm.addPass(createQCToQIRAdaptive()); } else { @@ -481,6 +482,15 @@ runDefaultPipeline(CompilerInput&& program, ProgramFormat output, return CompilerProgram(std::move(*qco)); } + if ((output == ProgramFormat::QIRBase || + output == ProgramFormat::QIRAdaptive) && + failed(runQCOTransformPasses( + qco->module(), + [](OpPassManager& pm) { pm.addPass(createInlinerPass()); }, + "failed to inline QCO calls", enableTiming, enableStatistics))) { + return std::nullopt; + } + if (target != nullptr) { if (!qco->compileForTarget(*target, enableTiming, enableStatistics)) { return std::nullopt; diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index c78ad4f069..afd3f34294 100644 --- a/mlir/lib/Compiler/Programs.cpp +++ b/mlir/lib/Compiler/Programs.cpp @@ -29,8 +29,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -61,6 +63,13 @@ namespace mlir { +static void ensureInlinerExtensions(MLIRContext* context) { + DialectRegistry registry; + func::registerInlinerExtension(registry); + LLVM::registerInlinerInterface(registry); + context->appendDialectRegistry(registry); +} + std::shared_ptr createCompilerContext() { DialectRegistry registry; registry.insert createCompilerContext() { registerLLVMDialectTranslation(registry); auto context = std::make_shared(registry); + ensureInlinerExtensions(context.get()); context->loadAllAvailableDialects(); return context; } @@ -143,7 +153,11 @@ parseTypedProgram(Parse&& parse) { // Program //===----------------------------------------------------------------------===// -Program::Program(Storage storage) : storage_(std::move(storage)) {} +Program::Program(Storage storage) : storage_(std::move(storage)) { + if (storage_.context) { + ensureInlinerExtensions(storage_.context.get()); + } +} bool Program::isValid() const noexcept { return static_cast(storage_.mod); diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index 96a38b6076..fcfcac0749 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -79,6 +79,13 @@ void populateDefaultQCOOptimizationPipeline(OpPassManager& pm) { pm.addPass(qco::createMergeSingleQubitRotationGates()); } +void populateQIRPreparationPipeline(OpPassManager& pm) { + pm.addPass(createInlinerPass()); + pm.addPass(mqt::createNormalizeGlobalPhases()); + pm.addPass(mqt::createUnrollModifiers()); + pm.addPass(createCanonicalizerPass()); +} + void populateQubitReusePipeline(OpPassManager& pm) { pm.addPass(qco::createMeasurementLifting()); pm.addPass(qco::createReplaceClassicalControls()); diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index dca50a8b34..3e5d394e43 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -46,8 +46,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -63,6 +65,7 @@ #include #include #include +#include #include #include @@ -461,6 +464,8 @@ static int runCompiler(int argc, char** argv) { tensor::TensorDialect, jeff::JeffDialect>(); registerBuiltinDialectTranslation(registry); registerLLVMDialectTranslation(registry); + func::registerInlinerExtension(registry); + LLVM::registerInlinerInterface(registry); MLIRContext context(registry); context.loadAllAvailableDialects(); @@ -534,6 +539,10 @@ 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) { + pm.addPass(createInlinerPass()); + } if (compilerTarget) { populateTargetCompilationPipeline(pm, *compilerTarget); return success(); @@ -583,7 +592,7 @@ static int runCompiler(int argc, char** argv) { if (*parsedOutputFormat == OutputFormat::QIRBase && failed(runPasses([](OpPassManager& pm) { - pm.addPass(mqt::createUnrollModifiers()); + populateQIRPreparationPipeline(pm); pm.addPass(createQCToQIRBase()); populateQIRCleanupPipeline(pm, false); return success(); @@ -593,7 +602,7 @@ static int runCompiler(int argc, char** argv) { if (*parsedOutputFormat == OutputFormat::QIRAdaptive && failed(runPasses([](OpPassManager& pm) { - pm.addPass(mqt::createUnrollModifiers()); + populateQIRPreparationPipeline(pm); pm.addPass(createQCToQIRAdaptive()); populateQIRCleanupPipeline(pm, true); return success(); diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index dcf023e095..5352bb68fc 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -1208,6 +1210,123 @@ TEST_F(CompilerPipelineTest, TypedProgramsNormalizeGlobalPhases) { EXPECT_EQ(StringRef(textual->str()).count("qco.gphase"), 1); } +// Test: typed QC-to-QIR conversion expands reusable unitary functions. +TEST_F(CompilerPipelineTest, QCProgramPreparesNestedUnitaryCallsForQIR) { + constexpr llvm::StringLiteral source = R"mlir(module { + func.func private @phased_rotation(%q: !qc.qubit) attributes {mqt.unitary} { + %phase = arith.constant 0.25 : f64 + %theta = arith.constant 0.3 : f64 + %phi = arith.constant 2.0 : f64 + %lambda = arith.constant 5.0 : f64 + qc.gphase(%phase) + qc.u(%theta, %phi, %lambda) %q : !qc.qubit + return + } + func.func @main() attributes {mqt.entry_point} { + %exponent = arith.constant 4.0 : f64 + %control = qc.alloc : !qc.qubit + %target = qc.alloc : !qc.qubit + qc.ctrl(%control) targets(%ctrl_arg = %target) { + qc.pow(%exponent) (%pow_arg = %ctrl_arg) { + qc.call @phased_rotation(%pow_arg) : !qc.qubit + qc.yield + } : !qc.qubit + qc.yield + } : {!qc.qubit}, {!qc.qubit} + qc.dealloc %target : !qc.qubit + qc.dealloc %control : !qc.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 = QCProgram::fromModule(ownedContext, std::move(moduleOp)); + ASSERT_TRUE(program); + + for (const auto profile : {QIRProfile::Base, QIRProfile::Adaptive}) { + auto qir = std::move(program->copy()).intoQIR(profile); + ASSERT_TRUE(qir); + EXPECT_NE(qir->str().find("llvm.call @__quantum__qis__cu3__body"), + std::string::npos); + size_t relativePhaseCalls = 0; + qir->module().walk([&](LLVM::CallOp call) { + if (call.getCallee() != "__quantum__qis__p__body") { + return; + } + llvm::APFloat angle(0.0); + ASSERT_TRUE(matchPattern(call.getOperand(0), m_ConstantFloat(&angle))); + if (angle.convertToDouble() == 1.0) { + ++relativePhaseCalls; + } + }); + EXPECT_EQ(relativePhaseCalls, 1U); + } +} + +TEST_F(CompilerPipelineTest, CallerOwnedContextSupportsTextualInlining) { + constexpr 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->runPassPipeline("inline")); + size_t calls = 0; + program->module().walk([&](qco::CallOp) { ++calls; }); + EXPECT_EQ(calls, 0U); + EXPECT_TRUE(succeeded(verify(program->module()))); +} + +// Test: QIR output exposes reusable functions to QCO optimization. +TEST_F(CompilerPipelineTest, DefaultQIRPipelineInlinesBeforeQCOOptimization) { + constexpr llvm::StringLiteral source = R"mlir(module { + func.func private @hs(%q: !qc.qubit) attributes {mqt.unitary} { + qc.h %q : !qc.qubit + qc.s %q : !qc.qubit + return + } + func.func @main() attributes {mqt.entry_point} { + %two = arith.constant 2.0 : f64 + %q = qc.alloc : !qc.qubit + qc.pow(%two) (%arg = %q) { + qc.call @hs(%arg) : !qc.qubit + qc.yield + } : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } + })mlir"; + + auto input = QCProgram::fromMLIRString(source); + ASSERT_TRUE(input); + auto output = runDefaultPipeline(CompilerInput{std::move(*input)}, + ProgramFormat::QIRAdaptive); + ASSERT_TRUE(output); + EXPECT_TRUE(std::holds_alternative(*output)); +} + // Test: typed QCO-to-jeff conversion expands reusable unitary functions. TEST_F(CompilerPipelineTest, QCOProgramInlinesNestedUnitaryCallsIntoJeff) { constexpr llvm::StringLiteral source = R"mlir(module { @@ -1313,6 +1432,12 @@ TEST_F(CompilerPipelineTest, JeffBinaryRoundTripPreservesReusableFunctions) { EXPECT_EQ(std::distance(main.getOps().begin(), main.getOps().end()), 3); + for (const auto format : + {ProgramFormat::QIRBase, ProgramFormat::QIRAdaptive}) { + auto output = runDefaultPipeline(CompilerInput{restored->copy()}, format); + ASSERT_TRUE(output); + EXPECT_TRUE(std::get(*output).llvmIR()); + } auto qc = std::move(*restored).intoQC(); ASSERT_TRUE(qc); EXPECT_TRUE(succeeded(verify(qc->module())));