diff --git a/.agent/plans/jeff-reusable-functions.md b/.agent/plans/jeff-reusable-functions.md new file mode 100644 index 0000000000..24e5916973 --- /dev/null +++ b/.agent/plans/jeff-reusable-functions.md @@ -0,0 +1,132 @@ +# Preserve reusable functions in jeff programs + +This ExecPlan follows `.agent/PLANS.md`. Its progress, discoveries, decisions, +and results must stay current. Run commands from the repository root. + +## Purpose + +Preserve function definitions and calls when exchanging QCO programs with jeff. +Repeated applications of a parameterized circuit should share one helper body, +including after binary serialization. Expand a call only when a quantum modifier +requires it: jeff supports ordinary calls, but its call instruction has no +control, inverse, or power modifier. + +## Progress + +- [x] (2026-09-04) Rebase onto current `main` and inspect the updated compiler + structure and pinned jeff reader. +- [x] (2026-09-04) Preserve native function definitions and calls in both + directions and correct entry-point indexing. +- [x] (2026-09-04) Expand calls only under quantum modifiers and normalize + exposed global phases. +- [x] (2026-09-04) Remove call-graph ordering and recursion restrictions now + that the pinned reader registers all function signatures before bodies. +- [x] (2026-09-04) Add binary-roundtrip, metadata, mutable-register, and + controlled-phase regressions. +- [x] (2026-09-04) Build and pass all 151 jeff conversion tests and all 158 + compiler tests against the dependency revision pinned by current `main`. +- [ ] Run repository lint on the final stack and record the result here. + +## Surprises & Discoveries + +The jeff schema represents complete programs as a function table and a +designated entry function. It supports function calls directly. The pinned +jeff-mlir dependency serializes and reads `func.func` and `func.call`; no new +schema or private metadata is needed. + +The entry-point attribute indexes the function table, not the string table. The +old conversions confused those tables. This can select a wrong function when a +custom gate adds a string or when multiple functions are present. + +The pinned reader now registers every function signature before reading bodies. +Forward references and recursion therefore need no exporter-specific ordering or +analysis. + +## Decision Log + +Use MLIR's function, call, and return type-conversion patterns. Add only the +conversion from `qco.call` to `func.call`. Strip `mqt.unitary` on export because +the verifier describes QC/QCO bodies, not jeff bodies. Do not infer this marker +on import. A complete imported program has one public entry point; make its +other functions private so downstream inlining can remove unused helpers. + +Use `inlineCall` only under QC/QCO modifiers, through the existing +`unroll-modifiers` pass. Its native inliner callback collects newly exposed +calls without rescanning the whole program or removing unrelated symbols. +Register promised inliner interfaces in the pass's dependent-dialect registry. +Normalize newly exposed global phases before distributing modifiers. + +Preserve live function signatures in QC/QCO cleanup. Omit native dead-value +removal there because it erases unused private-function arguments, including +required borrowed qubits. Keep canonicalization, CSE, and register shrinking. + +Keep source function order. Reject declarations and multiple outer blocks +because the serializer requires defined single-block functions. Reject mutable +classical-register helper arguments: their reference semantics cannot be +represented by passing a jeff array value without an explicit ABI. + +## Scope and implementation + +`mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp` owns helper signature conversion, +native calls, and serialized module metadata. +`mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp` owns the inverse type conversion, +entry-point lookup, and private helper visibility. Keep these changes in the +existing conversion libraries. + +`mlir/lib/Dialect/MQT/Transforms/UnrollModifiers.cpp` owns call expansion under +modifiers. QC and QCO dialect inliner interfaces supply the legality rules; +verified unitary functions are private, defined, nonrecursive, and single-block. +Do not add a full-program flattening pass or change caller-owned contexts in the +compiler API. Keep ordinary calls and helper definitions intact. + +The compiler regression in `mlir/unittests/Compiler/test_compiler_pipeline.cpp` +exports a parameterized helper used twice, serializes it, imports it, and +converts it to QC. The test checks function count, call count, gate body, +visibility, and entry-point identity. Include a custom gate so function and +string indexes differ. +`mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp` owns the +controlled-helper phase regression and metadata diagnostics, including a forward +reference that exercises the corrected reader. + +## Milestones and validation + +First build the conversion and compiler tests: + + cmake --preset release + cmake --build --preset release --target mqt-core-mlir-unittest-jeff-round-trip mqt-core-mlir-unittests-compiler -j4 + +Run focused tests, then their complete binaries: + + build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler --gtest_filter='CompilerPipelineTest.*Jeff*' + build/release/mlir/unittests/Conversion/JeffRoundTrip/mqt-core-mlir-unittest-jeff-round-trip + build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler + +Both native calls must survive the binary roundtrip with one shared helper body. +The controlled helper must retain its relative phase and controlled gate. Input +and successful output must verify. QC conversion must remove positional +pass-through quantum results while retaining helper calls. + +Finish with: + + uvx nox -s cpp-lint + uvx nox -s lint + +Keep target and QIR integration tests in the subsequent stack layers. Those +changes own flattening required by their output formats and must exercise +imported private jeff helpers. + +## Recovery and coordination + +Builds and tests are repeatable. Preserve unrelated work and do not modify +another task's worktree. Do not change the external jeff-mlir dependency without +coordination. This plan does not authorize remote actions. Inspect the complete +commit message, sign commits, and verify signatures before publication. + +## Outcomes & Retrospective + +The focused build and tests pass. The rebased design relies on the format's +native function table and corrected dependency reader, so no call graph, +function reordering, recursion policy, or private metadata is needed. Ordinary +calls remain reusable; only calls inside unsupported quantum modifiers are +expanded. An independent jeff/MLIR review removed one unnecessary whole-module +phase-normalization run when no candidate call can be inlined. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e2cc9f384..81ac13cec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,8 +49,8 @@ releases may include breaking changes. collection ([#2031], [#2133], [#2140], [#2150], [#2175], [#2176], [#2178]) ([**@burgholzer**], [**@simon1hofmann**]) - ✨ Add conversions between `jeff` and QCO ([#1479], [#1548], [#1565], [#1637], - [#1676], [#1706], [#1776], [#1836], [#1934], [#2000], [#2018], [#2105]) - ([**@denialhaag**], [**@burgholzer**]) + [#1676], [#1706], [#1776], [#1836], [#1934], [#2000], [#2018], [#2105], + [#2339]) ([**@denialhaag**], [**@burgholzer**]) - ✨ Add QIR generation support to the MQT Compiler Collection ([#1264], [#1446], [#1513], [#1521], [#1548], [#1567], [#1569], [#1570], [#1572], [#1580], [#1620], [#1624], [#1626], [#1648], [#1710], [#1751], [#1755], @@ -886,6 +886,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 +[#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 [#2335]: https://github.com/munich-quantum-toolkit/core/pull/2335 diff --git a/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td b/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td index b5143e0f6e..f636853e0b 100644 --- a/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td +++ b/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td @@ -21,12 +21,17 @@ def JeffToQCO : Pass<"jeff-to-qco", "mlir::ModuleOp"> { - Only specific `CustomOp`s are currently supported - Only specific `PPROp`s are currently supported - - Support for multiple functions is currently limited + - Mutable classical-register arguments in helper functions are not supported + + Function definitions and calls are preserved. Only the designated entry + point remains public; helper functions become private. Calls remain generic + `func.call` operations because jeff does not specify function unitarity. }]; let dependentDialects = ["mlir::arith::ArithDialect", - "mlir::cbit::CBitDialect", "mlir::math::MathDialect", - "mlir::mqt::MQTDialect", "mlir::scf::SCFDialect", + "mlir::func::FuncDialect", "mlir::cbit::CBitDialect", + "mlir::math::MathDialect", "mlir::mqt::MQTDialect", + "mlir::scf::SCFDialect", "mlir::tensor::TensorDialect", "mlir::qco::QCODialect", "mlir::qtensor::QTensorDialect", diff --git a/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td b/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td index 979f010250..3c4bf13f41 100644 --- a/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td +++ b/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td @@ -15,12 +15,16 @@ def QCOToJeff : Pass<"qco-to-jeff", "mlir::ModuleOp"> { This pass converts all operations from the QCO dialect to their equivalent operations in the `jeff` dialect. It ensures that the returned module is a valid `jeff` module that can be serialized. + Defined single-block functions and their calls are preserved. Mutable + classical-register arguments in helpers are not supported. Unitary calls + inside quantum modifiers must be expanded with `unroll-modifiers` first. + Note that this pass is still in development as QCO and `jeff` do not have full feature parity yet. `qco.static` is currently converted to `jeff.qubit_alloc` because `jeff` does not yet represent static (index-based) qubits. As the index is not preserved in `jeff`, it is not possible to round-tripping static qubits. }]; - let dependentDialects = ["mlir::jeff::JeffDialect", + let dependentDialects = ["mlir::func::FuncDialect", "mlir::jeff::JeffDialect", "mlir::arith::ArithDialect"]; } diff --git a/mlir/include/mlir/Dialect/MQT/Transforms/Passes.td b/mlir/include/mlir/Dialect/MQT/Transforms/Passes.td index 5b4685d8b4..fe0a5bfd2e 100644 --- a/mlir/include/mlir/Dialect/MQT/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/MQT/Transforms/Passes.td @@ -38,6 +38,10 @@ def UnrollModifiers : Pass<"unroll-modifiers", "mlir::ModuleOp"> { let dependentDialects = ["mlir::qc::QCDialect", "mlir::qco::QCODialect"]; let summary = "Unroll multi-operation modifiers into single-operation ones"; let description = [{ + Expands unitary function calls inside quantum modifiers and normalizes + exposed global phases before splitting modifiers. Calls outside modifiers + remain reusable. Functions marked `no_inline` remain calls. + Splits `ctrl` and `inv` modifiers whose body holds more than one unitary operation into a sequence of modifiers that each hold a single operation. For `inv`, the order of the operations is reversed. Classical operations of @@ -51,7 +55,7 @@ def UnrollModifiers : Pass<"unroll-modifiers", "mlir::ModuleOp"> { them. Modifiers nested in a `pow` body are unrolled either way. If a modifier cannot be unrolled, the modifier is left untouched. Such - modifiers are skipped silently; the pass never fails. + modifiers are skipped silently. }]; } diff --git a/mlir/include/mlir/Support/Passes.h b/mlir/include/mlir/Support/Passes.h index cd8e583e65..5ca5126ce0 100644 --- a/mlir/include/mlir/Support/Passes.h +++ b/mlir/include/mlir/Support/Passes.h @@ -20,11 +20,9 @@ class OpPassManager; class PassManager; } // namespace mlir -/** - * @brief Populate the pass manager and run it on the module. - */ +/// Populate the pass manager and run it on the module. mlir::LogicalResult runWithPassManager( - mlir::ModuleOp module, + mlir::ModuleOp moduleOp, mlir::function_ref populatePasses, mlir::StringRef errorMessage); @@ -46,13 +44,13 @@ void populateDecomposeMultiControlledPipeline(mlir::OpPassManager& pm, /// Parse and run a module-level MLIR textual pass pipeline. [[nodiscard]] mlir::LogicalResult -runPassPipeline(mlir::ModuleOp module, mlir::StringRef pipeline, +runPassPipeline(mlir::ModuleOp moduleOp, mlir::StringRef pipeline, bool enableTiming = false, bool enableStatistics = false); -/** - * @brief Populate a QC-oriented cleanup pipeline on the given pass manager. - * @details Adds generic cleanup and QC qubit-register shrinking. - */ +/// Populate a QC-oriented cleanup pipeline on the given pass manager. +/// +/// Adds generic cleanup and QC qubit-register shrinking while +/// preserving live function signatures. void populateQCCleanupPipeline(mlir::OpPassManager& pm); /// Run QC cleanup that preserves defined values on every syntactic loop edge. @@ -60,44 +58,34 @@ void populateQCCleanupPipeline(mlir::OpPassManager& pm); /// RemoveDeadValues, even when those edges are unreachable. void populateQCExportPipeline(mlir::OpPassManager& pm); -/** - * @brief Populate a QCO-oriented cleanup pipeline on the given pass manager. - * @details Adds generic cleanup and qtensor shrink-to-fit. - */ +/// Populate a QCO-oriented cleanup pipeline on the given pass manager. +/// +/// Adds generic cleanup and qtensor shrink-to-fit while +/// preserving live function signatures. void populateQCOCleanupPipeline(mlir::OpPassManager& pm); -/** - * @brief Populate a QIR-oriented cleanup pipeline on the given pass manager. - * @details Adds generic cleanup and QIR-specific simplifications. Updates the - * meta data accordingly. - */ +/// Populate a QIR-oriented cleanup pipeline on the given pass manager. +/// +/// Adds generic cleanup and QIR-specific simplifications. Updates the +/// metadata accordingly. void populateQIRCleanupPipeline(mlir::OpPassManager& pm, bool useAdaptive); -/** - * @brief Populate a `jeff`-oriented cleanup pipeline on the given pass manager. - * @details Adds generic cleanup and dead-value removal. This matches the QCO - * cleanup minus the QTensor-specific shrink pass, as QTensor operations no - * longer exist once lowered into the `jeff` dialect. - */ +/// Populate a `jeff`-oriented cleanup pipeline on the given pass manager. +/// +/// Adds generic cleanup and dead-value removal after lowering to jeff. void populateJeffCleanupPipeline(mlir::OpPassManager& pm); -/** - * @brief Run the QC-oriented cleanup pipeline on a module. - */ -[[nodiscard]] mlir::LogicalResult runQCCleanupPipeline(mlir::ModuleOp module); +/// Run the QC-oriented cleanup pipeline on a module. +[[nodiscard]] mlir::LogicalResult runQCCleanupPipeline(mlir::ModuleOp moduleOp); -/** - * @brief Run the QCO-oriented cleanup pipeline on a module. - */ -[[nodiscard]] mlir::LogicalResult runQCOCleanupPipeline(mlir::ModuleOp module); +/// Run the QCO-oriented cleanup pipeline on a module. +[[nodiscard]] mlir::LogicalResult +runQCOCleanupPipeline(mlir::ModuleOp moduleOp); -/** - * @brief Run the QIR-oriented cleanup pipeline on a module. - */ -[[nodiscard]] mlir::LogicalResult runQIRCleanupPipeline(mlir::ModuleOp module, +/// Run the QIR-oriented cleanup pipeline on a module. +[[nodiscard]] mlir::LogicalResult runQIRCleanupPipeline(mlir::ModuleOp moduleOp, bool useAdaptive); -/** - * @brief Run the `jeff`-oriented cleanup pipeline on a module. - */ -[[nodiscard]] mlir::LogicalResult runJeffCleanupPipeline(mlir::ModuleOp module); +/// Run the `jeff`-oriented cleanup pipeline on a module. +[[nodiscard]] mlir::LogicalResult +runJeffCleanupPipeline(mlir::ModuleOp moduleOp); diff --git a/mlir/lib/Conversion/JeffToQCO/CMakeLists.txt b/mlir/lib/Conversion/JeffToQCO/CMakeLists.txt index f1a712a5cd..c292f5c2a2 100644 --- a/mlir/lib/Conversion/JeffToQCO/CMakeLists.txt +++ b/mlir/lib/Conversion/JeffToQCO/CMakeLists.txt @@ -16,6 +16,7 @@ add_mlir_conversion_library( LINK_LIBS MLIRJeff MLIRJeffToNative + MLIRFuncTransforms MLIRCBitDialect MLIRMQTDialect MLIRQCODialect diff --git a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp index 0d12b55deb..37576bf89b 100644 --- a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp +++ b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp @@ -221,10 +221,8 @@ static void createBarrierOp(jeff::CustomOp& op, jeff::CustomOpAdaptor& adaptor, } } -/** - * @brief Gets the name of the entry point from the module attributes - */ -static FailureOr getEntryPointName(ModuleOp moduleOp) { +/// Resolve the entry point by its index in the function table. +static FailureOr getEntryPoint(ModuleOp moduleOp) { auto entryPointAttr = moduleOp->getAttrOfType("jeff.entrypoint"); if (!entryPointAttr || !entryPointAttr.getType().isUnsignedInteger()) { return moduleOp.emitError( @@ -232,20 +230,14 @@ static FailureOr getEntryPointName(ModuleOp moduleOp) { } auto entryPoint = entryPointAttr.getUInt(); - auto stringsAttr = moduleOp->getAttrOfType("jeff.strings"); - if (!stringsAttr) { - return moduleOp.emitError("requires an array 'jeff.strings' attribute"); - } - - if (entryPoint >= stringsAttr.size()) { - return moduleOp.emitError("'jeff.entrypoint' index is out of bounds"); - } - - auto name = dyn_cast(stringsAttr[entryPoint]); - if (!name) { - return moduleOp.emitError("'jeff.entrypoint' must index a string"); + for (auto [index, function] : + llvm::enumerate(moduleOp.getOps())) { + if (index == entryPoint) { + return function; + } } - return name.getValue(); + return moduleOp.emitError( + "'jeff.entrypoint' function index is out of bounds"); } /** @@ -1340,12 +1332,15 @@ struct ConvertJeffYieldOpToQCO final : OpConversionPattern { * ``` */ struct ConvertJeffMainToQCO final : OpConversionPattern { - using OpConversionPattern::OpConversionPattern; + ConvertJeffMainToQCO(TypeConverter& typeConverter, MLIRContext* context, + func::FuncOp entryPoint) + : OpConversionPattern(typeConverter, context, PatternBenefit(2)), + entryPoint_(entryPoint) {} LogicalResult matchAndRewrite(func::FuncOp op, OpAdaptor /*adaptor*/, ConversionPatternRewriter& rewriter) const override { - if (op.getSymName() != getEntryPointName(op->getParentOfType())) { + if (op != entryPoint_) { return failure(); } @@ -1368,7 +1363,7 @@ struct ConvertJeffMainToQCO final : OpConversionPattern { return failure(); } - /// A result-less jeff entry point uses the compiler's legacy status result. + // A result-less jeff entry point uses the compiler's legacy status result. const bool needsStatusResult = resultTypes.empty(); if (needsStatusResult) { resultTypes.push_back(rewriter.getI64Type()); @@ -1391,6 +1386,9 @@ struct ConvertJeffMainToQCO final : OpConversionPattern { return success(); } + +private: + func::FuncOp entryPoint_; }; /** @@ -1433,8 +1431,8 @@ struct JeffToQCO final : impl::JeffToQCOBase { void runOnOperation() override { MLIRContext* context = &getContext(); auto moduleOp = getOperation(); - auto entryPointName = getEntryPointName(moduleOp); - if (failed(entryPointName)) { + auto entryPoint = getEntryPoint(moduleOp); + if (failed(entryPoint)) { signalPassFailure(); return; } @@ -1480,6 +1478,23 @@ struct JeffToQCO final : impl::JeffToQCOBase { RewritePatternSet patterns(context); JeffToQCOTypeConverter typeConverter(context); + for (auto function : moduleOp.getOps()) { + if (function == *entryPoint) { + function.setPublic(); + continue; + } + if (llvm::any_of(function.getArgumentTypes(), [&](Type type) { + return isa(typeConverter.convertType(type)); + })) { + function.emitError("classical register arguments in helper functions " + "are not supported"); + signalPassFailure(); + return; + } + // A jeff module is a complete program with one external entry point. + function.setPrivate(); + } + // Configure conversion target target.addIllegalDialect(); target @@ -1488,23 +1503,25 @@ struct JeffToQCO final : impl::JeffToQCOBase { tensor::TensorDialect, scf::SCFDialect>(); target.addDynamicallyLegalOp([&](func::FuncOp op) { - return (op.getSymName() != *entryPointName || mqt::isEntryPoint(op)) && + return (op != *entryPoint || mqt::isEntryPoint(op)) && typeConverter.isSignatureLegal(op.getFunctionType()) && typeConverter.isLegal(&op.getBody()); }); - target.addDynamicallyLegalOp( - [&](func::ReturnOp op) { return typeConverter.isLegal(op); }); + target.addDynamicallyLegalOp( + [&](Operation* op) { return typeConverter.isLegal(op); }); // Register operation conversion patterns jeff::populateJeffToNativeConversionPatterns(patterns); populateFunctionOpInterfaceTypeConversionPattern( patterns, typeConverter); populateReturnOpTypeConversionPattern(patterns, typeConverter); + populateCallOpTypeConversionPattern(patterns, typeConverter); + patterns.add(typeConverter, context, *entryPoint); patterns.add(typeConverter, context, sharedArrayUpdates); patterns.add( - typeConverter, context, PatternBenefit(2)); + ConvertJeffIntArrayGetIndexOpToCBit>(typeConverter, context, + PatternBenefit(2)); patterns.add< ConvertJeffQuregAllocOpToQCO, ConvertJeffQuregExtractIndexOpToQCO, ConvertJeffQuregInsertIndexOpToQCO, ConvertJeffQuregFreeZeroOpToQCO, diff --git a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp index 8f761e3ae5..2029bfba5c 100644 --- a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp +++ b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp @@ -59,6 +59,7 @@ #include #include #include +#include #include #include #include @@ -474,25 +475,30 @@ static LogicalResult cleanUp(ModuleOp moduleOp, LoweringState& state) { return failure(); } - for (auto funcOp : moduleOp.getOps()) { - state.strings.emplace_back(funcOp.getSymName()); + std::optional entryPoint; + for (auto [index, function] : + llvm::enumerate(moduleOp.getOps())) { + if (index > std::numeric_limits::max()) { + return moduleOp.emitError( + "too many functions for the jeff function table"); + } + state.strings.emplace_back(function.getSymName()); + if (function.getSymName() == state.entryPointName) { + entryPoint = static_cast(index); + } } - - auto* const it = llvm::find(state.strings, state.entryPointName); - if (it == state.strings.end()) { + if (!entryPoint) { return failure(); } - const auto distance = std::distance(state.strings.begin(), it); - if (std::cmp_greater(distance, std::numeric_limits::max())) { - return failure(); + if (state.strings.size() > size_t{std::numeric_limits::max()} + 1) { + return moduleOp.emitError("too many strings for the jeff string table"); } - const auto entryPoint = static_cast(distance); OpBuilder builder(moduleOp.getContext()); auto uint16Type = builder.getIntegerType(16, false); moduleOp->setAttr("jeff.entrypoint", - builder.getIntegerAttr(uint16Type, entryPoint)); + builder.getIntegerAttr(uint16Type, *entryPoint)); SmallVector stringRefs; stringRefs.reserve(state.strings.size()); @@ -2124,61 +2130,28 @@ struct ConvertSCFWhileOpToJeff final } }; -/** - * @brief Converts the QCO-style main function to a `jeff`-style main function - * - * @par Example: - * ```mlir - * func.func @main() -> i64 attributes {mqt.entry_point} { ... } - * ``` - * is converted to - * ```mlir - * func.func @main() -> i64 { ... } - * ``` - */ -struct ConvertQCOMainToJeff final : StatefulOpConversionPattern { +/// Preserve a unitary call as a native jeff function call. +struct ConvertQCOCallToJeff final : StatefulOpConversionPattern { using StatefulOpConversionPattern::StatefulOpConversionPattern; LogicalResult - matchAndRewrite(func::FuncOp op, OpAdaptor /*adaptor*/, + matchAndRewrite(qco::CallOp op, OpAdaptor adaptor, ConversionPatternRewriter& rewriter) const override { - if (!mqt::isEntryPoint(op)) { - return failure(); - } - - if (op.getBlocks().size() != 1) { - return failure(); - } - auto* block = &op.getBlocks().front(); - - auto* returnOp = block->getTerminator(); - if (!isa(returnOp)) { - return failure(); - } - - getState().entryPointName = op.getSymName(); - - auto funcType = op.getFunctionType(); - SmallVector newInputs; - if (failed(getTypeConverter()->convertTypes(funcType.getInputs(), - newInputs))) { - return failure(); - } - SmallVector newResults; - if (failed(getTypeConverter()->convertTypes(funcType.getResults(), - newResults))) { - return failure(); + if (getState().inModifier()) { + return rewriter.notifyMatchFailure( + op, "modified calls must be expanded first"); } - + SmallVector results; if (failed( - rewriter.convertRegionTypes(&op.getBody(), *getTypeConverter()))) { + getTypeConverter()->convertTypes(op.getResultTypes(), results))) { return failure(); } - rewriter.startOpModification(op); - op.setType(rewriter.getFunctionType(newInputs, newResults)); - mqt::removeEntryPoint(op); - rewriter.finalizeOpModification(op); - + auto argAttrs = op.getArgAttrsAttr(); + auto resAttrs = op.getResAttrsAttr(); + auto call = rewriter.replaceOpWithNewOp( + op, op.getCallee(), results, adaptor.getOperands()); + call.setArgAttrsAttr(argAttrs); + call.setResAttrsAttr(resAttrs); return success(); } }; @@ -2378,6 +2351,26 @@ struct QCOToJeff final : impl::QCOToJeffBase { QCOToJeffTypeConverter typeConverter(context); LoweringState state; + for (auto function : moduleOp.getOps()) { + if (function.isExternal() || !function.getBody().hasOneBlock() || + !isa(function.getBody().front().getTerminator())) { + function.emitError("jeff export requires single-block definitions " + "ending in func.return"); + signalPassFailure(); + return; + } + if (mqt::isEntryPoint(function)) { + state.entryPointName = function.getSymName(); + mqt::removeEntryPoint(function); + } else if (llvm::any_of(function.getArgumentTypes(), + llvm::IsaPred)) { + function.emitError("classical register arguments in helper functions " + "are not supported"); + signalPassFailure(); + return; + } + function->removeAttr(mqt::MQTDialect::UnitaryAttrHelper::getNameStr()); + } state.cbitState.recordRegisterUses(moduleOp); // Configure conversion target @@ -2389,15 +2382,14 @@ struct QCOToJeff final : impl::QCOToJeffBase { target.addIllegalOp(); target.addDynamicallyLegalOp([&](func::FuncOp op) { - return !mqt::isEntryPoint(op) && - typeConverter.isSignatureLegal(op.getFunctionType()) && + return typeConverter.isSignatureLegal(op.getFunctionType()) && typeConverter.isLegal(&op.getBody()); }); - target.addDynamicallyLegalOp([&](func::ReturnOp op) { - return typeConverter.isLegal(op.getOperandTypes()); - }); + target.addDynamicallyLegalOp( + [&](Operation* op) { return typeConverter.isLegal(op); }); populateFunctionOpInterfaceTypeConversionPattern( patterns, typeConverter); + populateCallOpTypeConversionPattern(patterns, typeConverter); // Register operation conversion patterns jeff::populateNativeToJeffConversionPatterns(patterns); @@ -2477,7 +2469,7 @@ struct QCOToJeff final : impl::QCOToJeffBase { ConvertQCOInvOpToJeff, ConvertQCOPowOpToJeff, ConvertQCOYieldOpToJeff, ConvertIfOpToJeff, ConvertIfOpToJeff, ConvertSCFForOpToJeff, - ConvertSCFWhileOpToJeff, ConvertQCOMainToJeff, + ConvertSCFWhileOpToJeff, ConvertQCOCallToJeff, ConvertFuncReturnOpToJeff>(typeConverter, context, &state); /// Cloned region arguments already have target types. Convert their users diff --git a/mlir/lib/Dialect/MQT/Transforms/CMakeLists.txt b/mlir/lib/Dialect/MQT/Transforms/CMakeLists.txt index 68aea6ef52..34ad4dbb1d 100644 --- a/mlir/lib/Dialect/MQT/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/MQT/Transforms/CMakeLists.txt @@ -15,12 +15,15 @@ add_mlir_library( LINK_LIBS PRIVATE MLIRArithDialect + MLIRFuncInlinerExtension MLIRIR + MLIRLLVMIRTransforms MLIRMQTUtils MLIRPass MLIRQCDialect MLIRQCODialect MLIRSideEffectInterfaces + MLIRTransformUtils DEPENDS MLIRMQTTransformsIncGen) diff --git a/mlir/lib/Dialect/MQT/Transforms/UnrollModifiers.cpp b/mlir/lib/Dialect/MQT/Transforms/UnrollModifiers.cpp index cd3a63767b..aa190a9741 100644 --- a/mlir/lib/Dialect/MQT/Transforms/UnrollModifiers.cpp +++ b/mlir/lib/Dialect/MQT/Transforms/UnrollModifiers.cpp @@ -8,6 +8,7 @@ * Licensed under the MIT License */ +#include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/MQT/Transforms/Passes.h" #include "mlir/Dialect/MQT/Utils/GatePowering.h" #include "mlir/Dialect/MQT/Utils/Modifiers.h" @@ -22,18 +23,24 @@ #include #include #include +#include +#include +#include #include #include #include #include #include +#include #include #include #include #include #include +#include #include +#include #define DEBUG_TYPE "unroll-modifiers" @@ -333,9 +340,67 @@ static LogicalResult unrollModifier(qco::PowOp op, RewriterBase& rewriter) { namespace { +struct ModifierInliner final : InlinerInterface { + using InlinerInterface::InlinerInterface; + + SmallVector calls; + + void processInlinedBlocks(iterator_range blocks) override { + for (auto& block : blocks) { + block.walk([&](CallOpInterface call) { + if (isa(call.getOperation())) { + calls.push_back(call); + } + }); + } + } +}; + struct UnrollModifiers final : impl::UnrollModifiersBase { + void getDependentDialects(DialectRegistry& registry) const override { + UnrollModifiersBase::getDependentDialects(registry); + func::registerInlinerExtension(registry); + LLVM::registerInlinerInterface(registry); + } + protected: void runOnOperation() override { + SmallVector calls; + getOperation()->walk([&](CallOpInterface call) { + if (!isa(call.getOperation())) { + return; + } + for (auto* parent = call->getParentOp(); parent != nullptr; + parent = parent->getParentOp()) { + if (isa(parent)) { + calls.push_back(call); + return; + } + } + }); + if (!calls.empty()) { + ModifierInliner inliner(&getContext()); + inliner.calls = std::move(calls); + const InlinerConfig config; + SymbolTableCollection symbols; + bool changed = false; + while (!inliner.calls.empty()) { + auto call = inliner.calls.pop_back_val(); + auto callee = dyn_cast_or_null( + call.resolveCallableInTable(&symbols)); + if (callee && succeeded(inlineCall(inliner, config.getCloneCallback(), + call, callee, &callee.getBody()))) { + call->erase(); + changed = true; + } + } + if (changed && failed(normalizeGlobalPhases(getOperation()))) { + signalPassFailure(); + return; + } + } + SmallVector modifiers; getOperation()->walk([&](Operation* op) { if (isa #include #include #include #include #include #include +#include // The following headers are needed for some template instantiations. // IWYU pragma: begin_keep @@ -30,6 +32,32 @@ using namespace mlir; using namespace mlir::qc; +namespace { + +struct QCInlinerInterface final : DialectInlinerInterface { + using DialectInlinerInterface::DialectInlinerInterface; + + bool isLegalToInline(Operation* call, Operation* callable, + bool /*wouldBeCloned*/) const final { + auto callee = dyn_cast(callable); + return isa(call) && callee && !callee.getNoInline(); + } + + bool isLegalToInline(Region* destination, Region* source, + bool /*wouldBeCloned*/, + IRMapping& /*valueMapping*/) const final { + return destination->hasOneBlock() && source->hasOneBlock(); + } + + bool isLegalToInline(Operation* /*operation*/, Region* /*destination*/, + bool /*wouldBeCloned*/, + IRMapping& /*valueMapping*/) const final { + return true; + } +}; + +} // namespace + static ParseResult parseTargetAliasing(OpAsmParser& parser, Region& region, SmallVectorImpl& operands) { @@ -48,6 +76,8 @@ static void printTargetAliasing(OpAsmPrinter& printer, Operation* /*op*/, #include "mlir/Dialect/QC/IR/QCOpsDialect.cpp.inc" void QCDialect::initialize() { + addInterfaces(); + // NOLINTNEXTLINE(clang-analyzer-core.StackAddressEscape) addTypes< #define GET_TYPEDEF_LIST diff --git a/mlir/lib/Dialect/QCO/IR/QCOOps.cpp b/mlir/lib/Dialect/QCO/IR/QCOOps.cpp index 35fe631395..38f89e982e 100644 --- a/mlir/lib/Dialect/QCO/IR/QCOOps.cpp +++ b/mlir/lib/Dialect/QCO/IR/QCOOps.cpp @@ -15,6 +15,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" // IWYU pragma: associated #include +#include #include #include #include @@ -23,6 +24,7 @@ #include #include #include +#include #include #include @@ -36,6 +38,32 @@ using namespace mlir; using namespace mlir::qco; +namespace { + +struct QCOInlinerInterface final : DialectInlinerInterface { + using DialectInlinerInterface::DialectInlinerInterface; + + bool isLegalToInline(Operation* call, Operation* callable, + bool /*wouldBeCloned*/) const final { + auto callee = dyn_cast(callable); + return isa(call) && callee && !callee.getNoInline(); + } + + bool isLegalToInline(Region* destination, Region* source, + bool /*wouldBeCloned*/, + IRMapping& /*valueMapping*/) const final { + return destination->hasOneBlock() && source->hasOneBlock(); + } + + bool isLegalToInline(Operation* /*operation*/, Region* /*destination*/, + bool /*wouldBeCloned*/, + IRMapping& /*valueMapping*/) const final { + return true; + } +}; + +} // namespace + static bool isQCOLinearType(Type type) { if (isa(type)) { return true; @@ -498,6 +526,8 @@ void QCODialect::initialize() { #include "mlir/Dialect/QCO/IR/QCOOps.cpp.inc" >(); + + addInterfaces(); } //===----------------------------------------------------------------------===// diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index a4405c8fa8..96a38b6076 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -125,7 +125,6 @@ void populateQCExportPipeline(OpPassManager& pm) { void populateQCCleanupPipeline(OpPassManager& pm) { populateQCExportPipeline(pm); - pm.addPass(createRemoveDeadValuesPass()); } void populateQCOCleanupPipeline(OpPassManager& pm) { @@ -134,7 +133,6 @@ void populateQCOCleanupPipeline(OpPassManager& pm) { pm.addPass(mlir::mqt::createNormalizeGlobalPhases()); pm.addPass(createCSEPass()); pm.addPass(qtensor::createShrinkQTensorToFitPass()); - pm.addPass(createRemoveDeadValuesPass()); } void populateQIRCleanupPipeline(OpPassManager& pm, bool useAdaptive) { diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index f911e3bc62..dcf023e095 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -1207,7 +1208,135 @@ TEST_F(CompilerPipelineTest, TypedProgramsNormalizeGlobalPhases) { EXPECT_EQ(StringRef(textual->str()).count("qco.gphase"), 1); } -// Test: jeff programs round-trip through their binary APIs +// Test: typed QCO-to-jeff conversion expands reusable unitary functions. +TEST_F(CompilerPipelineTest, QCOProgramInlinesNestedUnitaryCallsIntoJeff) { + 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} { + %two = arith.constant 2.0 : f64 + %q = qco.alloc : !qco.qubit + %out = qco.pow(%two) (%arg = %q) { + %called = qco.call @flip(%arg) : (!qco.qubit) -> !qco.qubit + qco.yield %called : !qco.qubit + } : {!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 qco = QCOProgram::fromModule(ownedContext, std::move(moduleOp)); + ASSERT_TRUE(qco); + + auto jeffProgram = std::move(*qco).intoJeff(); + ASSERT_TRUE(jeffProgram); + EXPECT_EQ(jeffProgram->str().find("qco.call"), std::string::npos); + EXPECT_TRUE(succeeded(verify(jeffProgram->module()))); + auto helper = jeffProgram->module().lookupSymbol("flip"); + ASSERT_TRUE(helper); + EXPECT_FALSE(mlir::mqt::isUnitaryFunction(helper)); + auto main = jeffProgram->module().lookupSymbol("main"); + ASSERT_TRUE(main); + EXPECT_TRUE(main.getOps().empty()); + EXPECT_EQ(std::distance(main.getOps().begin(), + main.getOps().end()), + 1); +} + +TEST_F(CompilerPipelineTest, JeffBinaryRoundTripPreservesReusableFunctions) { + constexpr llvm::StringLiteral source = R"mlir(module { + func.func @main() -> i1 attributes {mqt.entry_point} { + %angle = arith.constant 0.25 : f64 + %q = qco.alloc : !qco.qubit + %a = qco.call @rotate(%angle, %q) : (f64, !qco.qubit) -> !qco.qubit + %b = qco.call @rotate(%angle, %a) : (f64, !qco.qubit) -> !qco.qubit + %c = qco.sx %b : !qco.qubit -> !qco.qubit + %bit, %out = func.call @read(%c) : (!qco.qubit) -> (i1, !qco.qubit) + qco.sink %out : !qco.qubit + return %bit : i1 + } + func.func private @rotate(%angle: f64, %q: !qco.qubit) -> !qco.qubit + attributes {mqt.unitary} { + %out = qco.ry(%angle) %q : !qco.qubit -> !qco.qubit + return %out : !qco.qubit + } + func.func private @read(%q: !qco.qubit) -> (i1, !qco.qubit) { + %out, %bit = qco.measure %q : !qco.qubit + return %bit, %out : i1, !qco.qubit + } + })mlir"; + auto qco = QCOProgram::fromMLIRString(source.str()); + ASSERT_TRUE(qco); + auto jeffProgram = std::move(*qco).intoJeff(); + ASSERT_TRUE(jeffProgram); + EXPECT_TRUE(succeeded(verify(jeffProgram->module()))); + EXPECT_EQ(jeffProgram->module() + ->getAttrOfType("jeff.entrypoint") + .getUInt(), + 0); + auto main = jeffProgram->module().lookupSymbol("main"); + ASSERT_TRUE(main); + EXPECT_EQ(std::distance(main.getOps().begin(), + main.getOps().end()), + 3); + auto helper = jeffProgram->module().lookupSymbol("rotate"); + ASSERT_TRUE(helper); + EXPECT_EQ(std::distance(helper.getOps().begin(), + helper.getOps().end()), + 1); + + auto restoredJeff = JeffProgram::fromBytes(jeffProgram->toBytes()); + ASSERT_TRUE(restoredJeff); + auto restored = std::move(*restoredJeff).intoQCO(); + ASSERT_TRUE(restored); + EXPECT_TRUE(succeeded(verify(restored->module()))); + helper = restored->module().lookupSymbol("rotate"); + ASSERT_TRUE(helper); + EXPECT_TRUE(helper.isPrivate()); + EXPECT_FALSE(mlir::mqt::isUnitaryFunction(helper)); + EXPECT_EQ(std::distance(helper.getOps().begin(), + helper.getOps().end()), + 1); + main = mlir::mqt::getEntryPoint(restored->module()); + ASSERT_TRUE(main); + EXPECT_EQ(main.getSymName(), "main"); + EXPECT_EQ(std::distance(main.getOps().begin(), + main.getOps().end()), + 3); + auto qc = std::move(*restored).intoQC(); + ASSERT_TRUE(qc); + EXPECT_TRUE(succeeded(verify(qc->module()))); + helper = qc->module().lookupSymbol("rotate"); + ASSERT_TRUE(helper); + EXPECT_EQ(helper.getNumResults(), 0); +} + +TEST_F(CompilerPipelineTest, JeffRejectsMutableClassicalHelperArguments) { + auto qco = QCOProgram::fromMLIRString(R"mlir(module { + func.func private @helper(%bits: !cbit.reg<1>) { + return + } + func.func @main() attributes {mqt.entry_point} { + %q = qco.alloc : !qco.qubit + qco.sink %q : !qco.qubit + return + } + })mlir"); + ASSERT_TRUE(qco); + EXPECT_FALSE(std::move(*qco).intoJeff()); +} + +// Test: jeff programs round-trip through their binary APIs. TEST_F(CompilerPipelineTest, JeffProgramsRoundTripThroughBytesAndFiles) { const std::string qasm = R"(OPENQASM 3.0; include "stdgates.inc"; diff --git a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp index 664e12ada5..52aefe61a3 100644 --- a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp +++ b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp @@ -369,6 +369,60 @@ static LogicalResult convertJeffToQCO(ModuleOp moduleOp) { return pm.run(moduleOp); } +TEST(JeffRoundTripRegressionTest, PreservesPhaseOfControlledFunctionCall) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(R"mlir(module { + func.func private @phased_x(%q: !qco.qubit) -> !qco.qubit + attributes {mqt.unitary} { + %phase = arith.constant 0.25 : f64 + qco.gphase(%phase) + %out = qco.call @flip(%q) : (!qco.qubit) -> !qco.qubit + return %out : !qco.qubit + } + 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} { + %control = qco.alloc : !qco.qubit + %target = qco.alloc : !qco.qubit + %c, %q = qco.ctrl(%control) targets(%arg = %target) { + %out = qco.call @phased_x(%arg) : (!qco.qubit) -> !qco.qubit + qco.yield %out : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) -> ({!qco.qubit}, {!qco.qubit}) + qco.sink %c : !qco.qubit + qco.sink %q : !qco.qubit + return + } + })mlir", + &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(convertQCOToJeff(*moduleOp))); + EXPECT_TRUE(succeeded(verify(*moduleOp))); + auto main = moduleOp->lookupSymbol("main"); + ASSERT_TRUE(main); + auto phases = llvm::to_vector(main.getOps()); + ASSERT_EQ(phases.size(), 1); + auto angle = + phases.front().getRotation().getDefiningOp(); + ASSERT_TRUE(angle); + EXPECT_DOUBLE_EQ(angle.getVal().convertToDouble(), 0.25); + EXPECT_EQ(phases.front().getNumCtrls(), 0); + EXPECT_FALSE(phases.front().getIsAdjoint()); + auto gates = llvm::to_vector(main.getOps()); + ASSERT_EQ(gates.size(), 1); + EXPECT_EQ(gates.front().getNumCtrls(), 1); + EXPECT_TRUE(main.getOps().empty()); + auto restored = deserialize(&context, serialize(*moduleOp).asPtr()); + ASSERT_TRUE(restored); + EXPECT_TRUE(succeeded(verify(*restored))); +} + TEST(JeffRoundTripRegressionTest, RejectsInvalidJeffModuleMetadata) { DialectRegistry registry; registry.insert(source, &context); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); - ASSERT_TRUE(succeeded(convertQCOToJeff(*module))); - ASSERT_TRUE(succeeded(verify(*module))); - ASSERT_TRUE(succeeded(convertJeffToQCO(*module))); - EXPECT_TRUE(succeeded(verify(*module))); - auto function = *module->getOps().begin(); + ASSERT_TRUE(succeeded(convertQCOToJeff(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(convertJeffToQCO(*moduleOp))); + EXPECT_TRUE(succeeded(verify(*moduleOp))); + auto function = *moduleOp->getOps().begin(); EXPECT_TRUE(function.getResultTypes().front().isInteger(64)); } @@ -759,10 +802,10 @@ module { } } )mlir"; - auto module = parseSourceString(source, &context); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - EXPECT_TRUE(failed(convertQCOToJeff(*module))); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_TRUE(failed(convertQCOToJeff(*moduleOp))); } TEST_P(JeffRoundTripTest, ProgramEquivalence) { diff --git a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp index 9b984d793d..0a698a5814 100644 --- a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp +++ b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp @@ -2146,6 +2146,37 @@ static LogicalResult runUnrollModifiers(ModuleOp moduleOp) { return pm.run(moduleOp); } +TEST_F(QCTest, UnrollModifiersInlinesUnitaryCalls) { + auto moduleOp = parseSourceString(R"mlir(module { + func.func private @flip(%q: !qc.qubit) attributes {mqt.unitary} { + qc.x %q : !qc.qubit + return + } + func.func @main(%q: !qc.qubit) { + %two = arith.constant 2.0 : f64 + qc.pow(%two) (%arg = %q) { + qc.call @flip(%arg) : !qc.qubit + qc.yield + } : !qc.qubit + return + } + })mlir", + context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(runUnrollModifiers(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + auto main = moduleOp->lookupSymbol("main"); + ASSERT_TRUE(main); + size_t calls = 0; + size_t xGates = 0; + main.walk([&](Operation* op) { + calls += isa(op); + xGates += isa(op); + }); + EXPECT_EQ(calls, 0); + EXPECT_EQ(xGates, 1); +} + /// Unrolls @p program and checks that it matches @p reference. static void expectUnrollsTo(MLIRContext* context,