Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .agent/plans/reusable-function-lowering.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 0 additions & 7 deletions bindings/mlir/qiskit/QiskitExport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2816,13 +2816,6 @@ collectGateFunctions(mlir::ModuleOp moduleOp, mlir::func::FuncOp entryPoint) {
ordered.push_back(function);
}
}
for (auto function : moduleOp.getOps<mlir::func::FuncOp>()) {
if (!gateCallDepths.contains(graph.lookupNode(&function.getBody()))) {
throw std::runtime_error("Qiskit circuit export cannot preserve "
"function '" +
function.getName().str() + "'");
}
}
return ordered;
}

Expand Down
11 changes: 7 additions & 4 deletions docs/mlir/target_compilation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 7 additions & 8 deletions mlir/include/mlir/Compiler/TargetCompilation.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
4 changes: 2 additions & 2 deletions mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}];
Expand Down
6 changes: 2 additions & 4 deletions mlir/include/mlir/Support/Passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion mlir/lib/Compiler/Pipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions mlir/lib/Compiler/TargetCompilation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 4 additions & 5 deletions mlir/lib/Dialect/MQT/IR/MQTDialect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -293,14 +293,13 @@ verifyEntryPoint(Operation* operation, const NamedAttribute attribute) {
<< "' must be a unit attribute";
}

auto function = dyn_cast<FunctionOpInterface>(operation);
auto function = dyn_cast<func::FuncOp>(operation);
auto moduleOp = operation->getParentOfType<ModuleOp>();
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()) {
Expand Down
54 changes: 32 additions & 22 deletions mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<CallGraphNode*,
llvm::GraphTraits<const CallGraphNode*>>::
begin(callGraph.lookupNode(&function.getBody()));
!component.isAtEnd(); ++component) {
auto* node = component->front();
if (node->isExternal()) {
continue;
Expand All @@ -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<func::FuncOp>(current));
if (current != function) {
auto gate = dyn_cast<func::FuncOp>(current);
if (!gate) {
return fail(current, "gate calls must target func.func definitions");
}
gateFunctions_.push_back(gate);
}
}
return success();
Expand Down Expand Up @@ -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, "
Expand Down Expand Up @@ -348,24 +352,30 @@ class OpenQASMEmitter {
? requested.str()
: uniqueName("gate", nextHelper));
}
const auto walkResult = moduleOp.walk([&](Operation* operation) {
if (isa<func::CallOp, qc::CallOp>(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<func::CallOp, qc::CallOp>(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()) {
if (!isa<func::FuncOp>(operation)) {
return fail(&operation, "only functions may appear at module scope");
}
}
return orderGateFunctions();
return success();
}

[[nodiscard]] LogicalResult collectProgramShape() {
Expand Down
4 changes: 4 additions & 0 deletions mlir/lib/Support/Passes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions mlir/tools/mqt-cc/mqt-cc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading